3.1 找出数组中重复的数字

题目描述

在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。

题解

对数组进行移位,判断i下标的数字等于i,如果不等则对其进行交换,eg:

{13,4,6,5,2,5} –> {3,1,4,6,5,2,5} //下标为0的数字为1,因此将下标为1的数字即3与1换位,使得1到了其应在的地方

{3,1,4,6,5,2,5} –> {6,1,4,3,5,2,5} //此时下标为0的数字为3,依然不等于其下标,因此找到下标为3的数字6与其交换,使3到了其应在的地方

{6,1,4,3,5,2,5} –> {5,1,4,3,5,2,6} //同理

{5,1,4,3,5,2,6} –> {2,1,4,3,5,5,6}

{2,1,4,3,5,5,6} –> {4,1,2,3,5,5,6}

{4,1,2,3,5,5,6} –> {5,1,2,3,4,5,6}

{5,1,2,3,4,5,6} //此时,要交换的两个数字相等,则证明该数字重复

可以看出,其时间复杂度为O(n),且不需要额外分配空间,空间复杂度为O(1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Solution {
// Parameters:
// numbers: an array of integers
// length: the length of array numbers
// duplication: (Output) the duplicated number in the array number,length of duplication array is 1,so using duplication[0] = ? in implementation;
// Here duplication like pointor in C/C++, duplication[0] equal *duplication in C/C++
// 这里要特别注意~返回任意重复的一个,赋值duplication[0]
// Return value: true if the input is valid, and there are some duplications in the array number
// otherwise false
public boolean duplicate(int numbers[],int length,int [] duplication) {
if(numbers == null){
return false;
}
for(int i = 0; i < length; i++){
while(numbers[i] != i){
if(numbers[i] == numbers[numbers[i]]){
duplication[0] = numbers[i];
return true;
}
swap(numbers, numbers[i], i);
}
}
return false;
}

private void swap(int[] numbers, int a, int b){
int temp = numbers[a];
numbers[a] = numbers[b];
numbers[b] = temp;
}
}

3.2 不修改数组找出重复的数字

题目描述

在一个长度为n+1的数组里的所有数字都在1~n的范围内,所以数组中至少有一个数字是重复的。请找出数组中任意一个重复的数字,但不能修改输入的数组。例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。

题解

此道题与上题类似,但有不同的几点:

1、题目要求不能修改输入的数组,因此可能要考虑创建辅助数组,这里的一个思路是将原数组的数字m移动到辅助数组中下标为m的位置,因此当移动时检测到辅助数组该下标已经有数字时,表示此数字重复了。这里的的空间复杂度为O(n),时间复杂度为O(n)。

2、题目中提到“在一个长度为n+1的数组里的所有数字都在1-n的范围内”,例如,若有个包含5个数的数组,但里面的数字只有1、2、3、4,那么必然有一个是重复的。这时候另一个思路则是在1、2、3、4、5中找出中间数3,以此将数组分割成两块,左半块是1-3(注意不是1-2),右半块是4-5,之后遍历整个数组,若数字在左半边的范围内,则将计数器加一。结束遍历时,若计数器的值大于这边块包含的个数,就说明重复的数字在这边块里,否则其就在右半块。紧接着把范围缩小到其中一边,继续重复以上操作。此操作类似二分查找法,需要的时间为O(nlogn),空间复杂度为o(1),相当于以时间换空间。下面给出的代码将根据此种思路。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class Solution {
// Parameters:
// numbers: an array of integers
// length: the length of array numbers
// duplication: (Output) the duplicated number in the array number,length of duplication array is 1,so using duplication[0] = ? in implementation;
// Here duplication like pointor in C/C++, duplication[0] equal *duplication in C/C++
// 这里要特别注意~返回任意重复的一个,赋值duplication[0]
// Return value: true if the input is valid, and there are some duplications in the array number
// otherwise false
public boolean duplicate(int numbers[],int length,int [] duplication) {
if(numbers == null){
return false;
}

int start = 1;
int end = length - 1;
while(end >= start){
int middle = (end - start) / 2 + start;
int count = countRange(numbers, length, start, middle);
if(end == start){
if(count > 1){
duplication[0] = start;
return true;
}
break;
}
if(count > middle - start + 1)
end = middle;
else
start = middle + 1;
}
return false;
}

public int countRange(int numbers[],int length, int start, int end){
if(numbers == null){
return 0;
}

int count = 0;
for(int i = 0; i < length; i++){
if(numbers[i] >= start && numbers[i] <= end){
count++;
}
}
return count;
}
}

4.二维数组中的查找

题目描述

在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Solution {
public boolean Find(int target, int [][] array) {
int len = array.length - 1;
int i = 0;
while(i < array[0].length && len >= 0){
if(array[len][i] > target){
len--;
} else if(array[len][i] < target){
i++;
} else{
return true;
}
}
return false;
}
}

5.替换空格

题目描述

请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。

题解

先遍历一次字符串,得到空格的总数,然后将字符串的长度设置为字符串的原长度加上空格数的两倍。将原始字符串末尾的值不断复制给新新字符串的末尾,每次遇到空格的时候在新字符串前插入%20。此算法中所有字符都只复制了一次,因此时间复杂度为O(n)。

在合并两个数组时,如果从前往后复制每个数字(或字符)则需要重复移动数字(或字符)多次,那么我们可以考虑从后往前复制,这样就能减少移动的次数,从而提高效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Solution {
public String replaceSpace(StringBuffer str) {
if(str == null){
return null;
}

int spaceNum = 0;
for(int i = 0; i < str.length(); i++){
if(str.charAt(i) == ' '){
spaceNum++;
}
}
int oldIndex = str.length() - 1;
int newLength = str.length() + spaceNum * 2;
str.setLength(newLength);
int newIndex = newLength - 1;
for(; oldIndex >= 0 && oldIndex <= newIndex; oldIndex--){
if(str.charAt(oldIndex) == ' ') {
str.setCharAt(newIndex--, '0');
str.setCharAt(newIndex--, '2');
str.setCharAt(newIndex--, '%');
}else{
str.setCharAt(newIndex--, str.charAt(oldIndex));
}
}
return str.toString();
}
}

6.从尾到头打印链表

题目描述

输入一个链表,按链表值从尾到头的顺序返回一个ArrayList。

1
2
3
4
5
6
7
8
9
10
11
/**
* public class ListNode {
* int val;
* ListNode next = null;
*
* ListNode(int val) {
* this.val = val;
* }
* }
*
*/

题解

使用递归

每访问到一个节点的时候,先递归输出它后面的节点,再输出该节点自身。

1
2
3
4
5
6
7
8
9
10
public class Solution {
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
ArrayList<Integer> arrayList = new ArrayList<>();
if(listNode != null){
arrayList.addAll(printListFromTailToHead(listNode.nextNode));
arrayList.add(listNode.val);
}
return arrayList;
}
}

使用栈

利用栈“先进后出”的特性,实现最先入栈的节点值最后输出。

1
2
3
4
5
6
7
8
9
10
11
12
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
Stack stack = new Stack();
while(listNode != null){
stack.push(listNode.val);
listNode = listNode.nextNode;
}
ArrayList<Integer> arrayList = new ArrayList<>();
while(!stack.isEmpty()){
arrayList.add((Integer) stack.pop());
}
return arrayList;
}

7.重建二叉树

题目描述

输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。

1
2
3
4
5
6
7
8
9
/**
* Definition for binary tree
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/

题解

对于前序遍历,第一个数字即为根节点的值;对于中序遍历,根据根节点值将序列划分为左右子树。接下来使用递归分别继续进行如上操作,便可不断构建出左右子树。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Solution {
public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
if(pre == null || in == null){
return null;
}
return reConstructBinaryTree(pre, 0, pre.length-1, in, 0, in.length-1);
}

private TreeNode reConstructBinaryTree(int[] pre, int startPre, int endPre, int[] in, int startIn, int endIn){

if(startPre > endPre || startIn > endIn)
return null;

TreeNode root = new TreeNode(pre[startPre]);

for(int i = startIn; i <= endIn; i++){
if(in[i] == pre[startPre]){
root.left = reConstructBinaryTree(pre, startPre+1, i-startIn+startPre, in, startIn, i-1);
root.right = reConstructBinaryTree(pre, startPre+i-startIn+1, endPre, in, i+1, endIn);
break;
}
}
return root;
}
}

8.二叉树的下一个节点

题目描述

给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。

1
2
3
4
5
6
7
8
9
10
11
12
/*
public class TreeLinkNode {
int val;
TreeLinkNode left = null;
TreeLinkNode right = null;
TreeLinkNode next = null;

TreeLinkNode(int val) {
this.val = val;
}
}
*/

题解

此题可分为两种情况:一种是一个节点有右子树,那么它的下一个节点就是它的右子树中的最左子节点;另一种是没有右子树,那么就判断它是否为父节点的左节点,如果是,则父结点为其下一个节点,如果不是,则向上遍历其父结点,找到为其祖先节点左节点的父结点,这个祖先节点就是它的下一个节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Solution {
public TreeLinkNode GetNext(TreeLinkNode pNode)
{
//1、一个节点有右子树,那么找到右子树的最左子节点
if (pNode.right != null) {
TreeLinkNode node = pNode.right;
while (node.left != null) {
node = node.left;
}
return node;
}

//2、一个节点没有右子树
while (pNode.next != null) {
if(pNode.next.left == pNode) return pNode.next;
pNode = pNode.next;
}
return null;
}
}

9.用两个栈实现队列

题目描述

用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。

题解

此题思路较为简单,主要就是利用栈“先入后出”和队列“先入先出”的特性,每次push的时候将值存到栈1中,pop的时候先将栈1的值放入栈2从而实现逆序,然后再对栈2进行pop操作,就实现了队列的“先进先出”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Solution {
Stack<Integer> stack1 = new Stack<Integer>();
Stack<Integer> stack2 = new Stack<Integer>();

public void push(int node) {
stack1.push(node);
}

public int pop() {
if (stack2.size() <= 0) {
while (stack1.size() > 0) {
stack2.push(stack1.pop());
}
}
return stack2.pop();
}
}

10.1 斐波那契数列

题目描述

大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。

题解

传统的做法是使用递归:return Fibonacci(n-1)+Fibonacci(n-2)。但是这种做法画出树形图就能看出有许多重复的节点,而且容易导致内存溢出,因而不建议使用。

使用循环

使用循环是一个较好的做法,不仅提高了时间效率,也解决了内存溢出的问题。实际上,任何递归都可以用循环来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Solution {
public int Fibonacci(int n) {
if(n == 0)
return 0;
if(n == 1)
return 1;
int num = 0;
int num1 = 0;
int num2 = 1;
for(int i = 2; i <= n; i++){
num = num1 + num2;
num1 = num2;
num2 = num;
}
return num;
}
}

使用尾递归

尾递归是递归的一种特殊形式,本质上和递归没有什么区别,但优化后可以重复利用同一个栈帧,大幅提高效率,具体介绍在我的博客中有介绍:。由于java没有对尾递归进行优化,所以在此题中用java解题时依旧无法解决内存溢出的问题,主要提供一种答题思路。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Solution {
public int Fibonacci(int n) {
return Fibonacci(n, 0, 1);
}

private static int Fibonacci(int n, int num1, int num2){
if(n == 0)
return 0;
if(n == 1)
return num2;
else
return Fibonacci(n - 1, num2, num1 + num2);
}
}

10.2 跳台阶

题目描述

一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。

题解

如果只有1级台阶,则只有一种跳法;如果有2级台阶,则可以一次跳两阶,或者一次跳一阶;如果有n级台阶,第一次跳就有两种不同的选择:当第一次只跳一阶时,总的跳法数等于后面n-1级台阶的跳法数,而如果第一次跳两阶的话,总的跳法数就等于后面n-2级台阶的跳法数。根据此规律可以得到以下公式:

1
2
3
4
f(n) = 0, 当n=0时
f(n) = 1, 当n=1时
f(n) = 2, 当n=2时
f(n) = f(n-1) + f(n-2), 当n>2时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Solution {
public int JumpFloor(int n) {
if(n <= 0)
return 0;
if(n == 1)
return 1;
if(n == 2)
return 2;
int num = 0;
int num1 = 1;
int num2 = 2;
for(int i = 3; i <= n; i++){
num = num1 + num2;
num1 = num2;
num2 = num;
}
return num;
}
}

10.3 变态跳台阶

题目描述

一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。

题解

思路一

此题和上题类似,但是一次可以跳多级台阶,依旧可以根据“第一次跳多少台阶,则跳法数等于剩下多少台阶的跳法数目”的思路进行分析,因此我们可以如下分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
如果有1级台阶,则有f(1) = 1 种跳法
如果有2级台阶,则有f(2) = f(2-1) + f(2-2) = 2 种跳法
如果有3级台阶,则有f(3) = f(3-1) + f(3-2) + f(3-3) = 4 种跳法
···
如果有n级台阶,则有f(n) = f(n-1) + f(n-2) + f(n-3) + ··· + f(0) 种跳法
又 f(n-1) = f(n-2) + f(n-3) + f(n-4) + ··· + f(0)
进行相减可得,f(n) - f(n-1) = f(n-1)
即,f(n) = 2f(n-1)

由此得出,
f(n) = 1, 当n=0时
f(n) = 1, 当n=1时
f(n) = f(n-1) + f(n-2), 当n>=2时

此题一个比较难理解的部分是,在公式中当n=0时,f(n)应当等于1而不是0。因为如果第一次就跳完了所有台阶,这也算一种跳法,此时f(n-n)=f(0)应当等于1而非0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Solution {
public int JumpFloorII(int n) {
if(n <= 0)
return 0;
if(n == 1)
return 1;
int num = 0;
int num1 = 1; //初始值应为1而非0
int num2 = 1;
for(int i = 2; i <= n; i++){
num = num1 + num2;
num2 = num1;
num1 = num;
}
return num;
}
}

思路二

在跳台阶的整个过程中,除了最后一阶是必须要跳的,其它每个台阶都有跳或者不跳两种可能性,因此f(n) = 2^(n-1)。

1
2
3
4
5
6
7
8
public class Solution {
public int JumpFloorII(int n) {
if(n <= 0)
return 0;
else
return (int)Math.pow(2, n-1);
}
}

10.4 矩形覆盖

题目描述

我们可以用2 1的小矩形横着或者竖着去覆盖更大的矩形。请问用n个2 1的小矩形无重叠地覆盖一个2 * n的大矩形,总共有多少种方法?

题解

1
2
3
f(n) = 1, 当n=0时
f(n) = 1, 当n=1时
f(n) = f(n-1) + f(n-2), 当n>=2时
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Solution {
public int RectCover(int n) {
if(n <= 0)
return 0;
if(n == 1)
return 1;
int num = 0;
int num1 = 1;
int num2 = 1;
for(int i = 2; i <= n; i++){
num = num1 + num2;
num2 = num1;
num1 = num;
}
return num;
}
}

14 剪绳子

题目描述

一根长度为n的绳子,将绳子剪为m段(剪m-1次),每段绳子的长度为k[0] - k[m];要求k[0] k[1] k[2] ··· k[m]的乘积为最大。n >1 且 m> 1。

题解

这道题可以采用动态规划来做。在剪第一刀的时候,我们有n-1种可能的选择,因此f(n)=max(f(i) * f(n-i))。由于递归会产生很多重复的子问题,因此采用由下而上的循环方式,将每个子问题的最优解放到数组dp里。最终的答案就是dp[n]。

在刚开始看书的时候并不理解为什么要对dp[1], dp[2], [dp3]逐一初始化,后来经过反复调试并思考,发现当n>3时dp[3]也就是当绳子长度为3时,不应该对其进行切割,因为切割后理论应得问题最优解就是2,而它的父问题要想得到最优解,应该直接使用整段未切割绳子也就是3。而当n<=3时,将在方法最开始就进行了一个正确的返回,即当绳子长度为3时,对其进行切割得到最优解为2。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Solution {
public int integerBreak(int n) {
if(n < 2)
return 0;
if(n == 2)
return 1;
if(n == 3)
return 2;

int []dp = new int[n +1];
dp[1] = 1;
dp[2] = 2;
dp[3] = 3;
for(int i = 4; i <= n; i++){
//把长度为i的绳子切成若干段
int max = 0;
for(int j = 1; j <= i/2; j++){
int p = dp[j] * dp[i-j];
if(max < p)
max = p;
}
dp[i] = max;
}
return dp[n];
}
}

15 二进制中1的个数

题目描述

输入一个整数,输出该数二进制表示中1的个数。

题解

常规解法

将输入的数字n与1做与运算,如果得出的结果是1,说明n的最低位是1,从而将计数器加一,并将1左移,进行n的次低位的判断,如此反复。这种做法整数为多少位就要循环多少次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14

public class Solution {
public int NumberOf1(int n){
int count = 0;
int flag = 1;
while(flag != 0){
if((n & flag) != 0){
count++;
}
flag = flag << 1;
}
return count;
}
}

优化解法

对输入的数字n减1再与自身进行与运算,即(n-1)&n可以将n最低位的1变成0:

1
2
3
n:       11101100
n-1: 11101011
n&(n-1): 11101000

基于以上,n中有多少个1,就可以进行多少次这样的操作。

1
2
3
4
5
6
7
8
9
10
public class Solution {
public int NumberOf1(int n){
int count = 0;
while(n != 0){
count++;
n = (n - 1) & n;
}
return count;
}
}

16 数值的整数次方

题目描述

给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。

题解

此题的关键在于对base、exponent为正数、负数和零的考虑。

循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

public class Solution {
public double Power(double base, int exponent) {
boolean isNegative = false;
if(exponent < 0){
isNegative = true;
exponent = -exponent;
}
double result = 1;
for(int i = 1; i <= exponent; i++){
result = base * result;
}
return isNegative ? 1 / result : result;
}
}

递归

a的n次方可以通过如下公式求解:

1
2
a^n = a^(n/2) * a^(n/2),              n为偶数
a^n = a^((n-1)/2) * a^((n-1)/2) * a, n为奇数

每次计算n都会变为原来的1/2,因此通过递归算法可以使时间复杂度降到logn,效率得到提升。除此之外,可以用右移运算和位与运算代替除2和求余运算两个操作,从而得到进一步优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Solution {
public double Power(double base, int exponent) {
if(exponent == 0)
return 1;
if(exponent == 1)
return base;
boolean isNegative = false;
if(exponent < 0){
isNegative = true;
exponent = -exponent;
}
double result = Power(base, exponent >> 1);
result *= result;
if((exponent & 0x1) == 1){
result *= base;
}
if(exponent < 0){
result = 1 / result;
}
return isNegative ? 1 / result : result;
}
}

17 打印从1到最大的n位数

题目描述

输入数字n,按顺序打印出从1到最大的n位十进制数。比如输入3,则打印出1、2、3一直到最大的3位数999。

题解

这道题可以使用递归对n位数进行全排列,在每一次递归调用之前都设置好下一位,当index为最后一位时,结束递归并打印。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

public class Solution {
public void PrintToMaxOfNDigits(int n){
if(n <= 0)
return;
char[] nums = new char[n];
for(int i = 0; i < 10; i++){
nums[0] = (char) ('0' + i);
PrintToMaxOfNDigits(nums, 0);
}
}

public void PrintToMaxOfNDigits(char[] nums, int index){
if(index == nums.length - 1){
System.out.println(nums);
return;
}

for(int i = 0; i < 10; i++){
nums[index+1] = (char) ('0' + i);
PrintToMaxOfNDigits(nums, index+1);
}
}
}

18.1 在O(1)时间内删除链表节点

题目描述

给定单向链表的头指针和一个节点指针,定义一个函数在O(1)时间内删除该节点。

1
2
3
4
5
6
7
8
9
10
/*
public class ListNode {
int val;
ListNode next = null;

ListNode(int val) {
this.val = val;
}
}
*/

题解

对于一个链表:1->2->3->4->5,如果要删除节点4,

首先想到的思路是找到节点4前面的节点(此处也就是节点3),将3的下一个节点重新设置为要删除的节点的下一个节点(此处也就是5),此时链表就变为了1->2->3->5。但由于链表是单向链表,不能从要删除的节点直接得到上一个节点,因此只能从头开始顺序查找,时间复杂度就为O(n)了。

另一种思路则是将要删除的节点的下一个节点的值赋值给要删除的节点,再将要删除的节点的下一个节点重新设置为下下个节点:
1->2->3->5->5
1->2->3->5
此时,时间复杂度就为O(1)了。

但如果要删除的节点为尾节点,则没有下一个节点,此种情况依然要使用顺序查找的方式删除节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Solution {
public ListNode deleteNode(ListNode head, ListNode tobeDelete) {
if(head == null || tobeDelete == null) {
return null;
}

//要删除的节点不是尾节点
if(tobeDelete.next != null) {
tobeDelete.val = tobeDelete.next.val;
tobeDelete.next = tobeDelete.next.next;
} else {
//要删除的节点是尾节点
ListNode node = head;
if(node == tobeDelete) {
//如果链表中只有要删除的这一个节点
return null;
}
while(node.next != tobeDelete) {
node = node.next;
}
node.next = null;

}

return head;
}
}

18.2 删除链表中重复的结点

题目描述

在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5

1
2
3
4
5
6
7
8
9
10
/*
public class ListNode {
int val;
ListNode next = null;

ListNode(int val) {
this.val = val;
}
}
*/

题解

如果当前节点的值与下一个节点的值相同,那么它们就是重复的节点,都可以被删除。为了保证删除之后的链表仍然是相连的,我们要把当前节点的前一个节点和后面值比当前节点的值大的节点相连。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class Solution {
public ListNode deleteDuplication(ListNode pHead)
{
if(pHead == null) {
return null;
}

//如果链表中只存在一个节点,则不存在重复的节点
if(pHead.next == null) {
return pHead;
}

ListNode preNode = null;
ListNode node = pHead;

while(node != null) {
ListNode nextNode = node.next;
if(nextNode != null && !(nextNode.val == node.val)) {
//当前节点与下一个节点不同
preNode = node;
node = nextNode;
} else {
if(node.next == null) {
break;
}
//当前节点与下一个相同
int value = node.val;
ListNode toBeDel = node;
while(toBeDel != null && toBeDel.val == value) {
nextNode = toBeDel.next;
toBeDel = nextNode;
}
if(preNode == null) {
pHead = nextNode;
} else {
preNode.next = nextNode;
}
node = nextNode;
}
}

return pHead;
}
}

19 正则表达式匹配

题目描述

请实现一个函数用来匹配包括’.’和’‘的正则表达式。模式中的字符’.’表示任意一个字符,而’‘表示它前面的字符可以出现任意次(包含0次)。在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串”aaa”与模式”a.a”和”abaca”匹配,但是与”aa.a”和”ab*a”均不匹配.

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class Solution {
public boolean match(char[] str, char[] pattern)
{
if(str == null || pattern == null) {
return false;
}
int strIndex = 0;
int patternIndex = 0;
return matchCore(str, pattern, strIndex, patternIndex);
}

public boolean matchCore(char[] str, char[] pattern, int strIndex, int patternIndex) {

//字符串与模式完全匹配
if(strIndex == str.length && patternIndex == pattern.length) {
return true;
}

//字符串未到达末尾,而模式已到达末尾,则匹配失败
if(strIndex != str.length && patternIndex == pattern.length) {
return false;
}

if(patternIndex + 1 < pattern.length && pattern[patternIndex + 1] == '*') {
//模式的第二个字符为*
if(strIndex < str.length && str[strIndex] == pattern[patternIndex] || strIndex < str.length && pattern[patternIndex] == '.') {
//*前的字符与字符串中的字符相等时
//可能的情况:匹配0位,模式向后移动两位跳过*;匹配一位,模式向后移动两位跳过*;匹配一位,模式不移动,下次继续匹配。
return matchCore(str, pattern, strIndex, patternIndex+2) || matchCore(str, pattern, strIndex+1, patternIndex+2)
|| matchCore(str, pattern, strIndex+1, patternIndex);
} else {
//*前的字符与字符串中的字符不相等时,匹配0位,跳过*
return matchCore(str, pattern, strIndex, patternIndex+2);
}

}

if(pattern[patternIndex] == '.' && strIndex < str.length || strIndex < str.length && pattern[patternIndex] == str[strIndex]) {
//模式的第二个字符不为*
if(str[strIndex] == pattern[patternIndex] || pattern[patternIndex] == '.') {
//如果字符相匹配,则接续操作
return matchCore(str, pattern, strIndex+1, patternIndex+1);
} else {
//字符不匹配,直接返回false
return false;
}
}

return false;
}
}

20 表示数值的字符串

题目描述

请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串”+100”,”5e2”,”-123”,”3.1416”和”-1E-16”都表示数值。 但是”12e”,”1a3.14”,”1.2.3”,”+-5”和”12e+4.3”都不是。

题解

表示数值的字符串遵循模式:A[.[b]][e|EC]或者.B[e|EC]。A和C都可以带有符号’+’或’-‘,B则不行,且A、B、C都必须为整数。因此,可以根据模式的顺序去依次匹配A、B、C。如果字符串中包含’.’,则’.’左右至少要有一方有数字,而如果字符串中包含’e’或’E’,则’e’或’E’两方都必须要有数字,且右方必须为整数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Solution {
private int index = 0;

public boolean isNumeric(char[] str) {
boolean flag;

if(str == null)
return false;

flag = scanInteger(str);

if(index < str.length && str[index] == '.') {
index++;
flag = scanUnsignedInteger(str) || flag;
}
if(index < str.length && (str[index] == 'e' || str[index] == 'E')) {
index++;
flag = scanInteger(str) && flag;
}

return flag && (index == str.length);
}

boolean scanUnsignedInteger(char[] str) {
int before = index;
while(index < str.length && str[index] >= '0' && str[index] <= '9') {
index++;
}
return index > before;
}

boolean scanInteger(char[] str) {
if(index < str.length && (str[index] == '+' || str[index] == '-'))
index++;
return scanUnsignedInteger(str);
}
}

21 调整数组顺序使奇数位于偶数前面

题目描述

输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分(拓展:并保证奇数和奇数,偶数和偶数之间的相对位置不变)。

题解

基本解法

一前一后扫描数组,若发现有偶数在前,奇数在后,则交换它们两的位置。此算法的时间复杂度为O(n),但是算法是不稳定的,也就是没法保证奇数和奇数,偶数和偶数之间的相对位置不变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Solution {
public void reOrderArray(int [] array) {
int head = 0;
int tail = array.length - 1;
while(head < tail) {
while(head < array.length && array[head] % 2 != 0) {
//正向遍历不为偶数的时候
head++;
}
while(tail >= 0 && array[tail] % 2 == 0) {
//反向遍历不为奇数的时候
tail--;
}
if(head < tail) {
int temp = array[head];
array[head] = array[tail];
array[tail] = temp;
}
}
}
}

拓展解法

要保证奇数和奇数,偶数和偶数之间的相对位置不变,则需要使用一个辅助数组,首先计算出奇数的个数,以此作为将偶数插入辅助数组的起始坐标。然后遍历原数组,将奇数放置于辅助数组的奇数起始坐标(也就是0),将偶数放置于辅助数组的偶数起始坐标,最后再将调整完毕的辅助数组中的元素依次放回原数组。此算法的时间复杂度为O(n),空间复杂度为O(n),相当于以空间换时间。另一种思路是可以使用插入排序的思想,在此不再阐述。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Solution {
public void reOrderArray(int [] array) {
int[] copy = new int[array.length];
int index = 0;
int count = 0;
for(int i = 0; i < array.length; i++) {
//统计奇数个数
if(array[i] % 2 != 0) count++;
}
int odd = 0;
int even = count;

while(index < array.length) {
if(array[index] % 2 != 0) {
//如果是奇数
copy[odd] = array[index];
odd++;
} else {
//如果是偶数
copy[even] = array[index];
even++;
}
index++;
}
for(int i = 0; i < array.length; i++) {
array[i] = copy[i];
}
}
}

22 链表中倒数第K个节点

题目描述

输入一个链表,输出该链表中倒数第k个结点。

1
2
3
4
5
6
7
8
9
/*
public class ListNode {
int val;
ListNode next = null;

ListNode(int val) {
this.val = val;
}
}*/

题解

传统的思路是先遍历一遍链表,计算出节点数n,则倒数第k个节点就是从头开始的第n-k+1个节点。但此种做法要遍历链表两边,效率不高。

另一种思路是定义两个指针,让两个指针之间的距离保持在k-1,则当第一个指针到达链表的尾节点时,第二个指针则指向倒数第k个节点。这种实现只需要遍历链表一次即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Solution {
public ListNode FindKthToTail(ListNode head,int k) {
if(head == null || k <= 0)
return null;

ListNode node1 = head;
ListNode node2 = head;

for(int i = 0; i < k-1; i++) {
if(node1.next == null)
return null;
node1 = node1.next;
}
while(node1.next != null) {
node1 = node1.next;
node2 = node2.next;
}
return node2;
}
}

23 链表中环的入口节点

题目描述

给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。

1
2
3
4
5
6
7
8
9
/*
public class ListNode {
int val;
ListNode next = null;

ListNode(int val) {
this.val = val;
}
}*/

题解

要找到链表中环的入口节点,整体思路与上一题类似。例如,如果环中有4个节点,则第二个指针要比第一个指针先走四步,然后同时向前走,当两个指针相遇时,所指向的节点就是入口节点。

根据此思路,要解决的问题是:如何计算环的节点数?这里可以先使用一快一慢两个指针,得到相遇时的节点(若第一个指针走到了null,说明链表中没有环,返回null),此节点必然在环内,然后从此节点开始绕环一圈,每走一步计数器加一,当回到原点时便得到了环的节点数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class Solution {

public ListNode EntryNodeOfLoop(ListNode head)
{
if(head == null)
return null;

ListNode meetingNode = MeetingNode(head);

if(meetingNode == null)
return null;

ListNode node1 = head;
ListNode node2 = head;
ListNode node = meetingNode.next;
int count = 1;
while(node != meetingNode) {
count++;
node = node.next;
}
for(int i = 0; i < count; i++) {
node1 = node1.next;
}

while(node1 != node2) {
node1 = node1.next;
node2 = node2.next;
}
return node1;
}

public ListNode MeetingNode(ListNode head) {
ListNode node1 = head;
ListNode node2 = head;

while(node1.next != null && node1.next.next != null) {
node1 = node1.next.next;
node2 = node2.next;
if(node1 == node2)
return node1;
}
return null;
}

}

24 反转链表

题目描述

输入一个链表,反转链表后,输出新链表的表头。

1
2
3
4
5
6
7
8
9
/*
public class ListNode {
int val;
ListNode next = null;

ListNode(int val) {
this.val = val;
}
}*/

题解

定义三个指针分别指向前一个节点,当前节点和后一个节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Solution {
public ListNode ReverseList(ListNode head) {
if(head == null)
return null;

ListNode preNode = null;
ListNode currNode = head;
ListNode nextNode = head.next;
ListNode reNode = null;

while(currNode != null) {
if(nextNode == null) {
reNode = currNode;
currNode.next = preNode;
break;
}
currNode.next = preNode;
preNode = currNode;
currNode = nextNode;
nextNode = currNode.next;
}
return reNode;
}
}

25 合并两个排序的链表

题目描述

输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。

1
2
3
4
5
6
7
8
9
/*
public class ListNode {
int val;
ListNode next = null;

ListNode(int val) {
this.val = val;
}
}*/

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Solution {
public ListNode Merge(ListNode list1,ListNode list2) {
if(list1 == null)
return list2;
if(list2 == null)
return list1;

ListNode head = null;

if(list1.val < list2.val) {
head = list1;
head.next = Merge(list1.next, list2);
}
else {
head = list2;
head.next = Merge(list1, list2.next);
}
return head;
}
}

26 树的子结构

题目描述

输入两棵二叉树A,B,判断B是不是A的子结构。(ps:我们约定空树不是任意一个树的子结构)

1
2
3
4
5
6
7
8
9
10
11
/**
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;

public TreeNode(int val) {
this.val = val;
}
}
*/

题解

第一步是在树A中查找与根节点的值一样的节点,第二步是判断以此节点为根节点的子树是不是和树B具有相同的结构。此题要特别注意由于计算机表示小数含有误差,不能直接使用==进行double类型的等值判断,而是判断两个小数的差的绝对值是否小于某一个可忽略的数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Solution {
public boolean HasSubtree(TreeNode root1,TreeNode root2) {
boolean result = false;

if(root1 != null && root2 != null) {
if(Equal(root1.val, root2.val))
result = DoesTree1HaveTree2(root1, root2);
if(!result)
result = HasSubtree(root1.left, root2);
if(!result)
result = HasSubtree(root1.right, root2);
}

return result;
}

public boolean DoesTree1HaveTree2(TreeNode root1, TreeNode root2) {
if(root2 == null)
return true;
if(root1 == null)
return false;

if(!Equal(root1.val, root2.val))
return false;

return DoesTree1HaveTree2(root1.left, root2.left) && DoesTree1HaveTree2(root1.right, root2.right);
}

public boolean Equal(double num1, double num2) {
if(num1- num2 > -0.0000001 && num1 - num2 < 0.0000001)
return true;
return false;
}
}

27 二叉树的镜像

题目描述

操作给定的二叉树,将其变换为源二叉树的镜像。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
二叉树的镜像定义:

源二叉树
8
/ \
6 10
/ \ / \
5 7 9 11

镜像二叉树
8
/ \
10 6
/ \ / \
11 9 7 5
1
2
3
4
5
6
7
8
9
10
11
/**
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;

public TreeNode(int val) {
this.val = val;
}
}
*/

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Solution {
public void Mirror(TreeNode root) {
if(root == null)
return;
if(root.left == null && root.right == null)
return;

TreeNode temp;
temp = root.left;
root.left = root.right;
root.right = temp;

if(root.left != null)
Mirror(root.left);
if(root.right != null)
Mirror(root.right);
}
}

28 对称的二叉树

题目描述

请实现一个函数,用来判断一颗二叉树是不是对称的。注意,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的。

1
2
3
4
5
6
7
8
9
10
11
12
13
  对称的二叉树 
8
/ \
6 6
/ \ / \
5 7 7 5

非对称的二叉树
8
/ \
6 9
/ \ / \
5 7 7 5
1
2
3
4
5
6
7
8
9
10
11
/**
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;

public TreeNode(int val) {
this.val = val;
}
}
*/

题解

对于上图的非对称的二叉树,可以发现前序序列为{8, 6, 5, 7, 6, 7, 5},对称前序序列为{8, 9, 5, 7, 6, 7, 5}。
而对于对称的二叉树,前序序列与对称前序序列都为{8, 6, 5, 7, 6, 7, 5}。
因此,通过比较二叉树的前序序列和对称前序序列即可判断出二叉树是否对称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Solution {
boolean isSymmetrical(TreeNode pRoot)
{
return isSymmetrical(pRoot, pRoot);
}

boolean isSymmetrical(TreeNode pRoot1, TreeNode pRoot2) {
if(pRoot1 == null && pRoot2 == null)
return true;
if(pRoot1 == null || pRoot2 == null)
return false;
if(pRoot1.val != pRoot2.val)
return false;
return isSymmetrical(pRoot1.left, pRoot2.right) && isSymmetrical(pRoot1.right, pRoot2.left);
}
}

29 顺时针打印矩阵

题目描述

输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下4 X 4矩阵: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 则依次打印出数字1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10.

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import java.util.ArrayList;

public class Solution {
public ArrayList<Integer> printMatrix(int [][] matrix) {
if(matrix == null)
return null;

int columns = matrix[0].length;
int rows = matrix.length;

if(columns == 0 || rows == 0)
return null;
ArrayList<Integer> arr = new ArrayList();
int start = 0;

while(columns > start*2 && rows > start*2) {
arr.addAll(PrintMatrixInCircle(matrix, columns, rows, start));
start++;
}
return arr;
}

public ArrayList<Integer> PrintMatrixInCircle(int [][]matrix, int columns, int rows, int start){
ArrayList<Integer> arr = new ArrayList();

int endX = columns - 1 - start;
int endY = rows - 1 - start;

//从左到右打印一行
for(int i = start; i <= endX; ++i) {
int number = matrix[start][i];
arr.add(number);
}

//从上到下打印一列
if(start < endY) {
for(int i = start + 1; i <= endY; ++i) {
int number = matrix[i][endX];
arr.add(number);
}
}

//从右到左打印一行
if(start < endX && start < endY) {
for(int i = endX - 1; i >= start; --i) {
int number = matrix[endY][i];
arr.add(number);
}
}

//从下到上打印一行
if(start < endX && start < endY - 1) {
for(int i = endY - 1; i >= start + 1; --i) {
int number = matrix[i][start];
arr.add(number);
}
}

return arr;
}
}

30 包含min函数的栈

题目描述

定义栈的数据结构,请在该类型中实现一个能够得到栈中所含最小元素的min函数(时间复杂度应为O(1))。

题解

使用一个辅助栈。第一次压入的时候,把该元素同时也压入到辅助栈中。以后每次压入新元素的时候,如果新元素比辅助栈栈顶的元素小,就把新元素也压入到辅助栈中,否则,就把辅助栈栈顶的元素再次压入。这么做可以使辅助栈的每个元素对应着数据栈中该位置元素之前的最小元素,即每次从数据栈和辅助栈中弹出一个元素时,辅助栈的栈顶都保存着数据栈的最小元素。由此我们也可以发现,在辅助栈中,新元素的值要么比上一层的值小,要么等于上一层的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.util.Stack;

public class Solution {
private Stack<Integer> data = new Stack<>();

private Stack<Integer> dataHelper = new Stack<>();


public void push(int node) {
data.push(node);
if(dataHelper.isEmpty()) {
dataHelper.push(node);
} else {
if(node < dataHelper.peek()) {
dataHelper.push(node);
} else {
dataHelper.push(dataHelper.peek());
}
}

}

public void pop() {
data.pop();
dataHelper.pop();
}

public int top() {
return data.peek();
}

public int min() {
return dataHelper.peek();
}
}

31 栈的压入、弹出序列

题目描述

输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。(注意:这两个序列的长度是相等的)

题解

使用一个栈来模拟压入、弹出的操作,可以得到以下规律:

  • 如果下一个弹出的数字刚好是栈顶数字,直接弹出
  • 否则,把压栈序列中还没有入栈的数字压入栈中,直到把下一个需要弹出的数字压入栈顶为止
  • 压栈序列为空还没找到,离开循环
  • 判断栈是否为空,为空则说明弹出序列匹配,否则,不匹配
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.ArrayList;
import java.util.Stack;

public class Solution {

Stack<Integer> helper = new Stack<>();

public boolean IsPopOrder(int [] pushA,int [] popA) {
if(pushA == null || popA == null)
return false;

for(int i = 0, j = 0; i < pushA.length; i++) {
helper.push(pushA[i]);

while(!helper.isEmpty() && j < popA.length && helper.peek() == popA[j]) {
helper.pop();
j++;
}
}

return helper.isEmpty();
}
}

32.1 从上往下打印二叉树

题目描述

从上往下打印出二叉树的每个节点,同层节点从左至右打印。

题解

每次打印一个节点的时候,如果该节点有子节点,就把子节点加入到队列中,然后再从队列头取首元素并打印,重复以上操作,直至队列为空。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Queue;

public class Solution {
public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
ArrayList<Integer> list = new ArrayList<>();
Queue<TreeNode> queue = new LinkedList<>();
if(root == null)
return list;
queue.add(root);
while(queue.size() != 0) {
TreeNode node = queue.poll();
list.add(node.val);
if(node.left != null) {
queue.add(node.left);
}
if(node.right != null) {
queue.add(node.right);
}
}
return list;
}
}

32.2 把二叉树打印成多行

题目描述

从上到下按层打印二叉树,同一层结点从左至右输出。每一层输出一行。

题解

用两个变量分别记录当前行还剩余的节点与下一行需要打印的节点。每打印完一行,都将下一行需要打印的节点数赋给当前剩余节点数,并将自身置0,以便重新开始新的一行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Queue;

public class Solution {
ArrayList<ArrayList<Integer> > Print(TreeNode root) {
ArrayList<ArrayList<Integer>> listAll = new ArrayList<>();
ArrayList<Integer> list = new ArrayList<>();
Queue<TreeNode> queue = new LinkedList<>();
if(root == null)
return listAll;
int toBePrinted = 1; //当前层中还没打印的节点数
int nextLevel = 0; //下一层的节点数
queue.add(root);
while(queue.size() != 0) {
TreeNode node = queue.poll();
list.add(node.val);
if(node.left != null) {
queue.add(node.left);
nextLevel++;
}
if(node.right != null) {
queue.add(node.right);
nextLevel++;
}
toBePrinted--;
if(toBePrinted == 0) {
listAll.add(list);
list = new ArrayList<>();
toBePrinted = nextLevel;
nextLevel = 0;
}
}
return listAll;
}
}

32.3 按之字形顺序打印二叉树

题目描述

请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。

题解

如果当前节点在奇数层,则将子节点以从左往右的顺序入栈;如果当前节点在偶数层,则将子节点以从右往左的顺序入栈。更简单地说:子节点入栈的方向与当前层节点弹出的方向一致。

在下面的算法中,用一个Stack数组保存当前层的栈与下一层的栈,用1和0表示奇数层和偶数层。当前层栈的节点弹出时,下一层栈的节点压入。如果当前层栈为空,则说明该行已经打印完成,将current与next置换(奇偶置换)后开始新的一行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Stack;

public class Solution {
public ArrayList<ArrayList<Integer> > Print(TreeNode root) {
if(root == null)
return new ArrayList<ArrayList<Integer>>();
Stack<TreeNode>[] stack = new Stack[2];
stack[0] = new Stack<TreeNode>();
stack[1] = new Stack<TreeNode>();
ArrayList<ArrayList<Integer>> listAll = new ArrayList<>();
ArrayList<Integer> list = new ArrayList<>();
if(root == null)
return listAll;
int current = 1; //表示奇数层
int next = 0; //表示偶数层
stack[current].push(root);
while(stack[current].size() != 0 || stack[next].size() != 0) {
TreeNode node = stack[current].pop();

list.add(node.val);

if(current == 1) {
//如果在奇数层,则子节点从左往右入栈
if(node.left != null) {
stack[next].push(node.left);
}
if(node.right != null) {
stack[next].push(node.right);
}
} else {
//如果在偶数层,则子节点从右往左入栈
if(node.right != null) {
stack[next].push(node.right);
}
if(node.left != null) {
stack[next].push(node.left);
}
}

if(stack[current].size() == 0) {
listAll.add(list);
list = new ArrayList<>();
current = 1 - current;
next = 1 - next;
}
}
return listAll;
}
}

33 二叉搜索树的后序遍历序列

题目描述

输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则输出Yes,否则输出No。假设输入的数组的任意两个数字都互不相同。

题解

先以以下二叉树为例,其输入数组为{5, 7, 6, 9, 11, 10, 8}。

1
2
3
4
5
    8
/ \
6 10
/ \ / \
5 7 9 11

我们可以发现,数组的最后一个数字8就是二叉树的根节点,然后从数组开始进行遍历,凡是比8小的都属于根节点的左子树,其余的就是根节点的右子树,即{5, 7, 6, /9, 11, 10,/ 8}。我们在看看根节点的左子树,同样最后一个数字6是左子树的根节点,而5、7分别属于左子树根节点的左右子树。

再看看另一个例子:{7, 4, 6, 5},由以上分析的规律可以发现,5为二叉树的根节点,而7、4、6都比5大,说明此二叉树没有左子树,而在右子树{7, 4, 6}中,7比6大,说明7在根节点的右子树中,而4却又比6小,这有违二叉树的定义,说明此数组不属于任何一个二叉树。

因此,我们可以使用递归来解决这个问题,先找到二叉树的根节点,再基于此根节点将数组拆分成左右子树,然后对左右子树分别进行递归。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class Solution {
public boolean VerifySquenceOfBST(int [] sequence) {
if(sequence == null || sequence.length <= 0)
return false;
return VerifySquenceOfBST(sequence, 0, sequence.length - 1);
}

public boolean VerifySquenceOfBST(int [] sequence, int begin, int end) {
if(end-begin<0)
return true;

int root = sequence[end];

int i = begin;
for(;i < end; i++) {
if(sequence[i] > root)
break;
}

int j = i;
for(;j < end; j++) {
if(sequence[j] < root)
return false;
}

boolean left = true;
if(i > 0)
left = VerifySquenceOfBST(sequence, begin, i-1);

boolean right = true;
if(i < end)
right = VerifySquenceOfBST(sequence, i, end - 1);

return (right && left);
}
}

34 二叉树中和为某一值的路径

题目描述

输入一颗二叉树的跟节点和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。(注意: 在返回值的list中,数组长度大的数组靠前)。

1
2
3
4
5
6
7
8
9
10
11
/**
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;

public TreeNode(int val) {
this.val = val;
}
}
*/

题解

此题运用深度优先搜索的思想。从左开始向下深度遍历,遇到叶节点之后,判断其值是否等于target,如果相等则将此路径加入到所有路径的列表中。每次回退的时候,都要将路径最后一个节点删除。

此题需要注意,将某一路径加入到所有路径列表时,必须新建一个ArrayList,否则每次都是将对同一个对象的引用加入到listAll中,而java中通过引用是可以改变对象内部的属性的,所以每次对list进行remove操作都会影响到listAll中已加入的所有list,最后由于list会回退到根节点并把根节点remove掉,导致listAll的路径数目虽然正确,但每条路径列表都为空。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Solution {
public ArrayList<ArrayList<Integer>> FindPath(TreeNode root,int target) {
ArrayList<ArrayList<Integer>> listAll = new ArrayList<>();
ArrayList<Integer> list = new ArrayList<>();
if(root == null)
return listAll;
findPath(listAll, list, root, target);
return listAll;
}

public void findPath(ArrayList<ArrayList<Integer>> listAll, ArrayList<Integer> list, TreeNode root, int target) {
list.add(root.val);
//如果为叶节点
if(root.left == null && root.right == null) {
if(root.val == target) {
ArrayList<Integer> newList = new ArrayList<>();
newList.addAll(list);
listAll.add(newList);
}
list.remove(list.size() - 1);
return;
}
if(root.left != null)
findPath(listAll, list, root.left, target-root.val);
if(root.right != null)
findPath(listAll, list, root.right, target-root.val);
list.remove(list.size() - 1);
return;

}
}

35 复杂链表的复制

题目描述

输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)

1
2
3
4
5
6
7
8
9
10
11
/*
public class RandomListNode {
int label;
RandomListNode next = null;
RandomListNode random = null;

RandomListNode(int label) {
this.label = label;
}
}
*/

题解

解决此题大体有以下两个步骤:

1、根据原始链表的每个节点创建对应的复制节点

2、设置复制出来的节点的random节点

此题的关键在于定位random节点,需保证算法的时间复杂度在O(n)。

哈希表

在第一步创建每个复制节点时,使用哈希表保存原节点与复制节点,之后设置random节点时,每当通过查找哈希表原节点的random节点便可以在O(1)的时间找到该复制节点应指向的random节点。此算法相当于以空间换时间,空间复杂度为O(n)。

这里需要注意,java中的map是不能直接使用iterator遍历的,因此需要先通过entrySet()方法获取set视图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;

public class Solution {
public RandomListNode Clone(RandomListNode head){
if(head == null)
return null;
HashMap<RandomListNode, RandomListNode> map = new HashMap<>();

RandomListNode cloneNodeHead = new RandomListNode(head.label);
RandomListNode cloneNode = cloneNodeHead;
map.put(head, cloneNode);
while(head.next != null) {
RandomListNode nextNode = new RandomListNode(head.next.label);
cloneNode.next = nextNode;
cloneNode = cloneNode.next;
head = head.next;
map.put(head, cloneNode);
}

Iterator<Entry<RandomListNode, RandomListNode>> it = map.entrySet().iterator();
while(it.hasNext()) {
Entry<RandomListNode, RandomListNode> entry = it.next();
//时间复杂度为O(1),相当于以空间换时间
RandomListNode sib = map.get(entry.getKey().random);
entry.getValue().random = sib;
}

return cloneNodeHead;
}
}

更好的解法

思路:在旧链表中创建新链表->根据旧链表的random节点初始化新链表的random节点->把新链表从旧链表中拆分出来。

此算法的时间复杂度为O(n),且不需要辅助空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class Solution {
public RandomListNode Clone(RandomListNode head)
{
if(head == null)
return null;
CloneNodes(head);
ConnectSiblingNodes(head);
return ReconnectNodes(head);
}

public void CloneNodes(RandomListNode head) {
RandomListNode test = head;
while(head != null) {
RandomListNode cloneNode = new RandomListNode(head.label);
RandomListNode next = head.next;
head.next = cloneNode;
cloneNode.next = next;
head = next;
}
}

public void ConnectSiblingNodes(RandomListNode head) {
while(head != null) {
RandomListNode cloneNode = head.next;
if(head.random != null) {
cloneNode.random = head.random.next;
}
head = head.next.next;
}
}

public RandomListNode ReconnectNodes(RandomListNode head) {
RandomListNode cloneNode = head.next;
RandomListNode cloneNodeHead = cloneNode;

while(head != null) {
RandomListNode next = head.next.next;
RandomListNode cloneNext;
//防止在最后一个节点处报空指针异常
if(next == null) {
cloneNext = null;
} else {
cloneNext = cloneNode.next.next;
}
head.next = next;
cloneNode.next = cloneNext;
head = next;
cloneNode = cloneNext;
}

return cloneNodeHead;
}
}

36 二叉搜索树与双向链表

题目描述

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。

1
2
3
4
5
6
7
8
9
10
11
/**
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;

public TreeNode(int val) {
this.val = val;
}
}
*/

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Solution {
public TreeNode Convert(TreeNode root) {
if(root == null)
return null;
if(root.left == null && root.right == null)
return root;
TreeNode left = Convert(root.left);
TreeNode p = left;
while(p != null && p.right != null) {
p = p.right;
}
if(left != null) {
root.left = p;
p.right = root;
}
TreeNode right = Convert(root.right);
if(right != null) {
root.right = right;
right.left = root;
}
return left != null ? left : root;
}
}

37 序列化二叉树

题目描述

请实现两个函数,分别用来序列化和反序列化二叉树

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Solution {

private int index = -1;

String Serialize(TreeNode root) {
StringBuilder str = new StringBuilder();
if(root == null) {
str.append("$,");
return str.toString();
}
str.append(root.val+",");
str.append(Serialize(root.left));
str.append(Serialize(root.right));
return str.toString();
}
TreeNode Deserialize(String str) {
String[] newStr = str.split(",");
index++;
if(index < str.length() && !newStr[index].equals("$")) {
TreeNode root = new TreeNode(Integer.valueOf(newStr[index]));
root.left = Deserialize(str);
root.right = Deserialize(str);
return root;
}
return null;
}
}

38 字符串的排列

题目描述

输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。

题解

回溯法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.util.ArrayList;
import java.util.List;
import java.util.Collections;

public class Solution {
public ArrayList<String> Permutation(String str) {
ArrayList<String> list = new ArrayList<>();
if(str == null || str.length() == 0)
return list;
Permutation(str.toCharArray(), 0, list);
Collections.sort(list);
return list;
}

public void Permutation(char[] c, int i, ArrayList<String> list) {
if(i == c.length) {
String str = String.valueOf(c);
if(!list.contains(str))
list.add(String.valueOf(c));
return;
} else {
for(int j = i; j < c.length; j++) {
swap(c, i, j);
Permutation(c, i+1, list);
swap(c, i, j);
}
}
}

public void swap(char[] c, int i, int j){
char temp;
temp = c[i];
c[i] = c[j];
c[j] = temp;
}
}

39 数组中出现次数超过一半的数字

题目描述

数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。

题解

基于辅助数组的解法

此种解法利用了辅助数组,在辅助数组中以原始数组的值为索引存储该值出现的次数,一旦次数超过原始数组的一半,则跳出循环返回该值。该解法空间复杂度为O(n),相当于以空间换时间,且由于数组的限制,事先必须要知道原始数组中值的范围,若要克服后者,可以使用其它数据结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Solution {
public int MoreThanHalfNum_Solution(int [] array) {
if(array == null || array.length == 0) {
return 0;
}
int length = array.length;
int[] helper = new int[length+1];
int result = 0;

for(int i = 0; i < length; i++) {
helper[array[i]]++;
if(helper[array[i]] > length / 2) {
result = array[i];
break;
}
}

return result;
}
}

多数投票算法

多数投票算(摩尔投票算法):定义一个结果变量和一个计数器,初始化的情况下计数器为0. 算法依次扫描序列中的元素,当处理某元素的时候,如果计数器为0,那么将该元素赋值给结果变量,然后将计数器设置为1,如果计数器不为0,那么将结果变量和该元素比较,如果相等,那么计数器加1,如果不等,那么计数器减1。处理之后,最后存储的结果变量就是这个数组中超过一半以上的元素。

需注意:如果一个元素的出现次数超过数组长度的一半,那么结果变量肯定为该元素,但结果变量元素的出现次数不一定超过数组长度的一半,因此需要进行第二次遍历确认。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Solution {
public int MoreThanHalfNum_Solution(int [] array) {
if(array == null || array.length == 0) {
return 0;
}

int result = array[0];
int n = 1;
for(int i = 1; i < array.length; i++) {
if(n == 0) {
result = array[i];
n = 1;
}
else if(array[i] == result)
n++;
else
n--;
}

n = 0;
for(int i = 0; i < array.length; i++) {
if(array[i] == result) {
n++;
}
}

if(n <= array.length / 2)
return 0;
return result;
}
}

40 最小的k个数

题目描述

输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.util.ArrayList;
import java.util.PriorityQueue;
import java.util.Comparator;

public class Solution {
public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
ArrayList<Integer> list = new ArrayList<>();
if(input == null || input.length <= 0 || k < 1 || k > input.length) {
return list;
}
PriorityQueue<Integer> heap = new PriorityQueue<>(k+1, new Comparator<Integer>() {
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
for(int i = 0; i < input.length; i++) {
heap.add(input[i]);
if(heap.size() > k) {
heap.poll();
}
}
for(Integer i : heap) {
list.add(i);
}
return list;
}
}

41 数据流中的中位数

题目描述

如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。

题解

用最大堆与最小堆来实现,插入的时间复杂度为O(log(n))。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import java.util.PriorityQueue;
import java.util.Comparator;

public class Solution {

PriorityQueue<Integer> max = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
});

PriorityQueue<Integer> min = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
});

public void Insert(Integer num) {
if((min.size()+max.size() & 1) == 0) {
//如果当前总数是偶数,则插入到最大堆
if(min.size() != 0 && num > min.peek()) {
//如果最小堆的数目不为0,且新插入的数字比最小堆的头要大
int temp = min.poll();
max.add(temp);
min.add(num);
} else {
//插入到最大堆
max.add(num);
}
} else {
//如果当前总数是奇数,则插入到最小堆
if(max.size() != 0 && num < max.peek()) {
//同理
int temp = max.poll();
min.add(temp);
max.add(num);
} else {
min.add(num);
}
}
}

public Double GetMedian() {
int size = max.size() + min.size();
Double d;

if(size == 0)
return -1.0;

if((size & 1) != 0) {
d = Double.valueOf(max.peek());
} else {
d = (double) (max.peek() + min.peek()) / 2;
}
return d;
}

}

42 连续子数组的最大和

题目描述

HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学。今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组,返回它的最大连续子序列的和,你会不会被他忽悠住?(子向量的长度至少是1)

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Solution {
public int FindGreatestSumOfSubArray(int[] data) {
if(data == null ||data.length <= 0){
return 0;
}

int sum=data[0], max=data[0];
for(int i = 1; i < data.length; i++){
if(sum <= 0){
sum = data[i];
} else{
sum += data[i];
}
if(sum > max){
max = sum;
}
}
return max;
}
}

44 数字序列中某一位的数字

题目描述

数字以 0123456789101112131415… 的格式序列化到一个字符串中,求这个字符串的第 index 位。

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class Solution {
public int digitAtIndex(int index) {
if(index < 0)
return -1;

int digits = 1; //digits表示有几位数,初始为一位数
while(true) {
int numbers = countOfIntegers(digits); //返回当前位数共有多少个数
if(index < numbers * digits) { //数字的个数乘位数能得到具体的某一位数字的下标
return digitAtIndex(index, digits);
}

index -= digits * numbers; //如果要查找的数字不在这位数里面,则跳过这些数字
digits++; //位数加一
}
}

/*
* 在n位数中的第index个数
*/
private int digitAtIndex(int index, int digits) {
int number = beginNumber(digits) + index / digits;
int indexFromRight = digits - index % digits; //得到在查找到的数字中具体从右数的哪一位
for(int i = 1; i < indexFromRight; i++) {
number /= 10;
}
return number % 10;
}

/*
* 计算n位的数字总共有多少,如二位数有10~99这90个数,三位数有100~999这900个数
*/
private int countOfIntegers(int digits) {
if(digits == 1)
return 10;

int count = (int) Math.pow(10, digits-1);
return count * 9;
}

/*
* 计算n位数的第一个数字,如二位数的第一个数字是10,三位数的第一个数字是100
*/
private int beginNumber(int digits) {
if(digits == 1)
return 0;
return (int)Math.pow(10, digits-1);
}
}

45 把数组排成最小的数

题目描述

输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。

题解

本题的关键在于定义一个规则判断两个数中谁应该排在前面,应该排在前面的数我们称其“小于”另一个数。例如,令m=32,n=2,则mn=322,nm=232,因为nm<mn,我们就称n小于m。之后我们便可使用这个比较方法(比较器)对数组中的所有元素进行排序即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.util.Arrays;
import java.util.Comparator;

public class Solution {
public String PrintMinNumber(int [] numbers) {
if(numbers == null || numbers.length <= 0)
return "";

StringBuilder res = new StringBuilder();
int len = numbers.length;
String[] str = new String[len];
for(int i = 0; i < len; i++) {
str[i] = numbers[i] + "";
}

Arrays.sort(str, new Comparator<String>() {
public int compare(String o1, String o2) {
String str1 = o1 + o2;
String str2 = o2 + o1;
return str1.compareTo(str2);
}
});

for(String s : str) {
res.append(s);
}

return res.toString();
}
}

46 把数字翻译成字符串

题目描述

给定一个数字,按照如下规则翻译成字符串:0 翻译成“a”,1 翻译成“b”… 25 翻译成“z”。一个数字有多种翻译可能,例如 12258 一共有 5 种,分别是 bccfi,bwfi,bczi,mcfi,mzi。实现一个函数,用来计算一个数字有多少种不同的翻译方法。

题解

如果用递归方法从上往下求解,必然会遇到许多重复的计算,因此可以从下往上进行求解。

我们可以先得到方程f(i) = f(i+1) + af(i+2),当第i个数与第i+1个数组成的数字在10-25的范围内,则a=1,否则a=0。以字符串“13225”为例,下标为0和1的数分别是1和3,组成的13是在10-25的范围内的,因此可以将其看成剩下的3225或者225这两种组合方式。以上是自顶向下的分析,再看看自底向上的实现,以下标为2的数2为例,首先就单独把这个数进行翻译,则加上上一个数的计算结果,又因为上一个数2和它组成的22是在10-25的范围内的,所以可以把它们组合在一起翻译,基于这种情况则再加上上上个数的结果,这两种情况的结果相加就是自底到这一个数的计算结果。一直循环到第一个数,dp[0]便是最终答案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Solution {
public int numDecodings(String s) {
int[] dp = new int[s.length()+1];
dp[s.length()] = 1;
dp[s.length()-1] = 1;
for(int i = s.length()-2; i >= 0; i--){
int a = s.charAt(i) - '0';
int b = s.charAt(i+1) - '0';
if(a * 10 + b > 25){
dp[i] = dp[i+1];
} else{
dp[i] = dp[i+1] + dp[i+2];
}
}
return dp[0];
}
}

47 礼物的最大价值

题目描述

小东所在公司要发年终奖,而小东恰好获得了最高福利,他要在公司年会上参与一个抽奖游戏,游戏在一个6*6的棋盘上进行,上面放着36个价值不等的礼物,每个小的棋盘上面放置着一个礼物,他需要从左上角开始游戏,每次只能向下或者向右移动一步,到达右下角停止,一路上的格子里的礼物小东都能拿到,请设计一个算法使小东拿到价值最高的礼物。

给定一个6*6的矩阵board,其中每个元素为对应格子的礼物价值,左上角为[0,0],请返回能获得的最大价值,保证每个礼物价值大于100小于1000。

题解

使用动态规划的思路:f(i,j) = max(f(i-1,j), f(i,j-1)),每一个坐标(i,j)的解只需要依赖其左边与上边的坐标,且最左边坐标的解只依赖上边的坐标,因此只需要一个一维数组作为缓存即可,该数组存有i行j列左边的所有解以及i-1行j列右边的所有解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Solution {
public int getMost(int[][] values) {
if(values == null || values.length == 0 || values[0].length == 0)
return 0;

int n = values[0].length;
int[] dp = new int[n];
for(int[] value : values) {
dp[0] += value[0];
for(int i = 1; i < n; i++) {
dp[i] = Math.max(dp[i], dp[i-1]) + value[i];
}
}
return dp[n-1];
}
}

48 最长不含重复字符的子字符串

题目描述

输入一个字符串(只包含 a~z 的字符),求其最长不含重复字符的子字符串的长度。例如对于 arabcacfr,最长不含重复字符的子字符串为 acfr,长度为 4。

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Solution {
public int LongestSubstringWithoutDupSolution(String str) {
if(str == null || str.length() <= 0)
return 0;

int maxLen = 0;
int curLen = 0;
int[] position = new int[26];
for(int i = 0; i < 26; i++) {
position[i] = -1;
}

for(int i = 0; i < str.length(); i++) {
//当前字母在position的下标index
int index = str.charAt(i) - 'a';
if(position[index] < 0) {
//如果这个字母之前没出现过
curLen += 1;
} else {
if(i-position[index] <= curLen) {
curLen = i-position[index];
} else {
curLen += 1;
}
}

position[index] = i;

if(curLen > maxLen)
maxLen = curLen;
}

return maxLen;
}
}

49 丑数

题目描述

把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Solution {
public int GetUglyNumber_Solution(int index) {
if(index <= 6)
return index;

int[] ugly = new int[index];
ugly[0] = 1;

int t2=0, t3=0, t5=0;

for(int i = 1; i < index; i++) {
ugly[i] = Math.min(ugly[t2]*2, Math.min(ugly[t3]*3, ugly[t5]*5));
while(ugly[t2] * 2 <= ugly[i])
t2++;
while(ugly[t3] * 3 <= ugly[i])
t3++;
while(ugly[t5] * 5 <= ugly[i])
t5++;
}
return ugly[ugly.length-1];
}
}

50.1 第一个只出现一次的字符

题目描述

在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写).

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.util.HashMap;

public class Solution {
public int FirstNotRepeatingChar(String str) {
if(str == null)
return -1;

HashMap<Character, Integer> map = new HashMap<>();
int index = -1;

for(int i = 0; i < str.length(); i++) {
char curr = str.charAt(i);
if(!map.containsKey(curr)) {
map.put(curr, 1);
} else {
int value = map.get(curr);
map.put(curr, ++value);
}
}

for(int i = 0; i < str.length(); i++) {
char curr = str.charAt(i);
if(map.get(curr) == 1) {
index = i;
break;
}
}
return index;
}
}

50.2 字符流中第一个不重复的字符

题目描述

请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符”go”时,第一个只出现一次的字符是”g”。当从该字符流中读出前六个字符“google”时,第一个只出现一次的字符是”l”。

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Solution {
private int[] map = new int[256];
private int index = 0;

public Solution() {
for(int i = 0; i < map.length; i++) {
map[i] = -1; //-1代表从未出现过
}
}

//Insert one char from stringstream
public void Insert(char ch) {
if(map[ch] == -1) {
map[ch] = index; //从未出现过,将它的下标赋值给它
} else {
map[ch] = -2; //出现过,则值为-2
}
index++;
}
//return the first appearence once char in current stringstream
public char FirstAppearingOnce() {
char result = '#';
int minIndex = Integer.MAX_VALUE;
for(int i = 0; i < map.length; i++) {
if(map[i] >= 0 && map[i] < minIndex) {
minIndex = map[i];
result = (char)i;
}
}
return result;
}
}

51 数组中的逆序对

题目描述

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007

题解

统计逆序对的过程:先把数组分隔成子数组,统计出子数组内部的逆序对的数目,然后再统计出两个相邻子数组之间的逆序对的数目。在统计逆序对的过程中,还需要对数组进行归并排序,而计算逆序对数目其实就是在进行归并排序的时候完成的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class Solution {
private long cnt = 0;
private int[] tmp;

public int InversePairs(int [] array) {
tmp = new int[array.length];
if(array == null || array.length <= 0) {
return -1;
}

mergeSort(array, 0, array.length-1);
return (int) (cnt % 1000000007);
}

private void mergeSort(int[] a, int lo, int hi) {
if(hi <= lo)
return;
int m = lo + (hi - lo) / 2;

mergeSort(a, lo, m);
mergeSort(a, m+1, hi);
merge(a, lo, m, hi);
}

private void merge(int[] a, int lo, int m, int hi) {
int i = lo, j = m+1, k = lo;
while(k <= hi) {
if(i > m) {
tmp[k] = a[j++];
} else if(j > hi) {
tmp[k] = a[i++];
} else if(a[i] < a[j]) {
tmp[k] = a[i++];
} else {
tmp[k] = a[j++];
cnt += m - i + 1;
}
k++;
}
for(k = lo; k <= hi; k++) {
a[k] = tmp[k];
}
}
}

52 两个链表的第一个公共结点

题目描述

输入两个链表,找出它们的第一个公共结点。

1
2
3
4
5
6
7
8
public class ListNode {
int val;
ListNode next = null;

ListNode(int val) {
this.val = val;
}
}

题解

此题大体有两种思路:

1、如果从后往前遍历两条链表,那么最后一个相同的节点就是我们要找的节点。这种思路要解决的问题在于链表是单向链表,该怎么逆序遍历链表。

2、如果从前往后遍历两条链表,那么第一个相同的节点就是我们要找的节点。这种思路要解决的问题在于如果两条链表的长度不同,便无法同时到达第一个公共节点,进而也就无法比较是否相等。

思路一:以空间换时间

将两个链表分别装到两个栈中,每次取出链表尾部的一个节点判断是否相等,最后一个相等的节点即为两个链表的第一个公共节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Solution {
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
if(pHead1 == null || pHead2 == null)
return null;

Stack<ListNode> stack1 = new Stack<>();
Stack<ListNode> stack2 = new Stack<>();
ListNode h1 = pHead1;
ListNode h2 = pHead2;
ListNode common = null;
while(h1!=null)
{
stack1.push(h1);
h1 = h1.next;
}
while(h2 != null) {
stack2.push(h2);
h2 = h2.next;
}
while(!stack1.empty() && !stack2.empty()) {
ListNode node1 = stack1.pop();
ListNode node2 = stack2.pop();
if(node1 == node2) {
common = node1;
}
}
return common;
}
}

思路二:进一步优化

上一种思路需要两个栈作为辅助空间,其实完全可以不用辅助空间,先分别遍历两个链表并记录他们的长度,长链表先走几步以此和短链表在同一起点出发,之后便可以同时遍历直至找出相同的节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class Solution {
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
if(pHead1 == null || pHead2 == null)
return null;

int len1 = 0;
int len2 = 0;
int diff; //两条链表的长度差
ListNode listLong; //标识链表的长短
ListNode listShort;
ListNode h1 = pHead1; //用于遍历的节点
ListNode h2 = pHead2;

while(h1!=null)
{
len1++;
h1 = h1.next;
}
while(h2 != null) {
len2++;
h2 = h2.next;
}
if(len1 > len2) {
listLong = pHead1;
listShort = pHead2;
diff = len1 - len2;
} else {
listLong = pHead2;
listShort = pHead1;
diff = len2 - len1;
}

for(int i = 0; i < diff; i++) {
listLong = listLong.next;
}

while(listLong != null && listShort != null) {
if(listLong == listShort)
return listLong;
listLong = listLong.next;
listShort = listShort.next;
}

return null;
}
}

53.1 数字在排序数组中出现的次数

题目描述

统计一个数字在排序数组中出现的次数。

题解

最直观的做法是顺序扫描,时间复杂度为O(n),不是最优解。由于输入的数组是排序的,那么就可以用二分查找的思路,找到第一个要查找的数字和最后一个要查找的数字,其坐标差即为该数字出现的次数。此时时间复杂度为O(logn)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class Solution {
public int GetNumberOfK(int [] array , int k) {
if(array == null || array.length <= 0)
return 0;
int start = getFirstK(array, k);
int end = getLastK(array, k);
int num = 0;
if(start != -1 && end != -1) {
num = end - start + 1;
}
return num;
}

public int getFirstK(int[] array, int k) {
int lo = 0;
int hi = array.length - 1;
int middle;
while(lo <= hi) {
middle = lo + (hi - lo) / 2;
if(array[middle] < k) {
lo = middle + 1;
} else if(array[middle] > k) {
hi = middle - 1;
} else {
if(middle - 1 >= 0 && array[middle - 1] == k) {
hi = middle - 1;
} else {
return middle;
}
}
}
return -1;
}

public int getLastK(int[] array, int k) {
int lo = 0;
int hi = array.length - 1;
int middle;
while(lo <= hi) {
middle = lo + (hi - lo) / 2;
if(array[middle] < k) {
lo = middle + 1;
} else if(array[middle] > k) {
hi = middle - 1;
} else {
if(middle + 1 < array.length && array[middle + 1] == k) {
lo = middle + 1;
} else {
return middle;
}
}
}
return -1;
}
}

53.2 0至n-1中缺失的数字

题目描述

在范围0~n-1内的n个数字中有且只有一个数字不在长度为n-1的递增排序数组(数字唯一)中,请找出这个数字。例如,{1,2,3,4}中少了0,{0,1,2,3}中少了4,{0,1,3,4}中少了2。

题解

用二分查找法找到第一个数字与下标不同的元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Solution {
public int getMissingNumber(int[] array) {
if(array == null || array.length <= 0)
return -1;
int lo = 0;
int hi = array.length - 1;
int middle;
while(lo <= hi) {
middle = lo + (hi - lo) / 2;
if(array[middle] == middle) {
lo = middle + 1;
} else {
if(middle - 1 >= 0 && array[middle - 1] != middle - 1) {
hi = middle - 1;
} else {
return middle;
}
}
}
return array.length;
}
}

53.3 数组中数值和下标相等的元素

题目描述

假设一个单调递增的数组里的每个元素都是整数并且是唯一的。请编写实现一个函数,找出数组中任意一个数值等于其下标的元素。

题解

由于每个数都是唯一的,如果第i个数字的值大于i,那么它右边的数字都大于对应的下标;如果第i个数字的值小于i,那么它左边的数字都小于对应的下标。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Solution {
public int IntegerIdenticalToIndex(int[] array) {
if(array == null || array.length <= 0)
return -1;
int lo = 0;
int hi = array.length - 1;
int middle;
while(lo <= hi) {
middle = lo + (hi - lo) / 2;
if(array[middle] > middle) {
hi = middle - 1;
} else if(array[middle] < middle) {
lo = middle + 1;
} else {
if(middle - 1 >= 0 && array[middle - 1] == middle - 1) {
hi = middle - 1;
} else {
return middle;
}
}
}
return -1;
}
}

54 二叉搜索树的第k个结点

题目描述

给定一棵二叉搜索树,请找出其中的第k小的结点。例如,(5,3,7,2,4,6,8)中,按结点数值大小顺序第三小结点的值为4。

1
2
3
4
5
6
7
8
9
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;

public TreeNode(int val) {
this.val = val;
}
}

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Solution {
private int cnt;
private TreeNode target;

TreeNode KthNode(TreeNode pRoot, int k) {
if(pRoot == null || k <= 0)
return null;
KthNodeCore(pRoot, k);
return target;
}

void KthNodeCore(TreeNode pRoot, int k) {
if(pRoot == null || target != null) {
return;
}
KthNodeCore(pRoot.left, k);
cnt++;
if(cnt == k)
target = pRoot;
KthNodeCore(pRoot.right, k);
}
}

55.1 二叉树的深度

题目描述

输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。

1
2
3
4
5
6
7
8
9
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;

public TreeNode(int val) {
this.val = val;
}
}

题解

1
2
3
4
5
6
7
8
9
public class Solution {
public int TreeDepth(TreeNode root) {
if(root == null)
return 0;
int left = TreeDepth(root.left);
int right = TreeDepth(root.right);
return left > right ? (left+1):(right+1);
}
}

55.2 平衡二叉树

题目描述

输入一棵二叉树,判断该二叉树是否是平衡二叉树。

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Solution {
public boolean IsBalanced_Solution(TreeNode root) {
return IsBalanced(root) != -1;
}

public int IsBalanced(TreeNode root) {
if(root == null)
return 0;
int left = IsBalanced(root.left);
if(left == -1)
return left;
int right = IsBalanced(root.right);
if(right == -1)
return right;
return Math.abs(left-right) > 1 ? -1 : 1+Math.max(left,right);
}
}

56.1 数组中只出现一次的数字

题目描述

一个整型数组里除了两个数字之外,其他的数字都出现了偶数次。请写程序找出这两个只出现一次的数字。

题解

因为任何一个数字异或它自己都等于0,而0异或任何一个数字都等于其本身,所以可以将数组中的所有数字都异或,例如对于含有一个数字只出现一次的数组{3,3,4,4,6}:

1
2
3
    3 ^ 3 ^ 4 ^ 4 ^ 6
-> 0 ^ 0 ^ 6
-> 6

而在此题中,数组里有两个数字只出现了一次,所以从头到尾异或数组中的每个数字会得到这两个数字的异或结果,由于这两个数字肯定不同,所以异或结果至少会包含一个1,我们以最右侧的1为标准将这两个数分到两个子数组中,于此同时这一位为1或0的出现两次的数字也会分别到这两个子数组中,然后再对两个子数组运用最上面的思路。

在这里,diff &= -diff可以得到只有最右侧为1的数,以此作为分割标准。(在计算机中,负数以其正值的补码形式表达,补码=反码+1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Solution {
public void FindNumsAppearOnce(int[] nums, int num1[], int num2[]) {
int diff = 0;
for (int num : nums)
diff ^= num;
diff &= -diff;
for (int num : nums) {
if ((num & diff) == 0)
num1[0] ^= num;
else
num2[0] ^= num;
}
}
}

56.2 数组中唯一只出现一次的数字

题目描述

在一个数组中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。

题解

把数组中所有数字的二进制表示的每一位都加起来,如果某一位的和能被3整除,那么那个只出现一次的数字二进制表示中对应的那一位是0,否则就是1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class NumberAppearingOnce {
public int solution(int numbers[]) {
if(numbers == null || numbers.length == 0)
return -1;
int[] bitSum = new int[32];
for(int i = 0; i < numbers.length; i++) {
int bitMask = 1;
for(int j = 31; j >= 0; j--) {
int bit = numbers[i] & bitMask;
if(bit != 0)
bitSum[j] += 1;
bitMask = bitMask << 1;
}
}

int result = 0;
for(int i = 0; i < 32; i++) {
result = result << 1;
result += bitSum[i] % 3;
}
return result;
}
}

57.1 和为s的两个数字

题目描述

输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。

题解

定义两个指针,一个指向数组头,一个指向数组末尾,如果指针指向的这两个数字相加小于S,则将头指针向后移动一位,否则将尾指针向前移动一位。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.util.ArrayList;

public class Solution {
public ArrayList<Integer> FindNumbersWithSum(int [] array,int sum) {
ArrayList<Integer> result = new ArrayList<>();
if(array == null || array.length == 0)
return result;

int lo = 0;
int hi = array.length - 1;
while(lo < hi) {
int curSum = array[lo] + array[hi];
if(curSum < sum) {
lo++;
} else if(curSum > sum) {
hi--;
} else {
result.add(array[lo]);
result.add(array[hi]);
break;
}
}
return result;
}
}

57.2 和为s的连续正数序列

题目描述

小明很喜欢数学,有一天他在做数学作业时,要求计算出9~16的和,他马上就写出了正确答案是100。但是他并不满足于此,他在想究竟有多少种连续的正数序列的和为100(至少包括两个数)。没多久,他就得到另一组连续正数和为100的序列:18,19,20,21,22。现在把问题交给你,你能不能也很快的找出所有和为S的连续正数序列? Good Luck!

题解

首先把lo和hi分别初始化为1和2(因为连续序列为正,且至少含有两个数字),如果lo和hi之间的数字相加大于S,将lo加一,而如果lo和hi之间的数字相加小于S,则将hi加一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.util.ArrayList;

public class Solution {
public ArrayList<ArrayList<Integer> > FindContinuousSequence(int sum) {
ArrayList<ArrayList<Integer>> listAll = new ArrayList<>();
if(sum <= 0)
return listAll;

int lo = 1;
int hi = 2;
int middle = sum / 2;
int curSum = lo + hi;
while(lo <= middle) {
if(curSum < sum) {
hi++;
curSum += hi;
} else if(curSum > sum) {
curSum -= lo;
lo++;
} else {
ArrayList<Integer> list = new ArrayList<>();
int i = lo;
while(i <= hi)
list.add(i++);
listAll.add(list);
curSum += ++hi;
}
}
return listAll;
}
}

58.1 翻转单词序列

题目描述

牛客最近来了一个新员工Fish,每天早晨总是会拿着一本英文杂志,写些句子在本子上。同事Cat对Fish写的内容颇感兴趣,有一天他向Fish借来翻看,但却读不懂它的意思。例如,“student. a am I”。后来才意识到,这家伙原来把句子单词的顺序翻转了,正确的句子应该是“I am a student.”。Cat对一一的翻转这些单词顺序可不在行,你能帮助他么?

题解

进行两次翻转:首先将整体进行翻转,得到.tneduts a ma I,再将每个单词进行局部翻转,得到student. a am I即为答案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Solution {
public String ReverseSentence(String str) {
if(str == null)
return null;
else if(str == "")
return "";
char[] data = str.toCharArray();
int i = 0, j = data.length-1;
reverse(data, i, j);
j = 0;
while(i < data.length) {
if(j == data.length || data[j] == ' ') {
reverse(data, i, j-1);
i = j + 1;
}
j++;
}
return new String(data);
}

private void reverse(char[] data, int i, int j) {
while(i <= j) {
char temp = data[i];
data[i] = data[j];
data[j] = temp;
i++;
j--;
}
}
}

58.2 左旋转字符串

题目描述

汇编语言中有一种移位指令叫做循环左移(ROL),现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。是不是很简单?OK,搞定它!

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Solution {
public String LeftRotateString(String str,int n) {
if(str == null)
return null;
else if(str == "")
return "";
else if(n >= str.length())
return str;

char[] data = str.toCharArray();
reverse(data, 0, n-1);
reverse(data, n, data.length-1);
reverse(data, 0, data.length-1);
return new String(data);
}

private void reverse(char[] data, int i, int j) {
while(i <= j) {
char temp = data[i];
data[i] = data[j];
data[j] = temp;
i++;
j--;
}
}
}

60 n个骰子的点数

题目描述

把 n 个骰子仍在地上,求点数和为 s 的概率。

题解

我们以n表示要扔的骰子数,s为所有骰子的点数之和,f(n, s)表示扔n个骰子时所有骰子的点数之和为s的排列情况总数。例如,n=2,s=5时,f(n, s) = f(2, 5) = 4 (4种情况即{1, 4}, {4, 1}, {2, 3}, {3, 2}

因为一个骰子有六个点数,那么第n个骰子可能出现1到6的点数,当第n个骰子点数为1的话,f(n,s) = f(n-1, s-1),当第n个骰子点数为2的话,f(n,s) = f(n-1, s-2),…,依次类推。

由以上分析我们便可以得到状态转移方程:f(n,s)=f(n-1,s-1)+f(n-1,s-2)+f(n-1,s-3)+f(n-1,s-4)+f(n-1,s-5)+f(n-1,s-6)

使用递归

得到状态方程后,最直观的就是使用递归求解。点数和的最小值为骰子数n,而最大值为6 * n。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Solution {
public List<Map.Entry<Integer, Double>> dicesSum(int n) {
List<Map.Entry<Integer, Double>> result = new ArrayList<>();
int minSum = n;
int maxSum = 6 * n;
double totalCase = Math.pow(6, n);
for(int i = minSum; i <= maxSum; i++) {
result.add(new AbstractMap.SimpleEntry<>(i, dicesSumCore(n, i) / totalCase));
}
return result;
}

private int dicesSumCore(int n, int sum){
if(n<1||sum<n||sum>6*n){
return 0;
}
if(n==1){
return 1;
}
int resCount=0;
resCount=dicesSumCore(n-1,sum-1)+dicesSumCore(n-1,sum-2)+
dicesSumCore(n-1,sum-3)+dicesSumCore(n-1,sum-4)+
dicesSumCore(n-1,sum-5)+dicesSumCore(n-1,sum-6);
return resCount;
}
}

动态规划

使用递归求解会产生大量重复的计算,所以使用动态规划更好。

在以下代码中使用了一个二维数组dp[2][maxSum+1],dp[0]和dp[1]表示当前状态和前一个状态(由状态转移方程f(n,s)=f(n-1,s-1)+f(n-1,s-2)+f(n-1,s-3)+f(n-1,s-4)+f(n-1,s-5)+f(n-1,s-6)可以看出当前状态仅依赖前一个状态,所以只用两个一维数组即可),而这两个状态的数组使用flag变量进行旋转。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Solution {
public List<Map.Entry<Integer, Double>> dicesSum(int n) {
List<Map.Entry<Integer, Double>> result = new ArrayList<>();
if(n < 1)
return result;
int face = 6;
int minSum = n;
int maxSum = face * n;
int flag = 1;
double totalCase = Math.pow(face, n); //总共有6的n次方种排列情况
long[][] dp = new long[2][maxSum+1]; //dp[flag][j]表示当前状态下产生点数和为j的排列次数

//设置初始状态,即f(1,1) = f(1,2) = f(1,3) = f(1,4) = f(1,5) = f(1,6) = 1
for(int i = 1; i <= face; i++)
dp[0][i] = 1;

//i表示当前扔出的骰子数,骰子数为1的情况在上面已经有过初始化
for (int i = 2; i <= n; i++, flag = 1 - flag) {
//将表示当前状态的数组清零
for (int j = 0; j <= maxSum; j++)
dp[flag][j] = 0;

for (int j = i; j <= maxSum; j++)
for (int k = 1; k <= face && k <= j; k++)
//此处即体现出状态转移方程
dp[flag][j] += dp[1 - flag][j - k];
}

for(int i = minSum; i <= maxSum; i++) {
result.add(new AbstractMap.SimpleEntry<>(i, dp[1 - flag][i] / totalCase));
}

return result;
}
}

61 扑克牌顺子

题目描述

从扑克牌中随机抽5张牌,判断是不是一个顺子,即这5张牌是不是连续的。大小王可看成任意数字。

题解

把大小王看成0,首先把数组排序,其次统计数组中0的个数,最后统计排序后的数组中相邻数字之间的空缺总数。如果空缺总数小于或者等于0的个数,那么这个数组就是连续的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.Arrays;

public class Solution {
public boolean isContinuous(int [] numbers) {
if(numbers == null || numbers.length == 0)
return false;

int numOfZero = 0;
int numOfGap = 0;

Arrays.sort(numbers);

for(int i = 0; i < numbers.length && numbers[i] == 0; i++) {
numOfZero++;
}

for(int i = numOfZero+1; i < numbers.length; i++) {
if(numbers[i] == numbers[i-1])
return false;
numOfGap += numbers[i] - numbers[i-1] - 1;
}
return numOfZero >= numOfGap ? true : false;
}
}

62 圆圈中最后剩下的数字

题目描述

首先,让小朋友们围成一个大圈。然后,他随机指定一个数m,让编号为0的小朋友开始报数。每次喊到m-1的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续0…m-1报数….这样下去….直到剩下最后一个小朋友,可以不用表演,并且拿到牛客名贵的“名侦探柯南”典藏版。请你试着想下,哪个小朋友会得到这份礼品呢?(注:小朋友的编号是从0到n-1)

题解

环形链表法

采用链表来模拟整个过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.LinkedList;

public class Solution {
public int LastRemaining_Solution(int n, int m) {
if(n < 1 || m < 1)
return -1;

LinkedList<Integer> list = new LinkedList<>();
int index = 0;

for(int i = 0; i < n; i++) {
list.add(i);
}
while(list.size() > 1) {
index = (index + m - 1) % list.size();
list.remove(index);
}
return list.get(0);
}
}

公式法

我们可以根据此公式使用递归或者循环来做:f(n,m) = [f(n-1,m) + m] % n

1
2
3
4
5
6
7
8
9
public class Solution {
public int LastRemaining_Solution(int n, int m) {
if (n == 0)
return -1;
if (n == 1)
return 0;
return (LastRemaining_Solution(n - 1, m) + m) % n;
}
}

63 股票的最大利润

题目描述

假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可获得的最大利润是多少?

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MaximalProfit {
public int maxProfit(int[] prices) {
if(prices == null || prices.length == 0)
return 0;
int minPrice = prices[0];
int maxProfit = 0;
for(int i = 1; i < prices.length; i++) {
if(prices[i] < minPrice)
minPrice = prices[i];
int currProfit = prices[i] - minPrice;
if(currProfit > maxProfit)
maxProfit = currProfit;
}
return maxProfit;
}
}

64 求1+2+···+n

题目描述

题解

1
2
3
4
5
6
7
public class Solution {
public int Sum_Solution(int n) {
int sum = n;
boolean flag = (sum > 0) && ((sum += Sum_Solution(--n)) > 0);
return sum;
}
}

65 不用加减乘除做加法

题目描述

写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。

题解

十进制加法三步走:
1、只做各位相加不进位
2、求出进位值
3、把前面两个结果加起来

而对于二进制也正是如此。使用异或完成相加的操作,而使用位与运算再左移完成进位的操作。

1
2
3
4
5
6
7
8
9
10
11
12
public class Solution {
public int Add(int num1,int num2) {
int sum, carry;
do {
sum = num1 ^ num2;
carry = (num1 & num2) << 1;
num1 = sum;
num2 = carry;
} while(carry != 0);
return sum;
}
}

拓展

不使用新变量交换两个变量的值:

基于加减法 基于异或运算
a = a + b a = a ^ b
b = a - b a = a ^ b
a = a - b a = a ^ b

66 构建乘积数组

题目描述

给定一个数组A[0,1,…,n-1],请构建一个数组B[0,1,…,n-1],其中B中的元素B[i]=A[0] A[1] A[i-1] A[i+1] A[n-1]。不能使用除法。

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Solution {
public int[] multiply(int[] A) {
int[] B = new int[A.length];
if(A == null || A.length == 0)
return B;

B[0] = 1;
for(int i = 1; i < A.length; i++) {
B[i] = B[i-1] * A[i-1];
}

int temp = 1;
for(int i = B.length - 2; i >= 0; i--){
temp = A[i+1] * temp;
B[i] = temp * B[i];
}
return B;
}
}

67 把字符串转换成整数

题目描述

将一个字符串转换成一个整数(实现Integer.valueOf(string)的功能,但是string不符合数字要求时返回0),要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0。

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Solution {
public int StrToInt(String str) {
if(str == null || str.length() == 0)
return 0;
boolean neg = str.charAt(0) == '-';
int num = 0;
for(int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if(i == 0 && (c == '+' || c == '-')) continue;
if(c < '0' || c > '9') return 0;
int temp = num;
num = num * 10 + (c - '0');
if((num - c + '0') / 10 != temp) return 0;
}
return neg ? -num : num;
}
}

68.1 二叉查找树中两个节点的最低公共祖先

题目描述

找到二叉查找树中两个节点的最低公共祖先

1
2
3
4
5
6
7
8
9
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/

题解

从根节点开始向下查找直到找到满足root.val >= p.valroot.val <= q.val的节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
while(root != null) {
if(root.val > p.val && root.val > q.val)
root = root.left;
else if(root.val < p.val && root.val < q.val)
root = root.right;
else
return root;
}
return null;
}
}

68.2 普通二叉树中两个节点的最低公共祖先

题目描述

找到普通二叉树中两个节点的最低公共祖先

1
2
3
4
5
6
7
8
9
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/

题解

深度优先搜索的思想:

1
2
3
4
5
6
7
8
9
10
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null || root.val == p.val || root.val == q.val)
return root;

TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
return left == null ? right : right == null ? left : root;
}
}