怕什么真理无穷,进一寸有一寸的欢喜

0%

算法笔记

OJ处理技巧

类名为public class Main

较好的做法是将要用到的类都封装好,不要放在Main类中,主类只需要持有要用到的类的对象,然后调用即可。

StringBuilder类的append方法,res.append(str1).append(str2)比res.append(str1+str2)效率高。

其中牛客网要求格式化输出数据,可以用DecimalFormat类,进行一位小数的输出。

1
DecimalFormat df = new DecimalFormat("0.00000");

或者使用String的format方法,支持float与double

1
String result = String.format("%.1f",data);

拷贝数组

1
2
Arrays.copyOf(T arr, int new Length);
Arrays.copyOf(T arr, int start, int end);// 不包括end

获取不知次数的输入

使用BufferedReader来获取,需要Import,主函数需要抛出异常

valueOf返回值类型为Integer,parseInt返回值类型为int

使用Scanner类中的hasNext()方法与nextLine()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Main {
public static void main(String[] args) throws IOException{
Scanner in = new Scanner(System.in);
while(in.hasNext()){
//获取第一行输入,输入为一个数
int len = Integer.valueOf(in.nextLine());
//获取第二行输入,输入为数组
String[] arrStr = in.nextLine().split(" ");
int [] arr = new int[len];
for (int i = 0; i < arr.length; i++) {
arr[i] = Integer.valueOf(arrStr[i]);
}
System.out.println(要求的函数);
}
}
}

获取有限次数的输入

输出的结果先用StringBuilder类存起来,如果有多行,每一行最后要加上换行标识。最后要输出的时候,将stringbuilder转变为String类输出。

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.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main{
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
//要持有的写的类
DogCatQueue dq = new DogCatQueue();
String s = null;
StringBuilder res = new StringBuilder();
s = br.readLine();
int count = Integer.valueOf(s);
for (int i = 0; i < count; i++) {
String[] strArr = br.readLine().split(" ");
//if或者switch判断
switch (strArr[0]){
case "add":
String type=strArr[1];
dq.add(new Pet(type,Integer.valueOf(strArr[2])));
break;
case "pollAll":
while(!dq.isEmpty()){
pet = dq.pollAll();
res.append(pet.getPetType()+" "+pet.getPetIndex()+"\n");
}
break;
case "isDogEmpty" :
res.append(dq.isDogEmpty()?"yes\n":"no\n");
break;
}
}
System.out.println(res.substring(0,res.length()-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
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
//要持有的写的类
//PringtEdge pe = new PringtEdge();
String[] s = null;
StringBuilder res = new StringBuilder();
s = br.readLine().split(" ");
//获取第一行的矩阵行与列
int row = Integer.valueOf(s[0]);
int col = Integer.valueOf(s[1]);
int[][] arr = new int[row][col];
//获取后面几行的矩阵数据
for (int i = 0; i < row; i++) {
s = br.readLine().split(" ");
for (int j = 0; j < col; j++) {
arr[i][j] = Integer.valueOf(s[j]);
}
}
//通过自己编写方法获取结果
//res = pe.printCircle(arr);
System.out.println(res.substring(0,res.length()-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.Scanner;
//交换数组中的数
public static void swap(int[] arr,int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
//打印数组
public static void printArr(int[] arr){
for(int i = 0; i < arr.length; i++){
System.out.print(arr[i]+" ");
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String str = sc.nextLine();
String[] strArray = str.split(" ");
int[] intArray = new int[strArray.length];
for(int i = 0;i
intArray[i] = Integer.parseInt(strArray[i]);
}
//要解决的
Solution(intArray);
//输出数组
for(int i = 0;i
System.out.print(intArray[i] + " ");
}
//or
printArr(printArr);
}

输入有两行,第一行为数组长度,第二行为数组

获取long类型的数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int len = Integer.parseInt(br.readLine());
long[] arr = new long[len];
String[] strArray = br.readLine().split(" ");
for(int i = 0;i
arr[i] = Long.parseLong(strArray[i]);
}
LessMoney lm = new LessMoney();
System.out.println(lm.getLessMoney(arr));
}

获取链表

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.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

//链表结构
class Node{
int value;
Node next;
Node pre;
Node(int value){
this.value = value;
}
}

//创建单向链表
private static Node createNode(String[] str,int n){
Node head = new Node(Integer.parseInt(str[0]));
Node node = head;
for(int i=1;i
Node newNode = new Node(Integer.parseInt(str[i]));
node.next = newNode;
node = newNode;
}
return head;
}
//创建双向链表
private static Node createNodeDL(String[] str,int n){
Node head = new Node(Integer.parseInt(str[0]));
Node node = head;
for(int i=1;i
Node newNode = new Node(Integer.parseInt(str[i]));
node.next = newNode;
node.next.pre = node;
node = newNode;
}
return head;
}
//打印列表
private static void printList(Node node){
StringBuilder builder = new StringBuilder();
while (node != null){
builder.append(node.value).append(" ");
node = node.next;
}
System.out.println(builder.toString());
}
//主函数部分
public static void main(String[] args) throws IOException{
BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
//创建第一个链表
int n = Integer.parseInt(input.readLine());
String[] strings1 = input.readLine().split(" ");
Node list1 = createNode(strings1,n);
//创建第二个链表
int m = Integer.parseInt(input.readLine());
String[] strings2 = input.readLine().split(" ");
Node list2 = createNode(strings2,m);
//要操作的函数
//printCommonPart(list1,list2);
}

获取树

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
62
63
64
65
66
67
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Stack;

class Node{
public int value;
public Node left;
public Node right;
public Node(int value){
this.value = value;
}
}

public static void main(String[] args) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
//读取总的节点数与根节点数
String[] s=reader.readLine().split(" ");
int n = Integer.parseInt(s[0]);
int rootIdx = Integer.parseInt(s[1]);
//构造桶,将每个节点放入对应值,分别存其左、右结点的值
int[][] arr = new int[n+1][2];
int t;
//将节点的数放入对应的桶中,构造得到数组
for (int i = 1; i < n; i++){
String[] sts = reader.readLine().split(" ");
t = Integer.parseInt(sts[0]);
arr[t][0] = Integer.parseInt(sts[1]);
arr[t][1] = Integer.parseInt(sts[2]);
}
//拿出头节点
Node head = new Node(rootIdx);
createTree(head,arr);
StringBuilder res = new StringBuilder();
preOrderRecur(head,res);
System.out.println(res.substring(0,res.length()-1));
res.delete(0,res.length());
inOrderRecur(head,res);
System.out.println(res.substring(0,res.length()-1));
res.delete(0,res.length());
posOrderRecur(head,res);
System.out.println(res.substring(0,res.length()-1));

}
//递归实现
public static void createTree(Node root,int[][] a){
//base case
if(root==null){
return ;
}
int i=root.value;
int l=a[i][0];
int r=a[i][1];
//左子树不为空,构造左子树
if(l!=0){
Node leftNode=new Node(l);
root.left=leftNode;
//递归产生左子树
createTree(leftNode,a);
}
//右子树不为空,构造右子树
if(r!=0){
Node rightNode=new Node(r);
root.right=rightNode;
createTree(rightNode,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
52
53
54
55
56
57
58
59
60
61
62
63
64
	//产生数组的对数器
//1,有一个随机样本产生器
//2,准备绝对正确的方法
//3,大样本测试
int testTime = 500000;
int size = 10;
int value = 100;
boolean succeed = true;
//进行testTime次测试
for (int i = 0; i < testTime; i++) {
int[] arr1 = generateRandomArray(size,value);//准备测试样本
int[] arr2 = copyArray(arr1);
int[] arr3 = copyArray(arr1);
//待测试方法
shellSort2(arr1);
//绝对正确方法
rightMethod(arr2);
if(!isEqual(arr1,arr2)){
succeed = false;
printArray(arr3);
break;
}
}
System.out.println(succeed?"Nice!":"False!");
}
//随机样本发生器
public static int[] generateRandomArray(int size, int value){
//Math.Random -> double[0,1)
//产生的长度为0-size
int[] arr = new int[(int)((size+1)*Math.random())];
//产生的值为-value-value
for(int i = 0;i< arr.length;i++){
arr[i] = (int)((value+1) * Math.random()) - (int)(value * Math.random());
}
return arr;
}
//正确方法
public static void rightMethod(int[] arr){
Arrays.sort(arr);
}
//拷贝数组
public static int[] copyArray(int[] arr){
return Arrays.copyOf(arr,arr.length);
}
//数组输出
public static void printArray(int[] arr){
for(int i : arr)
System.out.print(i+" ");
System.out.println();
}
//比较数组
public static boolean isEqual(int[] arr1,int[] arr2){
if((arr1==null&&arr2!=null)||(arr1!=null&&arr2==null))
return false;
if(arr1 == null && arr2 ==null)
return true;
if(arr1.length != arr2.length)
return false;
for (int i = 0; i < arr1.length; i++) {
if(arr1[i] != arr2[i])
return false;
}
return true;
}

排序问题

排序算法

冒泡排序

1
2
3
4
5
6
7
8
9
10
11
12
13
//思想:第一个排到最后一个,第一个排到倒数第二个,以此类推
public static void bubbleSort(int[] arr){
if (arr == null || arr.length<2)
return;
//两层循环
for (int end = arr.length-1; end > 0; end--) {
for (int start = 0; start < end; start++) {
//如果当前数比第二个数大,则交换
if (arr[start]>arr[start+1])
swap(arr,start,start+1);
}
}
}

选择排序

1
2
3
4
5
6
7
8
9
10
11
12
//思想:0-N-1比较,找最小的放第一个,1-N-1比较,找最小的放第二个,以此类推
public static void selectSort(int[] arr){
if (arr == null || arr.length<2)
return;
for (int i = 0; i < arr.length-1; i++) {
int minIndex = i;
for (int j = i+1; j < arr.length; j++) {
minIndex = arr[minIndex]>arr[j]?j:minIndex;
}
swap(arr,i,minIndex);
}
}

插入排序

1
2
3
4
5
6
7
8
9
10
//思想:如果当前数一直比左边的小,就左右交换
public static void insertSort(int[] arr){
if (arr == null || arr.length<2)
return;
for (int i = 1; i < arr.length; i++) {
for (int j = i-1; j >=0 && arr[j]>arr[j+1]; j--) {
swap(arr,j,j+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
public static void shellSort(int[] arr){
if (arr == null || arr.length < 2)
return;
//gap为arr.length/2到1
int temp = 0, j = 0;
for (int gap = arr.length/2; gap > 0; gap--) {
//从gap开始,后移一位
for (int i = gap; i < arr.length; i++) {
//记录开始的位置
j = i;
temp = arr[j];//此轮被比较的数
//只有在此组前一个数比当前大,才进行
if (arr[j - gap] > arr[j]){
//找到这个组中j的位置
while (j-gap >= 0 && arr[j - gap] > temp){
arr[j] = arr[j-gap];//上一个大的数到当前位置
j -= gap;//更新j的位置
}
//将这轮比较的数放在自己的位置
arr[j] = temp;
}
}
}
}

归并排序

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
//思想:将左边排好,将右边排好,然后将左右两边用外排的方式排好
public static void mergeSort(int[] arr){
if (arr == null || arr.length<2)
return;
sortProcess(arr,0,arr.length-1);
}
public static void sortProcess(int[]arr,int left,int right){
//base case
if (left==right)
return;
int mid = left + (right-left)/2;
sortProcess(arr,left,mid);
sortProcess(arr,mid+1,right);
merge(arr,left,mid,right);
}
public static void merge(int[] arr,int L,int mid,int R){
int[] help = new int[R-L+1];
int p1 = L,p2 = mid+1,i=0;
while (p1<=mid && p2<=R){
//谁小动谁
help[i++]=arr[p1]<=arr[p2]?arr[p1++]:arr[p2++];
}
while (p1<=mid){
help[i++]=arr[p1++];
}
while (p2<=R){
help[i++]=arr[p2++];
}
//将help拷贝至arr
for (int j = 0; j < help.length; j++) {
arr[L+j] = help[j];
}
}

快速排序

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
//思想:核心为partition部门,荷兰国旗思想,将数组分组小于,等于,大于区
public static void quickSort(int[] arr,int L, int R){
//base case:L=R
if (L < R){
//产生一个随机下标,和最后一位交换
swap(arr,L+(int)Math.random()*(R-L+1),R);
int[] p = partition(arr,L,R);
quickSort(arr,L,p[0]-1);
quickSort(arr,p[1]+1,R);
}
}
//然后对小于和大于区继续进行partition过程
public static int[] partition(int[] arr,int L,int R){
//小于区和大于区边界
int less = L-1,more = R;
//只要当前位置没有碰到大于区
while (L < more){
//如果当前数比最后一个数小,放在小于区
if (arr[L] < arr[R]){
swap(arr,L++,++less);
}else if (arr[L] > arr[R]){
//如果比最后一个数大,放在大于区,当前位置不变
swap(arr,L,--more);
}else {
//相等就判断下一个位置
L++;
}
}
swap(arr,more,R);
//返回等于区域的下标
return new int[]{less+1,more};
}

堆排序

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
//思想:先构造大根堆,然后将根节点取出,重新构造大根堆
public static void heapSort(int[] arr){
if (arr == null || arr.length<2)
return;
for (int i = 0; i < arr.length; i++) {
heapInsert(arr,i);
}
int heapSize = arr.length;
//取出一个,与最后个交换
swap(arr,0,--heapSize);
while (heapSize>0){
heapify(arr,0,heapSize);
swap(arr,0,--heapSize);
}
}
public static void heapInsert(int[] arr, int i){
while (arr[i] > arr[(i-1)/2]){
swap(arr,i,(i-1)/2);
i = (i-1)/2;
}
}
public static void heapify(int[] arr, int i, int size){
int left = 2*i+1;
while (left
int largest = (left+1)1]>arr[left] ? left+1 : left;

largest = arr[largest] > arr[i] ? largest : i;
if (largest == i)
return;
swap(arr,i,largest);
i = largest;
left = 2*i+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 static void bucketSort(int[] arr){
if (arr == null || arr.length<2)
return;
//统计最大数
int max = Integer.MIN_VALUE;
for (int i = 0; i < arr.length; i++) {
max = arr[i] > max ? arr[i] : max;
}
int[] bucket = new int[max+1];
//装入桶中
for (int i = 0; i < arr.length; i++) {
bucket[arr[i]]++;
}
int i = 0;
//从桶中倒出来
for (int j = 0; j < bucket.length; j++) {
while (bucket[j--] > 0){
//数组的值等于对应位置的值
arr[i++] = j;
}
}
}

小和问题

题目描述:

数组小和的定义如下:

例如,数组s = [1, 3, 5, 2, 4, 6],在s[0]的左边小于或等于s[0]的数的和为0;在s[1]的左边小于或等于s[1]的数的和为1;在s[2]的左边小于或等于s[2]的数的和为1+3=4;在s[3]的左边小于或等于s[3]的数的和为1;

在s[4]的左边小于或等于s[4]的数的和为1+3+2=6;在s[5]的左边小于或等于s[5]的数的和为1+3+5+2+4=15。所以s的小和为0+1+4+1+6+15=27

给定一个数组s,实现函数返回s的小和

[要求]

时间复杂度为O(nlogn),空间复杂度为O(n)

思路:找小和,就是看当前数右边哪些数比他大 ,顺序并没有关系,因此可以用mergesort的思想,先分再合,产生小和的过程为merge的过程,如果右边数比左边大,那么总的小和个数为左边当前数*右边数个数,然后将两个数组sort中产生的数和当前merge共三部门数相加即可。

坑:

最后的小和可能很大,需要long类型才能过OJ,小和为小于等于,因此merge外排时判断为<=

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.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main{
public static void main(String[] args) throws Exception{
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String s = null;
while((s=br.readLine())!=null){
int len = Integer.valueOf(s);
String[] arrStr = br.readLine().split(" ");
int[] arr = new int[len];
for(int i = 0;i < len;i++){
arr[i] = Integer.parseInt(arrStr[i]);
}
System.out.println(smallSum(arr));
}
}
public static long smallSum(int[] arr){
if(arr == null || arr.length < 2)
return 0;
return sortProcess(arr,0,arr.length-1);
}
public static long sortProcess(int[] arr,int l,int r){
if(l == r)
return 0;
int mid = l + (r - l)/2;
return sortProcess(arr,l,mid)+sortProcess(arr,mid+1,r)+merge(arr,l,mid,r);
}
public static long merge(int[] arr,int l,int mid, int r){
int p1 = l,p2 = mid+1,i = 0;
int[] help = new int[r-l+1];
long res = 0;
while(p1<=mid && p2 <= r){
if(arr[p1] <= arr[p2]){
res += arr[p1] * (r-p2+1);
help[i++] = arr[p1++];
}else{
help[i++] = arr[p2++];
}
}
while(p1<=mid){
help[i++] = arr[p1++];
}
while(p2<=r){
help[i++] = arr[p2++];
}
for(i = 0;i
arr[l+i] = help[i];
}
return res;
}
}

数组排序后相邻数的最大差值

题目描述

给定一个整形数组arr,返回排序后相邻两数的最大差值

arr = [9, 3, 1, 10]。如果排序,结果为[1, 3, 9, 10],9和3的差为最大差值,故返回6。

arr = [5, 5, 5, 5]。返回0。

[要求]

时间复杂度为O(n),空间复杂度为O(n)

当要排序,然后时间复杂度给定O(n),那么只能使用桶排序,思路为把n个数放在n+1个桶中,那么肯定有1个桶中没有数字,这样最大的差值一定出现在两个桶之间,那么桶中有哪些数字不重要,只需要关注桶中是否有数字,最大数字和最小数字即可,最大差值为当前非空桶的最小值减去上一个非空桶的最大值。将数字放入桶,依据为将min-max的数据,放入0-len的桶中,数据长度为为len,桶的个数为len+1。在计算时为了避免越界,采用long型,然后转换为int。可以优化的点在于,如果算出来数组的最大值和最小值相等,那么可以直接返回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
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
62
63
64
65
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;

public class Main{
public static void main(String[] args) throws Exception{
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String s = null;
while((s=br.readLine())!=null){
int len = Integer.valueOf(s);
int[] arr = new int[len];
String[] arrStr = br.readLine().split(" ");
for(int i = 0;i
arr[i] = Integer.valueOf(arrStr[i]);
}
System.out.println(maxGap(arr));
}
}
public static int maxGap(int[] arr){
if(arr == null || arr.length < 2)
return 0;
//统计数组最大和最小值
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
int len = arr.length;
for(int i = 0;i
min = Math.min(min,arr[i]);
max = Math.max(max,arr[i]);
}
//如果max=min,那么可以直接返回0
if(max == min)
return 0;
//建立桶,范围是0-len
//有三个属性,是否进来过数,最大值,最小值
boolean[] hasNum = new boolean[len+1];
int[] maxs = new int[len+1];
int[] mins = new int[len+1];
int bid = 0;
//将每一个数装入桶
for(int i = 0; i < len;i++){
//计算当前数应该在哪个桶
bid = bucket(arr[i],len,min,max);
//统计桶的最大最小值,是否进去过数
maxs[bid] = hasNum[bid]?Math.max(maxs[bid],arr[i]):arr[i];
mins[bid] = hasNum[bid]?Math.min(mins[bid],arr[i]):arr[i];
hasNum[bid] = true;//当前桶进去了数
}
//开始计算差值
int res = 0;
int lastMax = maxs[0];
for(int i = 1;i < len;i++){
if(hasNum[i]){
res = Math.max(res,mins[i]-lastMax);
lastMax = maxs[i];
}
}
return res;
}
public static int bucket(long num,long len, long min, long max){
//目的,让值为min的在0桶,让值为max的在len桶
//范围变换,从min-max变化为0-len
int res = (int)((num-min)*len/(max-min));
return res;
}
}

队列和栈

用数组实现栈和队列

用数组实现栈比较简单,此类需要持有一个数组和当前位置标记,有初始化构造函数,获取最上面一个数peek方法,入栈push方法,出栈pop方法,要注意的是执行者三个方法时第一步为判断index范围,pop方法返回类型要是Integer而不是int,因为换成int不能返回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
public class ArrayStack {
//用数组实现栈,应该持有数组,当前位置
private int[] arr;
private int index;
public ArrayStack(int size){
if (size < 0)
throw new IllegalArgumentException("The init size is less than 0");
arr = new int[size];
}
//peek,获取最上面的数
//返回类型为Integer,不然不能返回NULL
public Integer peek(){
if (index == 0)
return null;
return arr[index-1];
}
//入栈
public void push(int obj){
//如果超出范围,报错
if (index == arr.length)
throw new ArrayIndexOutOfBoundsException("The stack is full");
//如果正常,放入数据
arr[index++] = obj;
}
//出栈
public Integer pop(){
if (index == 0)
throw new ArrayIndexOutOfBoundsException("The stack is empty");
return arr[--index];
}
}

用数组实现队列,需要持有一个数组,一个入队列位置end,一个出队列位置start,一个队列大小size,用size来给end和start解耦

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
public class ArrayQueue {
//用数组实现队列,需要持有数组,队列末尾,队列开始,size
private int[] arr;
private int start;
private int end;
private int size;
public ArrayQueue(int initSize){
if (initSize < 0)
throw new IllegalArgumentException("The init size is less than 0");
arr = new int[initSize];
size = 0;
end = 0;
start = 0;
}
//peek方法
public Integer peek(){
if (size == 0)
return null;
return arr[start];
}
//push方法
public void push(int obj){
if (size == arr.length)
throw new ArrayIndexOutOfBoundsException("The queue is full");
size++;
arr[end] = obj;
//循环队列
end = (end + 1) % arr.length;
}
//poll方法
public int poll(){
if (size == 0)
throw new ArrayIndexOutOfBoundsException("The queue is empty");
size--;
int temp = arr[start];
start = (start+1)%arr.length;
return temp;
}
}

getMin()功能的栈

实现一个特殊功能的栈,在实现栈的基本功能的基础上,再实现返回栈中最小元素的操作。

思路:持有两个栈,一个为数据栈,一个为最小栈,数据栈正常进出,而最小栈有两种方法实现。

方法一:当进来的数比最小数要小,直接入栈;当比最小数大,入栈最小数出栈时。两个栈正常出栈,返回data栈的值。

方法二:当进来的数比最小数要小,直接入栈;当比最小数大,min不入栈 。出栈时,当data出栈数等于最小数才出栈 ,其他时候min不出栈。

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
import java.util.Stack;
public class MyStack2 {
//第二种方法,入栈的时候,如果当前数比较大,则min栈不进
//出栈的时候,如果当前数比min栈小,min栈不出
private Stack dataStack;
private Stack minStack;
//初始化
public MyStack2() {
dataStack = new Stack<>();
minStack = new Stack<>();
}
//getMin()方法
public Integer getMin() {
//如果栈为空,返回空
if (minStack.empty())
return null;
return minStack.peek();
}
//入栈方法
public void push(int num) {
if (minStack.empty()) {
minStack.push(num);
} else if (num <= getMin()) {
minStack.push(num);
}
dataStack.push(num);
}
//出栈方法
public Integer pop() {
if (dataStack.empty())
return null;
int num = dataStack.pop();
//只有当前数等于getmin,min栈才出
//只有等于,没有小于
if (num == getMin())
minStack.pop();
return num;
}
}

用队列实现栈

用队列来实现栈的功能,要有2个队列,当push的时候,正常进入队列,当peek的时候,先弹出其他的进入辅助队列,然后获取剩下来的一个的值,再将其放入辅助队列,再交换两个的引用。

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
import java.util.LinkedList;
import java.util.Queue;
public class TwoQueueStack {
//用两个队列实现栈
private Queue queue;
private Queue help;
public TwoQueueStack(){
queue = new LinkedList();
help = new LinkedList();
}
//push
public void push(int num){
queue.add(num);
}
public int peek(){
if (queue.isEmpty()){
throw new RuntimeException("Queue is empty");
}
while (queue.size()!=1){
help.add(queue.poll());
}
int res = queue.poll();
help.add(res);
swap();
return res;
}
public int pop(){
if (queue.isEmpty()){
throw new RuntimeException("Queue is empty");
}
while (queue.size()>1){
help.add(queue.poll());
}
int res = queue.poll();
swap();
return res;
}
//交换两个队列引用
public void swap(){
Queue temp = queue;
queue = help;
help = queue;
}
}

用栈实现队列

准备两个栈,一个push栈只用于push数据进去,一个pop栈只用于pop数据出来。从push栈倒数据进pop栈有两个要求,一个是pop栈中不能有数据,另一个是倒就要全部倒完。

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 TwoStackQueue {
private Stack stackPush;
private Stack stackPop;
public TwoStackQueue(){
stackPush = new Stack<>();
stackPop = new Stack<>();
}
public void push(int num){
stackPush.push(num);
goTo();
}
//关键的倒数据步骤
public void goTo(){
while (stackPop.isEmpty()){
//要倒完
while (!stackPush.isEmpty()){
stackPop.push(stackPush.pop());
}
}
}
//peek步骤
public int peek(){
if (stackPush.isEmpty() && stackPop.isEmpty())
throw new RuntimeException("The stack is empty");
goTo();
return stackPop.peek();
}
public int pop(){
if (stackPush.isEmpty() && stackPop.isEmpty())
throw new RuntimeException("The stack is empty");
goTo();
return stackPop.pop();
}
}

猫狗队列

实现一种猫狗队列的结构,要求如下:

  1. 用户可以调用 add 方法将 cat 或者 dog 放入队列中

  2. 用户可以调用 pollAll 方法将队列中的 cat 和 dog 按照进队列的先后顺序依次弹出

  3. 用户可以调用 pollDog 方法将队列中的 dog 按照进队列的先后顺序依次弹出

  4. 用户可以调用 pollCat 方法将队列中的 cat 按照进队列的先后顺序依次弹出

  5. 用户可以调用 isEmpty 方法检查队列中是否还有 dog 或 cat

  6. 用户可以调用 isDogEmpty 方法检查队列中是否还有 dog

  7. 用户可以调用 isCatEmpty 方法检查队列中是否还有 cat

思路:将Pet封装上一个index数据项,这样使用一个新类将二者封装。然后持有一个狗队列,一个猫队列,持有index,初始为0,在add操作中,如果是狗,就加入狗队列,是猫就加入猫队列。从队列中弹出较早入队列的,比较猫、狗队列中较小的index,弹出即可。

坑点:牛客网上的题目相比于原始题目,Pet自己也需要封装上一个index项,而比较的时候,比的是封装后的宠物队列类自己定义的index,而不是pet的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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
class Pet {
private String type;
private int x;
public Pet(String type,int index){
this.type = type;
this.x = index;
}
public String getPetType(){
return this.type;
}
public int getPetIndex(){
return this.x;
}
}
class PetEnterQueue {
//持有Pet和index
private Pet pet;
private long index;
public PetEnterQueue(Pet pet, long index){
this.pet = pet;
this.index = index;
}
//API:获取当前类型,index
public Pet getPet() {
return pet;
}
public long getIndex() {
return index;
}
public String getPetType(){
return pet.getPetType();
}
}
class DogCatQueue {
//猫狗队列,一个持有猫,,一个持有狗
private Queue dogQueue;
private Queue catQueue;
private long index;
public DogCatQueue(){
dogQueue = new LinkedList<>();
catQueue = new LinkedList<>();
index = 0;
}
public void add(Pet pet){
//如果为狗,加到狗;如果为猫,加到猫
if ("dog".equals(pet.getPetType())){
dogQueue.add(new PetEnterQueue(pet,index++));
}else if ("cat".equals(pet.getPetType())){
catQueue.add(new PetEnterQueue(pet,index++));
}else{
throw new RuntimeException("error, no dog or cat");
}
}
//弹出猫或者狗中较小的
public Pet pollAll(){
//如果两个均不为空
if (!dogQueue.isEmpty() && !catQueue.isEmpty()){
if (dogQueue.peek().getIndex() < catQueue.peek().getIndex()){
return dogQueue.poll().getPet();
}else {
return catQueue.poll().getPet();
}
}else if (!dogQueue.isEmpty()){
//狗不为空
return dogQueue.poll().getPet();
}else if (!catQueue.isEmpty()){
//猫不为空
return catQueue.poll().getPet();
}else{
throw new RuntimeException("The queue is empty");
}
}
//弹出狗队列
public Pet pollDog(){
if (dogQueue.isEmpty())
throw new RuntimeException("Dog queue is empty");
return dogQueue.poll().getPet();
}
//弹出猫队列
public Pet pollCat(){
if (catQueue.isEmpty())
throw new RuntimeException("Dog queue is empty");
return catQueue.poll().getPet();
}
public boolean isEmpty(){
return isCatEmpty()&&isDogEmpty();
}
public boolean isCatEmpty(){
return catQueue.isEmpty();
}
public boolean isDogEmpty(){
return dogQueue.isEmpty();
}
}

输入输出的处理比较麻烦,使用StringBuilder来添加结果,最后用substring方法将其转换为字符串,要记得添加换行。

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 static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String s = null;
DogCatQueue dq = new DogCatQueue();
Pet pet = null;
StringBuilder res=new StringBuilder();
s = br.readLine();
int count = Integer.valueOf(s);
for (int i = 0; i < count; i++) {
String[] strArr = br.readLine().split(" ");
switch (strArr[0]){
case "add":
String type=strArr[1];
dq.add(new Pet(type,Integer.valueOf(strArr[2])));
break;
case "pollAll":
while(!dq.isEmpty()){
pet = dq.pollAll();
res.append(pet.getPetType()+" "+pet.getPetIndex()+"\n");
}
break;
case "pollCat":
while(!dq.isCatEmpty()){
pet = dq.pollCat();
res.append(pet.getPetType()+" "+pet.getPetIndex()+"\n");
}
break;
case "pollDog":
while(!dq.isDogEmpty()){
pet = dq.pollDog();
res.append(pet.getPetType()+" "+pet.getPetIndex()+"\n");
}
break;
case "isDogEmpty" :
res.append(dq.isDogEmpty()?"yes\n":"no\n");
break;
case "isCatEmpty":
res.append(dq.isCatEmpty()?"yes\n":"no\n");
break;
case "isEmpty":
res.append(dq.isEmpty()?"yes\n":"no\n");
break;
}
}
System.out.println(res.substring(0,res.length()-1));
}

哈希表

RandomPool结构

题目:

设计一种结构,在该结构中加入如下三种功能

insert(key):将某个key加入到该结构,做到不重复加入

delete(key):将原本在结构中的某个key移除

getRandom():等概率随机返回结构中的任何一个key

要求:这三种方法的时间复杂度均为O(1)

思路:一个不行就两个。持有两个哈希表,一个为key-index映射,一个为index-key映射,麻烦的地方在于删除后如何保证之后的随机性,解决方法为每次找到要删除的位置,然后获取其key,获取最后行的key和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
import java.util.HashMap;
public class RandomPool<K> {
//持有两个哈希表,一个索引
private HashMap keyIndexMap;
private HashMap indexKeyMap;
private int index;
public RandomPool(){
keyIndexMap = new HashMap<>();
indexKeyMap = new HashMap<>();
index = 0;
}
//添加方法
public void insert(K key){
//如果已经存在了,返回
if (keyIndexMap.containsKey(key))
return;
keyIndexMap.put(key,index);
indexKeyMap.put(index++,key);
}
//随机返回方法
public K getRandom(){
//如果没有数据,返回空
if (index == 0)
return null;
int res = (int)(Math.random()*index);
return indexKeyMap.get(res);
}
//删除方法
public void delete(K key){
if (!keyIndexMap.containsKey(key))
return;
//1、找到要删除位置的index
int deleteIndex = keyIndexMap.get(key);
//2、找到最后的index和key
int lastIndex = --index;
K lastKey = indexKeyMap.get(lastIndex);
//3、将待删除位置的值替换为最后行的值
keyIndexMap.put(lastKey,deleteIndex);
indexKeyMap.put(deleteIndex,lastKey);
//4、删除最后行数据,将行数-1
keyIndexMap.remove(key);
indexKeyMap.remove(lastIndex);
}
}

一致性哈希

解决问题

应用在多服务器的场合,为了解决负载均衡问题,即客户端的请求能较均衡的分配到每台服务器上。经典的服务器结构是若有n台机器,将前端获取信息计算一个哈希值,然后%n,得到对应服务器并访问,但当服务器个数变化时信息需要重新计算才能再次均衡。

如何做

而一致性哈希就是为了解决这个问题,将哈希域映射到一个圆环,将服务器信息计算出哈希值映射到圆上,客户端请求信息的key同样计算哈希值映射到圆上,将信息交给顺时针找到的第一台服务器进行处理。当减少一台服务器,将要访问此机器的数据交由顺时针的下一个机器即可。当增加一台服务器,将其映射到圆上,将此机器逆时针上一台机器到此机器之间的顺序交给本机器处理即可。

存在问题

但是这样当服务器个数较少的时候,不容易在环上均匀分配,这样无法实现负载均衡。可以采用虚拟节点的方式,给每台机器分配多个虚拟节点,让虚拟节点在环上均衡分配,数据的key寻找对应的虚拟节点,再由虚拟节点寻找对应的机器。

负载均衡算法

1、轮训法

将请求按顺序轮流分配到后端服务器上,均衡对待每一台服务器,而不关心服务器本身的连接数和负载情况

2、随机法

通过系统随机计算,从后端服务器中随机选取一台来访问,当访问次数多了后,根据概率论,每台服务器上访问会均衡

3、源地址哈希

根据客户端的ip,计算哈希值,对服务器台数进行取模运算,得到的便是要访问的服务器。

4、加权轮训法

不同服务器抗压能力不同,给配置高、负载低的服务器更高的权重,处理更多的请求;而配置低、负载高的服务器更低权重,处理更少请求,降低负载

5、加权随机法

根据负载不同计算不同权重,按照权重随机请求服务器,而不是顺序的

6、最小连接数法

根据后端服务器当前连接情况,动态的选取当前积压连接数最少的服务器来处理当前请求,尽可能提高后端的利用率。

7、一致性哈希

在源地址哈希上改进,将哈希域映射为环,将计算器映射到环上,计算请求的key对应的哈希值,在顺时针找到的第一台服务器上进行请求。增加虚拟节点,来解决初始服务器数量少,负载不均衡的问题。

常见的hash算法

1、直接寻址法

取key的某个线性函数值作为散列地址,其哈希值为a*key+b

2、数字分析法

找出数字的规律,利用数据来构造冲突几率较低的散列地址

3、平方取中法

取key平方后的中间几位作为散列地址

4、折叠法

将key分割为位数相同的几部分,最后一部分位数可以不同,取这几份去除进位的叠加和来作为散列地址

5、随机数法

选择随机函数,取key的随机值作为散列地址,通常用在key长度不同的场合

6、除留余数法

将key对不大于散列表长的数p取余,得到的数为地址。不仅可以直接取模,也可以在折叠,平方取中后取模,一般将p取素数或散列表长

数组

转圈打印矩阵

思路:打印矩阵的思路一般都是宏观调度,用有限个变量去约束要打印的范围,然后循环调用打印函数。此处用左上角和右下角的点去约束范围,调用打印函数,然后将左上角和右下角的点进行收缩。打印矩阵函数为分别判断一行,一列和多行多列的情况,如果为单行单列的,用for循环控制行和列,如果为多行多列,分四次打印。

说明的是,在原方法中采用的是直接sout输出,此处使用StringBuilder进行添加。

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
62
63
64
65
66
67
68
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

class PringtEdge {
//转圈打印矩阵
public StringBuilder printCircle(int[][] arr){
//1、找两个边界点
int row1 = 0, col1 = 0;
int row2 = arr.length-1,col2 = arr[0].length-1;
StringBuilder sb = new StringBuilder();
//只要满足边界条件,循环调用打印函数
while (row1 <= row2 && col1 <= col2){
printEdge(arr,sb,row1++,col1++,row2--,col2--);
}
return sb;
}
public void printEdge(int[][] arr, StringBuilder sb, int row1, int col1, int row2, int col2){
//1、如果只有一行
if (row1 == row2){
for (int i = col1; i <= col2; i++) {
sb.append(arr[row1][i]+" ");
}
}else if(col1 == col2){
//2、如果只有一列
for (int i = row1; i <= row2; i++) {
sb.append(arr[i][col1]+" ");
}
}else{
//3、分四部分打印
int curR = row1;//当前行
int curL = col1;//当前列
while (curL < col2){
sb.append(arr[curR][curL++]+" ");
}
while (curR < row2){
sb.append(arr[curR++][curL]+" ");
}
while (curL > col1){
sb.append(arr[curR][curL--]+" ");
}
while (curR > row1){
sb.append(arr[curR--][curL]+" ");
}
}
}
}
public class Main{
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
//要持有的写的类
PringtEdge pe = new PringtEdge();
String[] s = null;
StringBuilder res = new StringBuilder();
s = br.readLine().split(" ");
int row = Integer.valueOf(s[0]);
int col = Integer.valueOf(s[1]);
int[][] arr = new int[row][col];
for (int i = 0; i < row; i++) {
s = br.readLine().split(" ");
for (int j = 0; j < col; j++) {
arr[i][j] = Integer.valueOf(s[j]);
}
}
res = pe.printCircle(arr);
System.out.println(res.substring(0,res.length()-1));
}
}

之字型打印矩阵

思路:宏观调度,打印的是两个点之间的对角线的值,那么只要解决点的运动路径和打印对角线即可。要注意的是点在运动过程中时,如果变量A依靠变量B来约束其行为,要先改变变量A,再改变B,不然B先改变则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
//之字型打印矩阵,找宏观规律
public StringBuilder printZhi(int[][] m){
//往右,再下
int row1 = 0,col1 = 0;
//往下,再右
int row2 = 0,col2 = 0;
int endR = m.length-1;
int endC = m[0].length-1;
StringBuilder res = new StringBuilder();
//先下,再上
boolean fromUp = false;
while (row1 <= endR){
printProcess(m,res,row1,col1,row2,col2,fromUp);
//路径变化
//关键为将引发判断的值后判断
row1 = col1 == endC ? row1+1:row1;
col1 = col1 == endC ? col1:col1+1;
col2 = row2 == endR ? col2+1 : col2;
row2 = row2 == endR ? row2:row2+1;
fromUp = !fromUp;
}
return res;
}
public void printProcess(int[][] m,StringBuilder res, int row1,int col1, int row2, int col2,boolean fromUp){
//如果fromUp为false,从左下向右上打印
if (!fromUp){
while (row1 <= row2){
res.append(m[row2--][col2++]+" ");
}
}else{
while (row1 <= row2){
res.append(m[row1++][col1--]+" ");
}
}
}

在行列都排好序的矩阵中找数

思路:从右上角或者左下角开始找,此处选右上角,要是当前数小了就往下,当前数大了就往左。

精髓的点在于开始寻找的位置,利用矩阵的特征,可以将部分不可能的情况给排除掉,这题从右上角或者左下角开始找都可以。但是左上和右下不行,因为没办法缩小规模。核心在于右上或左下,一边比它大,一边比它小。这样子存在要么去一边,要么另一边的0 1情况,即非黑即白。

实质是将没有可能的数据直接舍弃,即将问题的规模进行缩小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//在已排序的矩阵中找到数
public boolean findNum(int[][] m,int k){
//选择右上角的数
int row = 0,col = m[0].length-1;
int endR = m.length-1;
while (row<=endR && col >=0){
//如果当前数小,往下走
if (m[row][col] == k){
return true;
}else if(m[row][col]>k){
//当前数比较大,往左走
col--;
}else {
row++;
}
}
return false;
}

链表

打印两个链表的公共部分

给定两个升序链表,打印两个升序链表的公共部分。

思路:类似于外排,谁小动谁,如果相等,打印并两个一起动

值得注意的点:将head1或者2为空的情况放在最上面,直接返回;将head1.value==head2.value的情况放在三种判断的第一个,这样可以缩短判断时间;整理链表基本结构及生成链表的套路。StringBuilder添加时候,使用两次append比一次append两个str效率高。原因是使用+的时候多了生成String的步骤,这样降低了效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//打印两个链表公共部分
public static void printCommonPart(Node head1,Node head2){
//将不可能的情况先列出
if (head1 == null || head2 == null)
return;
StringBuilder res = new StringBuilder();
//类似外排
//谁小动谁,如果相等,添加并两个一起动
while (head1 != null && head2 != null) {
//将相等的判断放在最上面
if (head1.value == head2.value) {
//这样比直接append(value+" ")效率高
res.append(head1.value).append(" ");
head1 = head1.next;
head2 = head2.next;
} else if(head1.value < head2.value){
head1 = head1.next;
}else{
head2 = head2.next;
}
}
System.out.println(res.toString());
}

合并两个有序的链表

给定两个升序的单链表的头节点 head1 和 head2,请合并两个升序链表, 合并后的链表依然升序,并返回合并后链表的头节点。

思路:类似于外排,如果两个均不为空,谁小动谁;如果哪个为空,把另一个不为空的接到新链表后即可。

小技巧:新链表自己先做一个头节点会比较方便,在动链表的时候,流程为让新链表的下一个指向小节点,新链表当前节点跳到下一个,小节点链表头节点跳到下一个。另外如果有一个链表已经为空了,则不需要一个个节点去添加,直接把整个加上去即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//合并链表
public static Node mergeLink(Node head1, Node head2){
//类似于外排
Node res = new Node(0);
Node cur = res;
while (head1 != null && head2 != null){
if (head1.value <= head2.value){
cur.next = head1;
cur = head1;
head1 = head1.next;
}else {
cur.next = head2;
cur = head2;
head2 = head2.next;
}
}
//将空的链表部分直接拼接到新链表后面即可
cur.next = head1 == null ? head2 : head1;
cur = res.next;
res = null;
return cur;
}

判断链表是否为回文结构

给定链表的头结点,判断是否为回文链表

方式一:将链表元素全部存进栈中,利用栈先进后出的特点,与链表逐个比较,额外空间复杂度O(N)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//解法1:将链表中的数据用栈存起来,然后一个个取出来进行对比
public boolean isPalindrome1(Node head){
if (head == null || head.next == null)
return true;
//准备一个栈
Stack stack = new Stack<>();
Node node = head;
while(node != null){
stack.push(node);
node = node.next;
}
//取出来比较
node = head;
while (!stack.isEmpty()){
if (node.value != stack.pop().value)
return false;
node = node.next;
}
return true;
}

方法二:只放一半的元素进栈中,缩短一半的额外空间。

为了找到链表中点,用到的方法为快慢指针,是链表题目中比较常见的方法,慢指针一下子走一步,快指针走两步,当快指针走到末尾时,慢指针正好走到中间。注意:如果是要中点位置,慢指针从头结点开始,如果是中点位置下一个,慢指针从头节点下一个开始。此处慢指针的起始点就是头节点下一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//解法2:只需要判断链表的一半即可,问题是如何找到链表的中点
//用快慢指针
public boolean isPalindrome2(Node head){
if (head == null || head.next == null)
return true;
Node slow = head.next;
Node fast = head;
while (fast.next != null && fast.next.next != null){
slow = slow.next;
fast = fast.next;
}
//此时slow来到中点位置,将剩下的节点存入栈中
Stack stack = new Stack<>();
while (slow != null){
stack.push(slow);
}
slow = head;
while (!stack.isEmpty()){
if (slow.value != stack.pop().value)
return false;
slow = slow.next;
}
return true;
}

方法三:进阶方式,额外空间复杂度为O(1),不依靠栈,而是先找到链表中点,将链表右半部分进行翻转,这里有个操作是将中点节点指向null,方便后面判断。然后从链表两端进行判断,注意不能直接返回值,因为还需要将链表还原。判断结束后,将链表右半部分还原。

注意:反转链表,删除链表节点需要利用3个指针,一个指向当前节点,一个指向当前节点的前序节点,一个指向当前节点的后序节点。先保存其下一个进行位置的,再进行操作。

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
//解法3,不利用栈,先找到链表的中点,反转剩下的链表
//进行判断,然后将链表还原
public boolean isPalindrome3(Node head){
if (head == null || head.next == null)
return true;
//找到链表中点
Node n1 = head;
Node n2 = head;
while(n2.next != null && n2.next.next != null){
n1 = n1.next;
n2 = n2.next.next;
}
//此时n1来到中点,反转链表,先要记录n1下一个位置
n2 = n1.next;
n1.next = null;
Node n3 = null;
//反转链表
while (n2 != null){
n3 = n2.next;
n2.next = n1;
n1 = n2;
n2 = n3;
}
//此时n1为链表末尾,进行记录
n3 = n1;
//开始判断头和尾的值是否相等
n2 = head;
boolean res = true;
while (n2 != null && n1 != null){
if (n2.value != n1.value){
//不能直接返回假,这样链表没有被调整回来
res = false;
break;
}
n2 = n2.next;
n1 = n1.next;
}
//还原链表
n2 = n3.next;
n3.next = null;
while (n2 != null){
n1 = n2.next;
n2.next = n3;
n3 = n2;
n2 = n1;
}
return res;
}

单向链表的基础partition

题目描述:

给定一个链表,再给定一个整数 pivot,请将链表调整为左部分都是值小于 pivot 的节点,中间部分都是值等于 pivot 的节点, 右边部分都是大于 pivot 的节点。

除此之外,对调整后的节点顺序没有更多要求。

思路:将链表用数组存起来,然后对数组partition,再将数组组合成链表,返回arr[0],要注意的是将数组的最后个元素的next指向null,以及partition过程的约束条件是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
//单链表的partition基础问题,利用数组来实现
public static Node listPartition(Node head,int pivot){
if (head == null || head.next == null)
return head;
//1、统计链表长度
int len = 0;
Node node = head;
while(node != null){
len++;
node = node.next;
}
//2、造数组,将链表中元素填充进去
Node[] arr = new Node[len];
node = head;
int i = 0;
while (node != null){
arr[i++] = node;
node = node.next;
}
//3、数组的partition问题
//将其封装为函数
partitionArr(arr,pivot);
//将数组组合成链表
for (i = 1; i < len; i++) {
arr[i-1].next = arr[i];
}
//将最后个节点指针指向null
arr[i-1].next = null;
return arr[0];
}
public static void partitionArr(Node[] arr, int pivot){
int less = -1;
int more = arr.length;
int i = 0;
//约束条件为i < more或者i != more
//如果是等于,当more为数组最后,会越界
while (i < more){
if (arr[i].value < pivot){
swap(arr,i++,++less);
}else if (arr[i].value > pivot){
swap(arr,i,--more);
}else {
i++;
}
}
}

单链表的partition进阶

要求:在原来基础之上,partition后每个部分的节点从左至右的顺序和原链表中节点的顺序一致。要时间复杂度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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//链表的复杂partition
public static Node listPartition2(Node head,int pivot){
if (head == null || head.next == null)
return head;
//分三个部分,小于、等于、大于区,来个节点就丢到对应的地方去
//最后将三个部分合并
Node sH = null;
Node sT = null;
Node eH = null;
Node eT = null;
Node bH = null;
Node bT = null;
Node next = null;
while(head != null){
next = head.next;
head.next = null;
//将当前节点丢到三个部分中
if (head.value < pivot){
//如果为空
if (sH == null){
sH = head;
sT = head;
}else{
sT.next = head;
sT = head;
}
}else if(head.value == pivot){
if (eH == null){
eH = head;
eT = head;
}else{
eT.next = head;
eT = head;
}
}else{
if (bH == null){
bH = head;
bT = head;
}else{
bT.next = head;
bT = head;
}
}
head = next;
}
//将三个链表连接
if (sT != null){
sT.next = eH;
eT = eT == null ? sT:eT;
}
if (eT != null){
eT.next = bH;
}
return sH != null ? sH : eH != null ? eH : bH;
}

复制含有随机节点的链表

方法一:用哈希表,将key为原链表节点,value为新链表节点,然后再复制下一个和rand指针关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static Node copyListWithRand1(Node head){
//创建存储用的HashMap,key与value均为Node
HashMap map = new HashMap();
Node cur = head;
//对每个结点进行拷贝
while(cur != null){
map.put(cur, new Node(cur.value));
cur = cur.next;
}
cur = head;
while (cur != null){
//将复制节点的next与random指向cur相同部分
//get(x)为得到x节点的拷贝结点x'
map.get(cur).next = map.get(cur.next);//1' 指向 2(1.next)的对应节点2'
map.get(cur).random = map.get(cur.random);
cur = cur.next;
}
return map.get(head);
}

方法二:不使用其他数据结构,在原来链表中每个节点后面复制一个新节点,然后复制rand指针关系,再将两个链表拆分开来

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
public static Node copyListWithRand2(Node head){
if(head == null)
return null;
Node cur = head;
Node next = null;
// copy node and link to every node
while(cur != null){
next = cur.next;
cur.next = new Node(cur.value);
cur.next.next = next;
cur = next;
}
cur = head;
Node curCopy = null;
// set copy node rand
while (cur != null){
next = cur.next.next;
curCopy = cur.next;
curCopy.random = cur.random != null ? cur.random.next : null;
cur = next;
}
Node res = head.next;//记录下复制链表的头结点
cur = head;
// split
while(cur != null){
next = cur.next.next;
curCopy = cur.next;
cur.next = next;
curCopy.next = next != null ? next.next : null;
cur = next;
}
return res;
}

反转链表

非递归实现

反转单链表和双链表,基本思路是持有三个节点,当前节点的前序节点,当前节点,下一个节点。

对于单链表,如果当前节点不为空,保存其下一个节点next,当前节点指向前序节点pre,pre=当前节点,当前节点=next,这样便实现了反转;对于双向链表,只是多了一步让当前节点的前序节点=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
//反转单向链表
public static Node reverseListSL(Node head){
//准备三个节点
Node pre = null;
Node cur = head;
Node next = null;
while (cur != null){
next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
//反转双向链表
public static Node reverseListDL(Node head){
//准备三个节点
Node pre = null;
Node cur = head;
Node next = null;
while (cur != null){
next = cur.next;
cur.next = pre;
cur.pre = next;
pre = cur;
cur = next;
}
return pre;
}

递归实现

看其是否可以分解成具有相同解决思路的子问题。反转链表1->2->3->4,如果把1以后的链表都反转好了,让2指向1,1指向空即可。对于1后面的子链表,也可以这样去处理。因此可以使用递归去解决

  1. 定义递归函数,明确函数功能及返回值

    此递归函数实现的功能是反转某个节点开始的链表,返回的是反转后的新的结点

  2. 寻找递归公式

    • 先反转当前节点以后的链表,这样1->2->3->4变为1->2<-3<-4
    • 将当前node(1)的下一个节点(node.next)的指向(node.next.next)改为当前节点(node),node的后继结点变为空
    • 返回新的头结点
  3. 将递推公式带入定义好的递归函数中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//反转链表的递归实现
//递归函数要实现的功能是反转链表,返回的是反转后的链表头节点
public static Node reverseList2(Node node){
//base case
if (node.next == null)
return node;
//对当前节点做操作
//把后面的链表进行反转
Node newHead = reverseList2(node.next);
//把后面的反转好后,让当前node的下一个节点的下一个指向当前节点
//让当前节点的下一个指向空
node.next.next = node;
node.next = null;
//返回新的头节点
return newHead;
}

反转部分单向链表

给定一个单链表,在链表中把第 L 个节点到第 R 个节点这一部分进行反转。

思路:找到from-1,from,to,to+1处的四个链表,如果from或者to为空,直接返回,反转from-to之间的链表,然后让from-1指向to,from指向to+1。如果from-1为空,那么to变为新链表的头结点。

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
//反转from-to之间的链表
public static Node reversePartList(Node head, int fromIndex, int toIndex){
if (head == null || head.next == null)
return head;
//找到from-1,fro,to,to+1处的链表节点
Node fromPre = null;
Node from = null;
Node to = null;
Node toPos = null;
Node cur = head;
int count = 0;
//如果from和to的距离超过了链表长度,就不进行操作
while (cur != null){
count++;
if (count == fromIndex - 1){
fromPre = cur;
}else if (count == fromIndex){
from = cur;
}else if (count == toIndex){
to = cur;
}else if (count == toIndex + 1){
toPos = cur;
}
cur = cur.next;
}
if (from == null || to == null)
return head;
//反转from-to之间的结点
Node pre = null;
cur = from;
Node next = null;
while (cur != toPos){
next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
//如果from为头结点,新的头结点为to
if (fromPre == null){
head = to;
}else {
fromPre.next = to;
}
from.next = toPos;
return head;
}

两链表相交问题(五星级)

在本题中,单链表可能有环,也可能无环。给定两个链表的头节点,实现函数,如果两链表相交,返回相交的第一个节点;如果不相交,返回null即可。

要求:如果链表1的长度为N,链表2的长度为M,时间复杂度达到O(N+M),额外空间复杂度达到O(1)。

思路:遇到复杂问题进行拆解,首先需要判断两个链表是否有环,如果两个都没有环,那就是两个无环链表的相交问题;如果两个都有环,那就是两个有环链表的相交问题;如果一个有环,一个无环,是没有这种结构的。因此将问题差结成了三个,首先需要求解链表的入环节点。

判断链表是否有环有两种方法,一种是利用哈希表,如果当前节点在哈希表中,返回当前节点,否则加入哈希表,并遍历到下一个节点,此方法额外空间复杂度不为O(1)。

1
2
3
4
5
6
7
8
9
10
11
public static Node getFirstLoopNode(Node head){
HashSet set = new HashSet();
while(head != null){
if(set.contains(head)){
return head;
}
set.add(head);
head = head.next;
}
return null;
}

第二种方法是利用快慢指针,如果快指针没有遇到慢指针,快指针走2步,慢指针走1步,如果快指针后面为空,返回空。当快慢指针相遇,让快指针从链表头开始,和慢指针一起每次走一步,二者相遇的地方即为链表的入环节点。

假设起始点到入环处的长度为a,环的长度为L,当快指针与慢指针相遇时,设慢指针所走的路程为b,当慢指针入环时,快指针已经在环上了,设快指针距离入环点距离为c,这时候当慢指针继续走c步时,快指针就会赶上慢指针了,c<=L,此时慢指针还没有走一圈。

当快慢指针相遇时 ,两个指针走的距离为

p慢=a+b=n

p快=a+b+k*L=2n(快指针比慢指针多走k圈)

因此有n=a+b=k*L。此时若让快指针从头节点重新走a步,a=k*L-b,对于慢指针,因为其已经走了b步,此时再走a步,正好在环上走了k圈(a+b=k*L),因此两个指针会在入环点相遇。

判断条件是快慢指针没有相遇,而一旦快指针后面为null,返回即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static Node getLoopNode(Node head){
if(head == null || head.next == null || head.next.next == null)
return null;
Node slow = head.next;
Node fast = head.next.next;
while(slow != fast){
if(fast.next == null || fast.next.next == null)
return null;
fast = fast.next.next;
slow = slow.next;
}
fast = head;
while(slow != fast){
slow = slow.next;
fast = fast.next;
}
return slow;
}

这样便可以判断出链表是否有环。对于两个都没有环的链表,如果他们相交,则最后个节点一定相等,让更长的链表先将多的部分走完,两个链表再一起走,便会在第一个相交的地方相遇。

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 static Node noLoop(Node head1, Node head2){
if(head1 == null || head2 == null)
return null;
Node cur1 = head1;
Node cur2 = head2;
int n = 0;
//找到最后个节点并求两条链表长度差
while (cur1.next != null){
n++;
cur1 = cur1.next;
}
while (cur2.next != null){
n--;
cur2 = cur2.next;
}
//最后节点不相同,一定不相交
if(cur1 != cur2)
return null;
//将cur1指向更长的链表
cur1 = n > 0 ? head1 : head2;
cur2 = cur1 == head1 ? head2 : head1;
n = Math.abs(n);//得到较长链表需要多走的步数
while (n != 0){
n--;
cur1 = cur1.next;
}
//共同走到相同的节点
while (cur1 != cur2){
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur1;
}

而对于两个都有环的链表,有如下的三种结构。

其中如果两个链表的入环节点为同一个,则是第二种结构,可以简化为无环链表的相交问题;如果入环节点不为同一个,则让一个节点从环上走一圈,如果没有碰到另一个链表的入环节点,则说明是第一种,否则返回其中一个链表的入环节点即可。

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
//两个链表都有环的情况
public static Node bothLoop(Node head1, Node loop1, Node head2, Node loop2){
//有3种情况
Node cur1 = null;
Node cur2 = null;
//演变为无环链表的相交问题
if(loop1 == loop2){
cur1 = head1;
cur2 = head2;
int n = 0;
while(cur1 != loop1){
n++;
cur1 = cur1.next;
}
while(cur2 != loop2){
n--;
cur2 = cur2.next;
}
cur1 = n > 0 ? head1 : head2;
cur2 = cur1 == head1 ? head2 : head1;
n = Math.abs(n);
while(n != 0){
n--;
cur1 = cur1.next;
}
while(cur1 != cur2){
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur1;
}else{
//遍历loop1到自己,看是否遇到loop2
cur1 = loop1.next;
while(cur1 != loop1){
if(cur1 == loop2){
return loop1;
}
cur1 = cur1.next;
}
return null;
}
}

剩下的主方法就比较简单,求两个链表的入环节点,然后对无环链表相交,有环链表相交和一个有环一个无环进行判断即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
//主方法
public static Node getIntersectNode(Node head1, Node head2){
if(head1 == null || head2 == null){
return null;
}
Node loop1 = getLoopNode(head1);
Node loop2 = getLoopNode(head2);
if(loop1 == null && loop2 == null)
return noLoop(head1, head2);
if(loop1 != null && loop2 != null)
return bothLoop(head1,loop1,head2,loop2);
return null;
}

堆本质是一个二叉树 ,在Java中实现为优先级队列(PriorityQueue),默认为小根堆,即最小的在最上面。可以通过在新建堆时传入比较器对象来定义大根堆或者小根堆。其中如果比较器只用到一次,没有必要去专门定义一个类,可以用匿名内部类,实现compare()方法即可,更简单的是用lambda表达式,更简洁。

堆的结构很好用,在取中位数,第k大or第k小的数,前k大or前k小,贪心中经常用到。

随时找到数据流的中位数

题目描述:有一个源源不断的吐出整数的数据流,假设你有足够的空间来保存吐出的数。请设计一个名叫MedianHolder的结构,MedianHolder可以随时取得之前吐出所有数的中位数。

[要求]

  1. 如果MedianHolder已经保存了吐出的N个数,那么将一个新数加入到MedianHolder的过程,其时间复杂度是O(logN)。

  2. 取得已经吐出的N个数整体的中位数的过程,时间复杂度为O(1)

思路:持有两个堆,一个大根堆,一个小根堆,大根堆中存放较小的一半数,小根堆中存放较大的一半数,这样中位数就被大根堆和小根堆夹着。如果两个堆大小相等,则取平均;不然就返回较多的那个堆的堆顶元素。

其中比较关键的是将数加入堆的操作,如果大根堆为空或者当前数比大根堆顶的数小,则直接放入大根堆中,否则放入小根堆。然后进行堆的调整:哪个堆中的数据比另一个堆中数据多了2个,则拿一个到另一个堆中。这样将放数和调整堆进行了解耦,可以让代码变得更简洁。

其中牛客网要求格式化输出数据,可以用DecimalFormat类,进行一位小数的输出。

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
62
63
64
65
66
67
68
69
public class MedianHolder {
//随时找到数据流的中位数,需要持有两个堆
//大根堆
private PriorityQueue maxHeap = null;
//小根堆
private PriorityQueue minHeap = null;
// 取出所有整数部分和一位小数,格式化输出
DecimalFormat df = new DecimalFormat("#.0");
//构造方法,初始化
public MedianHolder(){
maxHeap = new PriorityQueue((o1,o2)->o2-o1);
minHeap = new PriorityQueue((o1,o2)->o1-o2);
}
//将数添加进堆中
public void addNum(int num){
//如果大根堆为空或者数比大根堆顶的数小,添加到大根堆
if (maxHeap.isEmpty() || num <= maxHeap.peek()){
maxHeap.add(num);
}else{
minHeap.add(num);
}
//堆的调整
modifyTwoHeap();
}
//调整堆
private void modifyTwoHeap() {
//如果大根堆比小根堆多两个,放一个进小根堆
if (maxHeap.size() == minHeap.size() + 2){
minHeap.add(maxHeap.poll());
}
if (minHeap.size() == maxHeap.size() + 2){
maxHeap.add(minHeap.poll());
}
}
//查找中位数
public void getMedian(){
//如果两个为空,返回空
if (maxHeap.size() == 0){
System.out.println(-1);
return;
}
//如果两个堆中数相等,取两个堆顶平均
if (minHeap.size() == maxHeap.size()){
System.out.println(df.format((maxHeap.peek()+minHeap.peek())/2.0));
}else {
//哪个多返回哪个的
System.out.println(df.format(maxHeap.size() > minHeap.size() ? maxHeap.peek() : minHeap.peek()));
}
}

public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String s = null;
StringBuilder res = new StringBuilder();
s = br.readLine();
int count = Integer.valueOf(s);
//要持有的写的类
MedianHolder m = new MedianHolder();
for (int i = 0; i < count; i++) {
//读一行数据
String[] str = br.readLine().split(" ");
if ("2".equals(str[0])){
m.getMedian();
}else {
m.addNum(Integer.valueOf(str[1]));
}
}
}
}

切金条

题目描述:给定一个正数数组arr,arr的累加和代表金条的总长度,arr的每个数代表金条要分成的长度。规定长度为k的金条分成两块,费用为k个铜板。返回把金条分出arr中的每个数字需要的最小代价。

要求:时间复杂度为O(n log n),空间复杂度为O(n)

题目说明:

1
2
3
4
如果先分成40和20两块,将花费60个铜板,再把长度为40的金条分成10和30两块,将花费40个铜板,总花费为100个铜板;
如果先分成10和50两块,将花费60个铜板,再把长度为50的金条分成20和30两块,将花费50个铜板,总花费为110个铜板;
如果先分成30和30两块,将花费60个铜板,再把其中一根长度为30的金条分成10和20两块,将花费30个铜板,总花费为90个铜板;
因此最低花费为90

思路:哈夫曼编码问题,即每次选出权重最小的数,新节点权重为二者相加,将新节点加入,再拿出两个,直到之后只有一个数,其值就是总的权重。

哈夫曼编码的应用:给出传递的电文,计算每个字母的权重,选出较小的两个节点构造成一个二叉树(小的在左,大的在右),新二叉树的权重为二者权重之和,然后将新二叉树放入,再重新拿出两个权重最小的。树构造好后,进行编码,左边为0,右边为1,只有叶子节点在存储了信息,这样从头节点找到叶子节点,便可以找到每个字母对应的编码。当给定了编码后,要如何去找到对应的字母呢?可以从字符串中取出字符,然后在树中进行寻找,如果找到了叶子节点,就得到了一个字母,再继续找下去。

做项目

题目描述:

输入:

参数1:正数数组costs

参数2:正数数组profits

参数3:正数k

参数4:正数m

参数说明:cost[i]表示i号项目的花费,profits[i]表示i号项目在扣除花费后还能挣到的钱(利润),k表示你不能并行、只能串行的最多做k个项目,m表示你初始的资金。

说明:你每做完一个项目,马上获得的收益,可以支持你去做下一个项目

输出:你最后获得的最大钱数。

分析:标准的贪心问题,选择项目的标准是:在花费比资金小的项目中,选择利润最多的,积累利润后,再选择花费比当前资金小的项目,直到做了k个项目。

思路:持有两个堆,一个小根堆,一个大根堆,小根堆依照项目花费排序,大根堆中依靠项目利润排序,从小根堆中弹出花费比当前资金少的项目进大根堆,然后从大根堆中弹出一个项目来做,累积资金,直到做了k个项目。

坑点:返回的收益需要是long类型,避免溢出!!!

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
//将项目的花费和利润封装为一个节点
class proNode{
public int c;
public int p;
public proNode(int c,int p){
this.c = c;
this.p = p;
}
}
public class FindMaximizedCapital {
//持有两个堆
private PriorityQueue minHeap;
private PriorityQueue maxHeap;
public FindMaximizedCapital(){
minHeap = new PriorityQueue((o1,o2)->o1.c-o2.c);
maxHeap = new PriorityQueue((o1,o2)->o2.p-o1.p);
}
//计算项目最大利润
public long findMaximizedCapital(int[] costs,int[] profits,int k,long m){
//将花费和利润封装放进小根堆
for (int i = 0; i < costs.length; i++) {
minHeap.add(new proNode(costs[i],profits[i]));
}
//进行k次循环做项目
for (int i = 0; i < k; i++) {
//知道小根堆不为空且堆顶项目花费比m少,弹进大根堆
while (!minHeap.isEmpty() && minHeap.peek().c <= m){
maxHeap.add(minHeap.poll());
}
//需要考虑到大根堆为空的情况,即虽然没有到k次,但没有项目可做
if (maxHeap.isEmpty())
return m;
//选出一个项目来做
m += maxHeap.poll().p;
}
return m;
}

public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String[] str = br.readLine().split(" ");
int n = Integer.valueOf(str[0]);
int w = Integer.valueOf(str[1]);
int k = Integer.valueOf(str[2]);
String[] strC = br.readLine().split(" ");
String[] strP = br.readLine().split(" ");
int[] costs = new int[n];
int[] profits = new int[n];
for(int i = 0;i
costs[i] = Integer.parseInt(strC[i]);
}
for(int i = 0;i
profits[i] = Integer.parseInt(strP[i]);
}
FindMaximizedCapital fm = new FindMaximizedCapital();
long win = fm.findMaximizedCapital(costs,profits,k,w);
System.out.println(win);
}
}

字符串拼接

给定一个字符串类型的数组strs,找到一种拼接方式,使得把所有字符串拼起来之后形成的字符串具有最低的字典序。

​ 字典序:每个字母相当于26进制的数,如果位数相同则比较字面值,长度不能的时候,把短的补到跟长的一样,补的内容相当于ASCII表中最小的内容,然后从最高位开始比较。

比较两个字符串,若str1+str2<= str2 + str1,则str1放前面,否则str2放前面。不要去证明贪心问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//比较器
public static class MyComparator implements Comparator<String>{
@Override
public int compare(String o1, String o2) {
//负数认为o1小,谁作为前缀小谁放前面
return (o1 + o2).compareTo(o2 + o1);
}
}
//用比较器来排序
public static String lowestString(String[] strs){
if (strs == null || strs.length == 0)
return "";
Arrays.sort(strs,new MyComparator());
StringBuilder sb = new StringBuilder();
for (String s : strs){
sb.append(s);
}
return sb.toString();
}

树的深度遍历

递归遍历方法

先序遍历:先中、再左、再右。对每一个结点,先打印当前结点,再打印其左子树所有结点,再打印右子树所有结点。

中序遍历:先左、再中、再右。对每一个结点,先打印其左子树所有结点,再打印当前结点,再打印右子树所有结点。

后序遍历:先左、再右、再中。对每一个结点,先打印其左子树所有结点,再打印右子树所有结点,再打印当前结点。

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 static void preOrderRecur(Node head){
if(head == null)
return;
//先打印当前节点
System.out.print(head.value+" ");
//递归打印左子树
preOrderRecur(head.left);
//递归打印右子树
preOrderRecur(head.right);
}
//中序打印二叉树的递归实现
public static void inOrderRecur(Node head){
if(head == null)
return;
//先打印左子树,再打印中间,再打印右子树
inOrderRecur(head.left);
System.out.print(head.value +" ");
inOrderRecur(head.right);
}
//后序打印二叉树的递归实现
public static void posOrderRecur(Node head){
if (head == null)
return;
//先打印左子树,再打印右子树,再打印当前节点
posOrderRecur(head.left);
posOrderRecur(head.right);
System.out.print(head.value + " ");
}

非递归遍历方法

先序遍历用栈结构实现,顺序为先中,再左,再右。将头结点放入栈,如果栈不为空,弹出当前节点,弹出就打印。如果右不为空,把右边的放入栈中。如果左不为空,把左边的放入栈中。为了让先弹左,再弹右,因此放入栈的时候要先右再左。

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 static void preOrderUnRecur(Node head){
System.out.print("pre-order: ");
if(head != null){
//准备一个栈
Stack stack = new Stack();
//将头结点压入
stack.add(head);
//只要栈不为空
while (!stack.isEmpty()){
//弹出并打印头结点
head = stack.pop();
System.out.print(head.value+" ");
//如果右节点不为空,压入
if(head.right != null){
stack.push(head.right);
}
//如果左节点不为空,压入
if(head.left != null){
stack.push(head.left);
}
}
}
System.out.println();
}

中序遍历用栈结构实现,顺序为先左,再中,再右。栈中先放所有的左边界,head从头往左移动,直到空,这样所有的左边界都到栈中了。当往左移动不下去了,从栈中弹出一个并打印,并向右移动。

​ 因为从中一直往左压栈,因此弹出的时候一定是从左到中,往右跑是因为要把右子树也按照这种方法压入栈。那么整个顺序是先左再中再右。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//中序遍历非递归实现
public static void inOrderUnRecur(Node head){
System.out.println("in-order: ");
if(head != null){
Stack stack = new Stack<>();
//只要栈不为空或者head不为空就循环
while (!stack.isEmpty() || head != null){
//如果head不为空,一直将左节点压入栈
if(head != null){
stack.push(head);
head = head.left;
}
//如果head为空,弹出并打印栈中元素,
else{
head = stack.pop();
System.out.print(head.value + " ");
head = head.right;
}
}
}
System.out.println();
}

后续遍历是先左再右再中。中左右,是先弹出中,然后压入右,压入左。那么中右左就是先弹出中,然后压入左,压入右。然后该打印的时候不打印,放入一个help,再弹出来,就是先左再右再中了。

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 static void posOrderUnRecur(Node head){
System.out.println("pos order: ");
if(head != null){
Stack stack = new Stack<>();
Stack help = new Stack<>();
//先把头节点压入
stack.push(head);
while (!stack.isEmpty()){
head = stack.pop();
//该输出的时候弹入辅助栈
help.push(head);
//先压左后压右
if(head.left != null){
stack.push(head.left);
}
if(head.right != null){
stack.push(head.right);
}
}
//将辅助栈中数据弹出
while (!help.isEmpty()){
System.out.print(help.pop().value+" ");
}
}
System.out.println();
}

折纸问题

这个问题实质是二叉树的中序遍历问题,头结点为下,左孩子为下,右孩子为下,用递归方式解决,递归函数参数列表有当前树高度,为左还是右(左为下,右为上)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//直观打印折纸
//本质上是树的中序遍历,左结点为下,右结点为上,头节点为下
//用递归实现,功能是中序打印二叉树,需要传入当前树的高度,左还是右
//因为只有两种可能,所以可以用布尔类型的变量来表示
public static void printAllFolds(int N){
printProcess(1,N,true);
}

private static void printProcess(int i, int n, boolean b) {
//base case
if (i > n)
return;
//left,cur,right
printProcess(i+1,n,true);
System.out.println(b ? "下" : "上");
printProcess(i+1,n,false);
}

在二叉树中找到一个节点的后继节点

现在树结点多了一个parent结点,指向父节点,头节点的parent指向null。只给一个在二叉树中某个节点node,实现返回node的后继节点的函数。在二叉树的后序遍历的序列中,node的下一个节点叫做node的后继节点。

中序遍历为左、中、右,考虑当前节点为中的情况,因此如果有右子树,后继节点应该是右子树的最左结点。当前节点如果为左,其为父节点的左孩子,则父节点就是后继节点。如果当前节点为右,表明其所在的左子树已被遍历完,需要找到某个节点,其为父节点的左孩子,返回此父节点。

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 static Node getNextNode(Node node){
if(node == null)
return null;
//node有右子树,找到右子树最左节点
if(node.right != null){
return getLeftMost(node.right);
}
//没有右子树
else {
Node parent = node.parent;//拿到父节点
//只要此节点不为父节点的左子树
//跳到node是parent左节点停,返回parent
//加上Parent不为空是为了兼顾没有后继的情况
//其中parent不为空要放在前面,不然就算空了,因为parent左孩子不为node,循环仍继续
while (parent != null && parent.left != node){
//当前节点不是父节点的左孩子则继续
node = parent;
parent = node.parent;
}
return parent;
}
}
//找最左节点
public static Node getLeftMost(Node node){
if(node == null){
return node;
}
while (node.left != null){
node = node.left;
}
return node;
}

树的序列化

树的先序方式序列化与反序列化

中、左、右。将遍历的结果用字符串进行记录。当一个节点的左子树为空时,可以用一个特殊符号来记录,如#。

序列化思路:利用递归来进行中序遍历,如果是空节点,则添加#!,如果不为空,添加value!,然后遍历左子树与右子树。

1
2
3
4
5
6
7
8
9
10
public static String serialByPre(Node head){
if (head == null){
return "#!";
}
//中、左、右的递归
String res = head.value + "!";
res += serialByPre(head.left);
res += serialByPre(head.right);
return res;
}

反序列化思路:将字符串用!进行分割,然后将数组中的字符一个个构造成节点,可以将其加入到队列中,也可以用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
//前序方式反序列化
public static Node reconByPreString(String preStr){
//分割字符串
String[] values = preStr.split("!");
Queue queue = new LinkedList<>();
//将所有元素加入到队列,用数组也可以,需要传递下标
for (int i = 0; i != values.length; i++) {
queue.offer(values[i]);
}
return reconPreOrder(queue);
}
//给一个队列建立树
public static Node reconPreOrder(Queue queue){
String value = queue.poll();
//空节点
if (value.equals("#")){
return null;
}
//建立新节点等于head
Node head = new Node(Integer.valueOf(value));
//左、右子树分别交给递归去实现
head.left = reconPreOrder(queue);
head.right = reconPreOrder(queue);
return head;
}

树的层序方式序列化与反序列化

层序方式也是广度优先遍历。

序列化思路:将头节点加入队列,并将其添加进StringBuilder,从队列中弹出一个节点,将其左右子树的值加入,没有就加入#,而不选择在弹出的时候添加是因为空节点无法加入队列。如果左子树不为空,加入队列;如果右子树不为空,加入队列。

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 static String serialByLevel(Node head){
if (head == null)
return null;
StringBuilder sb = new StringBuilder();
Queue queue = new LinkedList<>();
queue.offer(head);
sb.append(head.value).append("!");
while (!queue.isEmpty()){
head = queue.poll();
//在节点进去的时候就添加,不然添加不了空节点
if (head.left != null){
queue.offer(head.left);
sb.append(head.left.value).append("!");
}else {
sb.append("#!");
}
if (head.right != null){
queue.offer(head.right);
sb.append(head.right.value).append("!");
}else {
sb.append("#!");
}
}
return sb.toString();
}

按层方式反序列化 思路:将字符串分割,建造头节点并放入队列中,记录头节点。从队列中弹出一个节点,建造其左子树与右子树,如果左子树不为空,加入队列;如果右子树不为空,加入队列。此处用index+数组来控制当前要利用的字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//按层方式反序列化
public static Node reconByLevelString(String levelStr){
//将字符串分割
String[] str = levelStr.split("!");
int value = 0;
//产生头节点,并放进队列中
Node res = generateNode(str[value++]);
Node head = res;
Queue queue = new LinkedList<>();
queue.offer(head);
while (!queue.isEmpty()){
head = queue.poll();
head.left = generateNode(str[value++]);
head.right = generateNode(str[value++]);
if (head.left != null){
queue.offer(head.left);
}
if (head.right != null){
queue.offer(head.right);
}
}
return res;
}

判断树的类型

判断树是否为平衡二叉树

平衡二叉树中,在任何一个节点,左子树与右子树高度差,不超过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
public static boolean isB(Node head){
return process(head).isB;
}
public static ReturnData process(Node head){
//空树是高度为0的平衡树
if (head == null)
return new ReturnData(true, 0);
ReturnData leftData = process(head.left);
//左子树不平衡,则直接不平衡
//在不满足时高度为0是因为高度用不上
if (!leftData.isB) {
return new ReturnData(false, 0);
}
ReturnData rightData = process(head.right);
if (!rightData.isB){
return new ReturnData(false, 0);
}
//左、右树均平衡
if (Math.abs(leftData.h - rightData.h) > 1){
return new ReturnData(false, 0);
}
//在满足的时候,子结构要给父过程提供高度,为二者中较高的+1
return new ReturnData(true,Math.max(leftData.h, rightData.h)+1);
}

判断一棵二叉树是否是搜索二叉树

如果为搜索二叉树,则其中序遍历的输出一定是升序的,那么中序遍历树,如果当前节点的值小于上一个节点的值,则不为搜索二叉树,因此需要记录上一个节点的值 ,第一个节点的上一个节点为Integer的最小值,当遍历到节点比上一个节点小,返回假,否则更新上一个节点值为当前节点值,遍历下一个节点。当将树遍历完后,返回真。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//判断是否是搜索二叉树,用中序遍历非递归改
//只要在返回的时候,判断是否比前一个数大即可
public static boolean isBST(Node head){
if (head == null)
return true;
Stack stack = new Stack();
int pre = Integer.MIN_VALUE;
while (!stack.isEmpty() || head != null){
if (head != null){
stack.push(head);
head = head.left;
}else{
//弹出一个元素
head = stack.pop();
if (head.value < pre){
return false;
}
pre = head.value;
head = head.right;
}
}
return true;
}

判断一棵二叉树是否为完全二叉树

思路:利用层级优先遍历,一个节点的左右子树有四种不同状态,如果左有且右有,则继续判断;如果左有,右没有,则后面不能出现叶子节点;如果左没有,右有,肯定不是;如果左没有,右没有,后面不能出现叶子节点。

因此有两种是绝对不可能的

  • 左没有,右有
  • 之前有右边没有的,后来有非空节点
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 static boolean isCBT(Node head){
if (head == null)
return true;
//双端链表,实现队列
Queue queue = new LinkedList<>();
//表示是否开启了情况2
boolean leaf = false;
Node l = null;
Node r = null;
queue.offer(head);
while (!queue.isEmpty()){
head = queue.poll();
l = head.left;
r = head.right;
//有右没左或子节点不全且之后有非叶节点
if ((leaf && (l !=null || r != null)) || (l == null && r !=null)){
return false;
}
if (l != null)
queue.offer(l);
if (r != null){
queue.offer(r);
}else{
//右为空,开启状态2
leaf = true;
}
}
return true;
}

剑指Offer

数组

数组中重复数字

允许修改数组

长度为n的数组里所有数字均出现在o~n-1的范围内,数组内某些数字是重复的,但不知道几个重复了,也不知道数字重复了几次。找出数组中任意一个重复的数字。如 ,长度为7的数组{2,3,1,0,2,5,3},对应的输出为重复数字为2或者3。

方法1:使用哈希表,如果一个元素添加进哈希表了,就返回此元素,如果没有加入过,就加入。

时间复杂度O(n),额外空间复杂度O(n)。

1
2
3
4
5
6
7
8
9
10
11
12
13
//使用哈希表
public static Integer findNumHash(int[] arr){
if (arr == null || arr.length < 2)
return null;
HashSet set = new HashSet<>();
for (int i = 0; i < arr.length; i++) {
if (set.contains(arr[i]))
return arr[i];
else
set.add(arr[i]);
}
return null;
}

这种情况下解决的是找到第一个重复元素的问题,但是要额外建立一个哈希表。

方法2:抽屉原理

如果有n个数分布为0-n-1,在没有重复的时候,必然是一个萝卜一个坑,即可以将值为i的元素(i=0~n-1)放在第i个坑中,但是因为有重复的元素存在,会出现在第i个坑中值不为i的情况,这时候可以将它放在它原本应在的地方,直到当前坑中放了对应的萝卜,如果此时第i个坑中元素为k,但第k个坑中元素也为k,说明当前元素就是重复的。

总的时间复杂度为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
//找到第一个重复的数字,抽屉原理
public static Integer findNum(int[] arr){
if (arr == null || arr.length < 2)
return null;
//如果数组的长度不在0-arr.length-1之间,返回
for (int i = 0; i < arr.length; i++) {
if (arr[i] < 0 || arr[i] > arr.length-1)
return null;
}
//从第一个到最后一个
//如果当前数和角标不等,交换,直到相等或和要交换角标处的值相等
for (int i = 0; i < arr.length; i++) {
while (arr[i] != i){
if (arr[i] == arr[arr[i]]){
return arr[i];
}
//swap(arr,i,arr[i])
int temp = arr[i];
arr[i] = arr[arr[i]];
//不能直接使用arr[arr[i]],因为此时arr[i]已经变化了
arr[temp] = temp;
}
}
return null;
}

不修改数组找到重复数字

在长度为n+1的数组里的所有数字都在1~n的范围之内,因此数组中至少有一个数字是重复的。找出任意一个重复的数字,但不能修改输入的数组,如输入长度为8的数组{2,3,5,4,3,2,6,7},输出为重复的数字2或者3。额外空间复杂度为O(1)。

不能使用哈希表,不能使用抽屉原理,这时候可以考虑二分。因为如果1~n的数字没有重复,那么1~(n-1)/2(n-1)/2+1~n中元素个数应该和其角标left-right之间的范围相同,但是因为有重复的,那么假设重复数字为3,那么1~7分为1~45~7,这时候1~4中的元素个数肯定会多于4,然后再到1~4中进行寻找,直到找到left=right,如果这时候left值的数字出现个数>1,则输出left。

用到的为二分的模板

核心为

  1. left与right的取值很重要,需要夹住所有可能的情况
  2. while循环中用left < right,这样退出循环的时候一定有left==right,不用思考返回left还是right
  3. mid选择左中位数(left + ((right - left) >> 1))或者右中位数(left + ((right - left+1) >> 1))
  4. 只用两个判断,一次排除一半的结果
  5. 可以选择是否在循环结束后对夹住的数进行判断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int left = 数1, right = 数2;
while(left < right){
int mid = left + ((right - left) >> 1);
if(判断条件1){
left = mid + 1;
}else{
right = mid;
}
//或者
if(判断条件1){
right = mid - 1;
}else{
left = mid;
}
}
//在while结束后,对left处的值选择性进行判断
//多种输出可能
return left;
return arr[left];
return -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
//使用二分的思路
public static Integer findNum2(int[] arr){
if (arr == null || arr.length < 2)
return null;
//看是否在0-n-1范围之内
for (int i = 0; i < arr.length; i++) {
if (arr[i] < 0 || arr[i] > arr.length-1)
return null;
}
//二分,看哪边的数字更多,再继续找
int left = 1, right = arr.length - 1;
while (left < right){
int mid = left + ((right-left) >> 1);
//计算个数
int count = getFre(arr,left,mid);
if (count > mid + 1 - left){
right = mid;
}else {
//去另一半
left = mid+1;
}
}
int count = getFre(arr,left,right);
if (count > 1)
return left;
//没有找到
return -1;
}
//统计在l-m之间出现的次数
private static int getFre(int[] arr, int l, int m) {
int count = 0;
for (int i = 0; i < arr.length; i++) {
if (arr[i] >= l && arr[i] <= m){
count++;
}
}
return count;
}

矩阵中的路径【回溯】

请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。

思路:回溯的经典案例。如果当前字符可以,则看其上下左右的是否符合,不进入已经去过的位置,如果匹配长度为字符串的长度,返回真,不然的话就返回假,并重置当前所做的操作。

关键在于要维护一个与矩阵大小相同的布尔型矩阵,表示当前字符是否已经遍历过,如果遍历过了则不进入,如果当前字符与要遍历的第i个字符相等,则将布尔型的矩阵相对应位置置为true,再看其上下左右的,如果失败了,则让当前位置的标志重置为false。

容易错的点为,base case判断不完全,应该有

  1. 当前遍历到的(i,j)在矩阵范围内
  2. 当前位置的字符与第k个字符相等
  3. 当前位置没有进来过

同时需要注意的是一维到二维的映射关系,应该是第i行*列数+当前列j。

最后成功的base case条件为已经将所有的字符都判断完毕了,需要传递给下一级递归函数一个int变量。

递归函数实现的功能为,判断当前字符周围的字符是否可以找到所给的字符串。需要传递的参数有基本的矩阵的参数等,以及下一次寻找的起始位置(i,j),下一次遍历的字符串位置len。为了不让重复的字符遍历到,当遍历一个字符后,需要将其标志变量置为true,这样下一次不会再遍历。同时回溯为了消除下一次的影响,需要将标志位重置。

回溯的小总结

  1. 利用递归来处理子问题
  2. base case(递归终止条件,包括失败与成功条件)
  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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public static boolean hasPath(char[] matrix, int rows, int cols, char[] str) {
//如果数据不相符,直接返回假
if (matrix == null || str == null || matrix.length != rows * cols || str.length < 1)
return false;
//新建一个判断矩阵
boolean[] isIn = new boolean[rows * cols];
for (int i = 0; i < isIn.length; i++) {
isIn[i] = false;
}
//用来检测现在判断字符串长度的变量
int len = 0;
//对每一个数都进行判断
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
//如果当前字符开头的数组可以,则返回真
if (process(matrix, rows, cols, str, isIn, i, j, len)){
return true;
}
}
}
return false;
}

private static boolean process(char[] matrix, int rows, int cols, char[] str, boolean[] isIn, int i, int j, int len) {
//一维映射到二维,很重要
int index = i * cols + j;
//base case
//如果当前数据不再矩阵范围内,直接返回假
//如果已经进入了当前位置,返回假
//当前位置的数和str第len个字符不等
if (i < 0 || i >= rows || j < 0 || j >= cols || matrix[index] != str[len] || isIn[index])
return false;
//如果已经将数字找完了,返回真
if (len == str.length - 1)
return true;
//当前数字进入过了
isIn[index] = true;
//看当前数字的上下左右是否符合
if (process(matrix,rows,cols,str,isIn,i-1,j,len+1)
|| process(matrix,rows,cols,str,isIn,i+1,j,len+1)
|| process(matrix,rows,cols,str,isIn,i,j-1,len+1)
|| process(matrix,rows,cols,str,isIn,i,j+1,len+1)){
return true;
}
//还原当前状态
isIn[index] = false;
return false;
}

机器人的行走路径

描述:地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于k的格子。 例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8 = 19。请问该机器人能够达到多少个格子?

思路:回溯法,从当前格子开始走,需要维护一个是否走过的矩阵。在递归中,如果超出范围,已经走过了,当前位置不适合,返回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
24
25
26
27
28
29
30
31
32
33
34
35
public int movingCount(int threshold, int rows, int cols) {
//基础判断
if(rows <= 0 || cols <= 0 || threshold < 0)
return 0;
//准备是否进入的矩阵
boolean[] f = new boolean[rows * cols];
for (int i = 0; i < f.length; i++) {
f[i] = false;
}
int count = getCount(threshold,rows,cols,0,0,f);
return count;
}
private int getCount(int threshold, int rows, int cols, int i, int j, boolean[] f) {
int index = i * cols + j;
//如果超出范围,已经进来过,得到的数不符合,返回0
if (i < 0 || i >= rows || j < 0 || j >= cols || f[index] || getAllNum(i) + getAllNum(j) > threshold)
return 0;
//当前数字进来过了
f[index] = true;
//总的计数+1,返回其他四个方向的
//因为这个路径走过的,其他也不能走,因此不用重置f的状态
return 1 + getCount(threshold,rows,cols,i - 1,j,f)
+ getCount(threshold,rows,cols,i + 1,j,f)
+ getCount(threshold,rows,cols,i,j - 1,f)
+ getCount(threshold,rows,cols,i,j + 1,f);
}
private int getAllNum(int i) {
//得到所有位数之和
int sum = 0;
while (i > 0){
sum += i % 10;
i = i / 10;
}
return sum;
}

总结:对于矩阵上路径的遍历或者人物的行走,非常适合用回溯算法。

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

题目1:输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。

思路:典型的partition过程。但没法保证相对位置不变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//奇数放前面,偶数放后面,典型的partition问题
//但是这样不能保证相对位置不变
public void reOrderArray(int [] arr) {
int less = -1;
int more = arr.length;
int cur = 0;
while (cur < more){
if ((arr[cur] & 1) != 0){
swap(arr,++less,cur++);
}else {
swap(arr,--more,cur);
}
}
}
private void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}

题目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 void reOrderArray2(int [] arr) {
if (arr == null || arr.length < 2)
return;
//遍历数组,找到一个为偶数的,再找到后面为奇数的,将偶数-奇数段整体后移,空出来的放奇数
int i = 0, j = 0;
while (i < arr.length){
//为奇数,则继续找
while (!isEven(arr[i])){
i++;
}
//从i+1开始找,为偶数继续找
j = i + 1;
while (j < arr.length && isEven(arr[j])){
j++;
}
//如果j还在范围内,交换i+1-j-1之间的数
if (j < arr.length){
int temp = arr[j];
for (int k = j-1; k >= i; k--) {
arr[k+1] = arr[k];
}
arr[i] = temp;
}else {
//超出了就结束
break;
}
}
}

众数

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

方法一:排序,然后取中位数,然后次数是否大于长度一半。

方法二:mergeSort,找到左边数组中位数,再找右边数组的中位数,比较两个是否相同,如果不同分别计算在数组中出现的次数,返回次数多的。如果不一定有众数,需要额外判断。

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 int moreThanHalfNum1(int [] arr) {
//方法一,mergesort
if(arr == null || arr.length < 1)
return 0;
if (arr.length == 1)
return arr[0];
return process(arr,0,arr.length-1);
}

private int process(int[] arr, int l, int r) {
if (l >= r)
return arr[l];
int mid = l + ((r - l) >> 1);
int left = process(arr,l,mid);
int right = process(arr,mid+1,r);
if (left == right)
return left;
//左边不一样,计算左,右两边数字的个数
int leftNum = getNumFre(arr,l,mid,left);
int rightNum = getNumFre(arr,mid+1,r,right);
return leftNum > rightNum ? left : right;
}
private int getNumFre(int[] arr, int l, int mid,int num) {
int res = 0;
for (int i = l; i <= mid; i++) {
if (arr[i] == num)
res++;
}
return res;
}

方法三:投票法。众数出现的次数比其他数多,那么用每个数来抵消。如果当前计数为0,则众数为当前数。如果当前数与众数相同,计数器+1,不然-1。最后判断众数出现次数是否超过一半。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//投票计数法
public int moreThanHalfNum2(int [] arr) {
if(arr == null || arr.length < 1)
return 0;
if (arr.length == 1)
return arr[0];
//如果之前的数和现在相同,+1,如果计数为0,重新计数
int count = 0, pre = 0;
for (int i = 0; i < arr.length; i++) {
if (count == 0){
pre = arr[i];
}
count += arr[i] == pre ? 1 : -1;
}
count = 0;
for (int i = 0; i < arr.length; i++) {
if (arr[i] == pre)
count++;
}
return count > arr.length / 2 ? pre : 0;
}

连续子数组的最大和

输入一个整型数组,数组里有正数也有负数。数组中一个或连续的多个整/数组成一个子数组。求所有子数组的和的最大值。要求时间复杂度为O(n)。如6,-3,-2,7,-15,1,2,2,最大和的连续子数组为6,-3,-2,7,为8。

方法1:赌徒思想:如果之前拿到的数组和小于等于0,那么选择不要,因为加上只会让当前的和更小,让数组和为当前数;否则数组和加上当前数,判断当前和是否为最大。思路跟求众数的投票法比较类似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//找到连续子数组的最大和
public int FindGreatestSumOfSubArray(int[] arr) {
if (arr == null || arr.length < 1)
return 0;
int sum = 0, res = Integer.MIN_VALUE;
for (int i = 0; i < arr.length; i++) {
//如果之间的和小于等于0了,抛弃不要,为当前的;不然加上当前的
if (sum <= 0) {
sum = arr[i];
} else {
sum += arr[i];
}
res = Math.max(res, sum);
}
return res;
}

方法2:dp,求其递归公式,对于dp(n),如果n <1或者dp(n-1)<=0,dp(n)=arr[n];如果 dp(n-1)>0,dp(n)=dp(n-1)+arr[n]。第一种更优雅点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//dp写法
public int FindGreatestSumOfSubArray2(int[] arr) {
if (arr == null || arr.length < 1)
return 0;
int[] dp = new int[arr.length];
dp[0] = arr[0];
int res = dp[0];
for (int i = 1; i < arr.length; i++) {
if (dp[i-1] <= 0){
dp[i] = arr[i];
}else {
dp[i] = dp[i-1] + arr[i];
}
res = Math.max(res,dp[i]);
}
return res;
}

把数组排成最小的数

输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。

示例 :

1
2
输入: [10,2]
输出: "102

就是字符串的拼接问题,定义下比较器即可,可以使用堆,语法简单但是时间复杂度高。更好的是使用List的排序,然后自定义比较器即可。核心在于比较器的定义,算贪心。

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 static String printMinNumber1(int[] arr) {
if (arr == null || arr.length < 1)
return "";
PriorityQueue heap = new PriorityQueue<>((o1,o2)->(o1+o2).compareTo(o2+o1));
for (int i : arr) {
heap.add(String.valueOf(i));
}
StringBuilder sb = new StringBuilder();
while (!heap.isEmpty()){
sb.append(heap.poll());
}
return sb.toString();
}

public static String printMinNumber2(int[] arr) {
if (arr == null || arr.length < 1)
return "";
ArrayList list = new ArrayList<>();
for (int i : arr) {
list.add(i);
}
Collections.sort(list, new Comparator() {
@Override
public int compare(Integer o1, Integer o2) {
String s1 = String.valueOf(o1);
String s2 = String.valueOf(o2);
return (s1+s2).compareTo(s2+s1);
}
});
StringBuilder sb = new StringBuilder();
for (Integer i : list) {
sb.append(i);
}
return sb.toString();
}

把数字翻译成字符串

给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。

示例 :

输入: 12258
输出: 5
解释: 12258有5种不同的翻译,分别是”bccfi”, “bwfi”, “bczi”, “mcfi”和”mzi”

思路:对于第一个字符的位置,即12258中的1,总共的个数为2258的个数跟258的个数。即f(n) = f(n+1)+gxf(n+2),其中函数g可以取0或者1,取决于当前位置的数字与下一个数字组成的数字是否在10-25之间,要大于10是因为,如果像09这种,只能是0跟9,而小于25是因为最大英文字母只能到25。这样变成一个递归问题,但是如果直接这样递归算,有重复的子情况,选择动态规划,即从最后往前面算,对于最后一个数字,只能有1种情况,而对于倒数第二个数字,如果其与最后个数字合起来组成的数字符合规范,则个数是最后个数字+1,不然个数与最后个相同。而其他位置的使用递归公式计算,最后输出dp数组第一个位置的元素值。

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 static int getTranslationCount(int number) {
String s = String.valueOf(number);
if (s.length() < 2)
return 1;
int[] dp = new int[s.length()];
int len = dp.length - 1;
dp[len] = 1;
dp[len - 1] = getCount(s,len-1) ? dp[len] + 1 : dp[len];
for (int i = len - 2; i >= 0; i--) {
if (getCount(s,i)){
dp[i] = dp[i+1] + dp[i+2];
}else {
dp[i] = dp[i+1];
}
}
return dp[0];
}
private static boolean getCount(String s, int i){
//判断i,i+1组成的字符串是否在10-25之间
String str = s.substring(i,i+2);
int temp = Integer.valueOf(str);
if (temp >= 10 && temp <= 25)
return true;
return false;
}

礼物的最大价值

在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?

示例 :

输入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 12
解释: 路径 1→3→5→2→1 可以拿到最多价值的礼物

思路:dp,对于第一行元素与第一列元素,值为当前数加上左边或者上面的数。核心在于递归公式

dp(i)(j)=grid(i)(j)+max(dp(i-1)(j),dp(i)(j-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
//礼物拿取,明显的dp
public int maxValue(int[][] grid) {
if (grid == null || grid.length < 1)
return 0;
int[][] dp = new int[grid.length][grid[0].length];
int row = grid.length - 1;
int col = grid[0].length - 1;
dp[0][0] = grid[0][0];
//第一行赋值
for (int i = 1; i <= col; i++) {
dp[0][i] = grid[0][i] + dp[0][i - 1];
}
//第一列赋值
for (int i = 1; i <= row; i++) {
dp[i][0] = grid[i][0] + dp[i - 1][0];
}
//其他位置赋值
for (int i = 1; i <= row; i++) {
for (int j = 1; j <= col; j++) {
dp[i][j] = grid[i][j] + Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
return dp[row][col];
}

而优化的写法是只用一维的dp矩阵,先更新第一行的,然后从左到右更新第二行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//dp的优化写法
//只用一维矩阵来存数据
public int maxValue2(int[][] grid) {
if (grid == null || grid.length < 1)
return 0;
int[] dp = new int[grid[0].length];
int row = grid.length - 1;
int col = grid[0].length - 1;
dp[0] = grid[0][0];
//更新第一行
for (int i = 1; i <= col; i++) {
dp[i] = grid[0][i] + dp[i - 1];
}
//从第二行开始更新到最后一行
for (int i = 1; i <= row; i++) {
//更新第一列
dp[0] = dp[0] + grid[i][0];
for (int j = 1; j <= col; j++) {
dp[j] = grid[i][j] + Math.max(dp[j-1],dp[j]);
}
}
return dp[col];
}

丑数

题目1:判断一个数是不是丑数

编写一个程序判断给定的数是否为丑数。

丑数就是只包含质因数 2, 3, 5正整数。1也是丑数。

思路:一个数,如果与2的余数为0,就让他一直除以2;对3,5也一样,如果最后是1,说明是丑数,因为只有2,3,5这几个因数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public boolean isUgly(int num) {
if(num < 1)
return false;
while(num % 2 == 0){
num = num / 2;
}
while(num % 3 == 0){
num = num / 3;
}
while(num % 5 == 0){
num = num / 5;
}
return num == 1;
}

题目2:求出第n个丑数

输入: n = 10
输出: 12
解释: 1, 2, 3, 4, 5, 6, 8, 9, 10, 12 是前 10 个丑数。

思路:有第一个丑数1,其他丑数是1x2,1x3,1x5中较小的那个。这样得到第二个丑数2,然后看当前哪个丑数有乘2,3,5的资格,发现是2x2与1x3,1x5比较,第三个丑数是3,有资格乘2,3,5的丑数分别是2,2,1,以此类推,直到求出第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
//求第n个丑数
public static int nthUglyNumber(int n) {
if (n < 1)
return 0;
if (n == 1)
return 1;
int[] arr = new int[n];
arr[0] = 1;
int p2 = 0, p3 = 0, p5 = 0;
for (int i = 1; i < n; i++) {
//计算下一个丑数
arr[i] = Math.min(Math.min(arr[p2] * 2, arr[p3] * 3), arr[p5] * 5);
//更新对应的资格指针
//不使用else if是因为可能要同时更新多个资格指针
if (arr[i] == arr[p2] * 2) {
p2++;
}
if (arr[i] == arr[p3] * 3) {
p3++;
}
if (arr[i] == arr[p5] * 5) {
p5++;
}
}
return arr[n - 1];
}

题目3:超级丑数

编写一段程序来查找第 n 个超级丑数。

超级丑数是指其所有质因数都是长度为 k 的质数列表 primes 中的正整数。

示例:

输入: n = 12, primes = [2,7,13,19]
输出: 32
解释: 给定长度为 4 的质数列表 primes = [2,7,13,19],前 12 个超级丑数序列为:[1,2,4,7,8,13,14,16,19,26,28,32]

思路:与第n个丑数相同,其实上一题就是超级丑数的特殊情况,用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
//超级丑数
public int nthSuperUglyNumber(int n, int[] primes) {
if (n < 1)
return 0;
//一个数组存对应因子的资格指针
int[] hasArr = new int[primes.length];//默认为0,指向第一个数
int[] res = new int[n];//保存结果的数组
res[0] = 1;
int minNum = 0;
for (int i = 1; i < n; i++) {
//求指针所对应位置乘当前指针代表的因此最小值
minNum = Integer.MAX_VALUE;
for (int j = 0; j < primes.length; j++) {
minNum = Math.min(minNum,res[hasArr[j]]*primes[j]);
}
//更新所有指针与当前丑数
res[i] = minNum;
for (int j = 0; j < primes.length; j++) {
if (minNum == res[hasArr[j]]*primes[j]){
hasArr[j]++;
}
}
}
return res[n-1];
}

留坑:力扣上的丑数3

逆序对

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。

输入: [7,5,6,4]
输出: 5

思路:mergesort的过程,分而治之,在merge外排的时候,如果左边的数大于右边,则左边剩下的数都比右边的当前数大,那么逆序对的个数加上左边剩下数的个数。

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
//数组中的逆序对
public static int inversePairs1(int [] arr) {
if (arr == null || arr.length < 2)
return 0;
//mergeSort
return process(arr,0,arr.length - 1);
}
private static int process(int[] arr, int l, int r) {
if (l == r)
return 0;
int mid = l + ((r - l) >> 1);
return process(arr,l,mid) + process(arr,mid+1,r)+merge(arr,l,mid,r);
}
private static int merge(int[] arr, int l, int mid, int r) {
//外排
int[] help = new int[r - l + 1];
int index = 0;
int p1 = l, p2 = mid + 1;
int count = 0;
while (p1 <= mid && p2 <= r){
//谁小动谁
if (arr[p1] <= arr[p2]){
help[index++] = arr[p1++];
}else {
//关键
//左边的都比右边当前的大
count += mid - p1 + 1;
help[index++] = arr[p2++];
}
}
while (p1 <= mid){
help[index++] = arr[p1++];
}
while (p2 <= r){
help[index++] = arr[p2++];
}
//copy array
for (int i = 0; i < help.length; i++) {
arr[l+i] = help[i];
}
return count;
}

数组中数字出现的次数

题目1:一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。

输入:nums = [4,1,4,6]
输出:[1,6] 或 [6,1]

思路:基础版是只有一个数出现一次,其他都是2次,那么将所有的数相异或,那么最后得到的数就是他本身。一个数异或自己为0,一个数异或0为自己。现在有2个数只出现一次,那么所有的数异或后肯定不为0,那么不为0的这个数肯定有1,则有1的那一位是因为一个数那位是0,另一个数那位是1,那么找到那个为1的位,将此位是1的分成1组,为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
24
25
//找出数组中只出现了1次的两个数字
public int[] singleNumbers(int[] nums) {
if(nums == null || nums.length < 2)
return new int[]{};
//记录全^后的数子,找到是哪一位为1
int res = 0;
for (int num : nums) {
res ^= num;
}
//找到res哪一位为1
int index = 0;
while ((res & 1) == 0 && index < 32){
res = res >> 1;//右移一位
++index;
}
//遍历数组,如果指定位是1,放到一组,指定位是0,放到另一组
int count1 = 0, count2 = 0;
for (int num : nums) {
if (((num >>index) & 1) == 0)
count1 ^= num;
else
count2 ^= num;
}
return new int[]{count1,count2};
}

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

输入:nums = [3,4,3,3]
输出:4

思路:如果一个数出现3次,那么他的每一位出现的次数都是3的倍数。那么统计所有数字所有位是1的次数,用32的数组来存,如果某一位是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
24
25
26
27
28
//一个数出现1次,其他都出现了3次
//统计所有数的每一位二进制个数,如果当前位可以整除3,说明那个数当前位是0,否则是1
public int singleNumber(int[] nums) {
if (nums == null || nums.length < 1)
return 0;
int[] bitsMap = new int[32];//存每一位的个数
int bit = 1;
for (int num : nums) {
bit = 1;
//统计当前数每一位
for (int i = 31; i >= 0; i--) {
if ((num & bit) != 0){
//当前位+1
bitsMap[i]++;
}
//看下一位
bit = bit << 1;
}
}
//看每一位
int res = 0;
for (int i = 0; i < 32; i++) {
//将之前结果左移
res = res << 1;
res += bitsMap[i] % 3;
}
return res;
}

和为s的连续正数序列

输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。

序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。

输入:target = 9
输出:[[2,3,4],[4,5]]

思路:类似于双指针判断一个排序数组中是否存在和为s的两个数,同样使用双指针,如果双指针之间的数和等于目标数,加入结果集,右指针右移,sum变化;如果和大于目标,sum减去左边数,左指针右移;如果和小于目标,sum加上右边数,右指针右移。

核心:指针移动,sum变化。

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
//核心,动i或者j都要动sum
public static ArrayList> findContinuousSequence(int target) {
ArrayList> res = new ArrayList<>();
if(target < 3)
return res;
int i = 1, j = 2;//两个指针
int sum = i + j;
ArrayList temp = null;
while (j <= target / 2 + 1){
if (sum == target){
//将i,j之间结果添加
temp = new ArrayList<>();
for (int k = i; k <= j; k++) {
temp.add(k);
}
res.add(temp);
++j;
sum += j;
}else if (sum > target){
//太大了,移除左边的数
sum -= i;
++i;
}else if (sum < target){
//小了,j右移
++j;
sum += j;
}
}
return res;
}

滑动窗口最大值

给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。

输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7]

滑动窗口的位置 最大值


[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7

思路:用一个双端的队列来解决,如果一个数比队列中的其他元素都要大,那么将比他小的都弹出即可,然后将其放在队列的尾端。这样每次队列的头部就是最大的值,但是如果窗口已经超出了队列头部,那么就要把队列头部的值给划出。然后如果当前的位置已经到了窗口长度处,则开始将最大值放入结果集。

重点:放入队列的是元素的下标,而不是值,方便来判断是否将队列头部的值移出去。将比当前位置元素小的元素弹出的时候,使用的是while循环,要将全部比他小的都弹出去。

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 static int[] maxSlidingWindow(int[] nums, int k) {
if(nums == null || nums.length < 1)
return new int[]{};
if (k > nums.length)
return new int[]{nums[0]};
//准备一个双端队列,存的是下标
LinkedList list = new LinkedList<>();
int[] res = new int[nums.length-k+1];//存结果
//遍历数
for (int i = 0; i < nums.length; i++) {
//如果队列不为空,且末尾的值比当前小,弹出
//判断条件是while
while (!list.isEmpty() && nums[list.getLast()] < nums[i]){
list.pollLast();
}
//将这个数字加入到末尾
list.addLast(i);
//如果最前面的元素下标超出了,将超出范围元素移出
if (i-k >= list.peekFirst()){
list.pollFirst();
}
//如果超过滑动窗口长度,加入结果
if (i >= k-1){
res[i+1-k] = nums[list.peekFirst()];
}
}
return res;
}

股票的最大利润

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

输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。

解决思路:找波谷与波峰,将当前数与之前的最小数相减,判断是否符合,更新最小数

1
2
3
4
5
6
7
8
9
10
11
12
public int maxProfit(int[] prices) {
if(prices == null || prices.length < 2)
return 0;
//保存最小值和结果
int minNum = prices[0], res = 0;
for(int i = 1; i < prices.length; i++){
if(prices[i] - minNum > res)
res = prices[i] - minNum;
minNum = Math.min(minNum,prices[i]);
}
return res;
}

栈的压入、弹出序列

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

思路:准备一个栈,两个指针,i指向数组1,j指向数组2,循环条件为i没有越界或栈不为空(i越界后 ,有可能j还可以往后继续判断),如果栈为空且栈顶元素与j位置不同,若i越界了,返回假,不然将i位置元素压入栈,i后移。如果栈顶元素与j位置相同,弹出栈顶元素,j后移。循环结束后,如果i与j都越界,说明是符合的,不然就返回假。

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 boolean isPopOrder(int [] pushA,int [] popA) {
//base
if (pushA == null || popA == null || pushA.length != popA.length)
return false;
//准备栈
Stack stack = new Stack<>();
int i = 0, j = 0;
while (i < pushA.length || !stack.isEmpty()){
//如果栈为空或栈顶元素不为popA[j],将pushA[i]压入栈
if (stack.isEmpty() || stack.peek() != popA[j]){
if (i == pushA.length)
return false;
stack.push(pushA[i++]);
}else {
stack.pop();
j++;
}
}
if (i == pushA.length && j == i){
return true;
}else {
return false;
}
}

字符串

替换空格

请实现一个函数,把字符串中的每个空格替换成“%20”,如输入“We are happy”,则输出“We%are%happy”

方法:先统计传入的字符串str中空格的个数,然后建造一个新的字符数组,其长度为str非空格字符长度+空格个数*待替换字符串长度,然后用两个指针,一个遍历str,一个指向新的字符数组末尾,如果str当前不为空格,直接复制到新数组,新数组指针移动;如果str当前为空格,新数组从后逐个拷贝传入的字符串,直到遍历完str。

其中要注意的是新数组长度,需要先将原数组长度减去空格,再加上空格长度乘待替换字符串长度。如果不减去空格长度,新数组前面会有空余。如果没有空格,直接返回输入字符串即可

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
//不用系统的split方法,先统计插入后的字符个数,再从最后一个开始插入
public static String replaceBlank2(String str, String s){
//统计str空格数量及s长度
int spCount = 0;
int sSize = s.length();
//统计str中空格个数
for (int i = 0; i < str.length(); i++) {
if (' '==str.charAt(i)){
spCount++;
}
}
if (spCount == 0)
return str;
//非空格字符数+替换后的字符数
int newSize = str.length() - spCount + spCount * sSize;
char[] res = new char[newSize];
//两个指针,一个指向strArr,一个指向res
int p1 = str.length() - 1;
int p2 = res.length - 1;
for (; p1 >= 0; p1--) {
char c = str.charAt(p1);
if (' ' != c){
//当前不为空格,直接复制
res[p2--] = c;
}else {
//如果为空格,p2拷贝s
for (int i = sSize - 1; i >= 0; i--) {
res[p2--] = s.charAt(i);
}
}
}
return new String(res);
}

打印1到最大的n位数

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

思路:此题看上去很简单,其实真正的考点是大数问题,如超出了int和long类型长度的数字,这时候可以用字符串来手动模拟自增的过程,那么可以将这个问题进行拆解,一是字符串自增,二是打印字符串。

对于字符串自增,开辟长度为n的char数组,初始全部填充0,从最后位遍历到第一位。当前位的值是数组值-‘0’+进位,然后如果是最后一位自增+1模拟每次自增,然后判断当前位是否为10,如果当前位是第一位,说明到头了,可以返回真,如果不是,将当前位变成0,进位变成1。如果当前位小于10,说明没有进位,赋值后直接返回即可。

对于打印矩阵,找到第一个不是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
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
//打印从1-n所有的数
public static void print1ToMaxOfNDigits(int n){
if (n <= 0)
return;
char[] ch = new char[n];
for (int i = 0; i < ch.length; i++) {
ch[i] = '0';
}
//只要第一位没有进位,就继续
while (!increacement(ch)){
//打印矩阵
printNum(ch);
}
}

private static void printNum(char[] ch) {
//如果当前位数不为0,就加入
int n = 0;
while (n < ch.length){
if (ch[n] == '0'){
n++;
}else {
break;
}
}
for (int i = n; i < ch.length; i++) {
System.out.print(ch[i]);
}
System.out.println();
}

private static boolean increacement(char[] ch) {
//从最后一位开始,模拟自增
int len = ch.length - 1;
int sum = 0;//当前位数的和
int addNum = 0;
for (int i = len; i >= 0; i--) {
//当前位加上进位
sum = ch[i] - '0' + addNum;
//如果是最低位,加一
if (i == len){
sum++;
}
//如果sum为10了,看是不是第一位,是的话返回true
if (sum == 10){
if (i == 0){
return true;
}else {
//不是,需要当前位改成0,进位
sum = 0;
ch[i] = (char)(sum + '0');
addNum = 1;
}
}else {
//没有进位,直接返回
ch[i] = (char)(sum + '0');
break;
}
}
return false;
}

字符串全部子序列

打印一个字符串的全部子序列,包括空字符串。子串是连在一起的,子序列中的字符在字符串中不一定是连在一起的。

思路:初始为一个空字符串,遍历每一个字符,可以选择要当前的字符或者不要,然后递归子字符串。最后输出的就是全部的子序列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void printAllSebSeq(String str){
if (str == null)
System.out.println("");
allSubSequence(str.toCharArray(),0,"");
}

private static void allSubSequence(char[] ch, int i, String s) {
//base case
if (i == ch.length){
System.out.println(s);
return;
}
//选择而要当前子串与不要
allSubSequence(ch,i+1,s);
allSubSequence(ch,i+1,s+ch[i]);
return;
}

打印字符串的全排列

打印一个字符串的全排列,如字符串123,有6种全排列,打印出来。

思路:递归。将所有的字符依次与第一个交换,然后递归变化交换后的子串。递归结束后,将字符交换回来,即回溯不影响下一次的结果。base case为没有子串为止,输出字符串。为了在遍历字符串进行交换的时候,不对重复的字符做操作,在每轮交换前定义一个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
33
//字符串的全排列
public static void printAllPermutations(String str) {
if (str == null){
System.out.println("");
return;
}
process(str.toCharArray(),0);
}
//作用,打印出字符串的全排列
private static void process(char[] ch, int i) {
//base case
if (i == ch.length){
System.out.println(String.valueOf(ch));
return;
}
HashSet set = new HashSet<>();
//将当前字符与后面每一个交换,然后递归子串
for (int j = i; j < ch.length; j++) {
if (!set.contains(ch[j])){
set.add(ch[j]);
swap(ch,i,j);
//处理子串
process(ch,i+1);
//回溯,交换回来
swap(ch,i,j);
}
}
}
private static void swap(char[] ch, int i, int j) {
char temp = ch[i];
ch[i] = ch[j];
ch[j] = temp;
}

方法2:dfs,一个字符串的全排列其实可以看成是树,每次从没有选择过的字符串中进行选择,加入到结果集中,到最后的时候将结果集打印即可。

因此遍历树可以使用深度优先遍历dfs,dfs的核心点在于

  1. 截止条件,什么时候返回
  2. 遍历候选节点
    • 筛选候选节点
    • 改变状态
    • 进行下一次的dfs
    • 状态回溯

关键就是截止条件+遍历候选节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private static void dfs(char[] ch, boolean[] f, int i, char[] res) {
//1、截止条件
if (i == ch.length){
//结果加入,返回
System.out.println(String.valueOf(res));
}
//2、遍历候选节点
for (int j = 0; j < ch.length; j++) {
char c = ch[j];
//2.1,筛选
if (!f[j]){
//如果当前没有被选过,修改状态
f[j] = true;
//加入到结果
res[i] = c;
//下一次dfs
dfs(ch,f,i+1,res);
//回溯,修改状态
f[j] = false;
res[i] = 0;
}
}
}

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

请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。

输入: “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。

方法一:滑动窗口

用哈希表来维护一个滑动窗口,如果当前字符没有加入过,加入,移动右边指针,计算长度;如果加入过了,将左边元素移出,右边指针不变,直到没有重复的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//滑动窗口解法
public static int lengthOfLongestSubstring1(String s) {
if(s == null || s.length() < 1)
return 0;
HashSet set = new HashSet<>();
int i = 0, j = 0;
int res = 0;
while (j < s.length()){
if (!set.contains(s.charAt(j))){
set.add(s.charAt(j++));
res = j - i > res ? j - i : res;
}else {
set.remove(s.charAt(i++));
}
}
return res;
}

方法二:滑动窗口改进版

保存每个字符出现的位置,初始默认为-1,保存上一次的长度cur。遍历字符串,如果此字符没有出现过,cur=cur+1;如果出现过,若出现的位置比当前窗口的起点小(起点为当前位置-窗口长度),说明不在当前维护的窗口中,cur=cur+1,否则,当前子串出现了重复的,更新cur为当前位置-当前字符上一次出现位置,即窗口起点为重复位置的下一个。因为不确定传入的字符范围,选择用一个较大长度的数组来存。如果确定只有a-z,可以只用26长度的数组存位置。

其中在更新的时候,写法选择 res = Math.max(res,++cur),因为cur需要被更新,且计算最大值用的是更新后的,因此不需要cur++写法,而是++cur。即更新了位置,又可以用来比较是否是最大值。

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 static int lengthOfLongestSubstring2(String s) {
if(s == null || s.length() < 1)
return 0;
int[] arr = new int[130];//保存每个字母出现的位置,默认为-1
for (int i = 0; i < arr.length; i++) {
arr[i] = -1;
}
//定义上一个长度f(i-1)与当前长度f(n)
int cur = 0;
int res = 0;//返回最大长度
int index = 0;
//遍历s
for (int i = 0; i < s.length(); i++) {
//计算当前字母对应的位置
index = s.charAt(i);
//如果没有出现或者距离比当前的大,+1
if (arr[index] < 0 || i - arr[index] > cur){
//更新最大与当前长度
res = Math.max(res,++cur);
}else {
//出现过了,当前长度减少
cur = i - arr[index];
}
//更新字符出现位置
arr[index] = i;
}
return res;
}

第一个只出现一次的字符

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

方法一:哈希表

第一次遍历求出每个字符出现的频率,第二次遍历找到第一次频率为1的。

1
2
3
4
5
6
7
8
9
10
11
12
13
public static int firstNotRepeatingChar1(String str) {
if (str == null || str.length() < 1)
return -1;
HashMap map = new HashMap<>();
for (int i = 0; i < str.length(); i++) {
map.put(str.charAt(i),map.getOrDefault(str.charAt(i),0) + 1);
}
for (int i = 0; i < str.length(); i++) {
if (map.get(str.charAt(i)) == 1)
return i;
}
return -1;
}

方法二:用数组存,因为char2字节,最多256,用256数组模拟哈希表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//数组存
public static int firstNotRepeatingChar2(String str) {
if (str == null || str.length() < 1)
return -1;
int[] arr = new int[256];
for (int i = 0; i < str.length(); i++) {
arr[str.charAt(i)]++;
}
for (int i = 0; i < str.length(); i++) {
if (arr[str.charAt(i)] == 1)
return i;
}
return -1;
}

字符流中第一个只出现一次的字符

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

思路:用一个数组来对应每个字符,记录其出现的顺序,初始默认均为-1,如果第一次进来,赋值为顺序,第二次进来,赋值为-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
37
public class FisrtOneceCharInStream {
//字符流中第一个只出现一次的字符
int[] arr = new int[256];//表示字符出现的位置
int index;//记录出现的顺序
//初始化均为-1,表示没有出现过
public FisrtOneceCharInStream(){
index = 0;
for (int i = 0; i < arr.length; i++) {
arr[i] = -1;
}
}
public void Insert(char ch)
{
//没有出现过,放到对应的位置
if (arr[(int)ch] == -1){
arr[(int)ch] = index;//对应的位置
}else {
//已经出现过,变成-2
arr[(int)ch] = -2;
}
++index;
}
//return the first appearence once char in current stringstream
public char FirstAppearingOnce()
{ int minFre = Integer.MAX_VALUE;
int index = 0;//对应的字符转成int
for (int i = 0; i < arr.length; i++) {
if (arr[i] > -1 && arr[i] < minFre){
minFre = arr[i];
index = i;
}
}
if (minFre == Integer.MAX_VALUE)
return '#';
return (char)index;
}
}

翻转单词顺序

输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。为简单起见,标点符号和普通字母一样处理。例如输入字符串”I am a student. “,则输出”student. a am I”。

输入: “the sky is blue”
输出: “blue is sky the”

思路:先把字符串整体旋转,然后分别将每个单词再进行旋转,这样就可以得到结果。重点是对每个单次旋转,找到起始点,与空格处,然后旋转之间的字符串,然后起始点移动。

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 String reverseSentence(String str) {
if (str == null || str.length() < 2)
return str;
char[] ch = str.toCharArray();
//如果全部是空格,返回
if (str.replaceAll(" ","").equals(""))
return str;
//先整体翻转
reverseAllStr(ch,0,ch.length-1);
//然后按照空格分隔,每个再反转
//找到非空格的位置
int start = 0, end = 0;
while (start < ch.length){
//找到end位置
while (end < ch.length && ch[end] != ' '){
++end;
}
//交换
reverseAllStr(ch,start,end-1);
++end;
start = end;
}
return String.valueOf(ch);
}
private void reverseAllStr(char[] ch, int i, int j) {
//从头,末尾,两两交换
while (i < j){
swap(ch,i++,j--);
}
}
private void swap(char[] ch, int i, int j) {
char c = ch[i];
ch[i] = ch[j];
ch[j] = c;
}

左旋转字符串

字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串”abcdefg”和数字2,该函数将返回左旋转两位得到的结果”cdefgab”。

输入: s = “abcdefg”, k = 2
输出: “cdefgab”

思路:

与之前旋转数组的思路很类似,可以将前面k个先旋转,剩下的再旋转,然后整体再旋转,旋转三次得到结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static String leftRotateString(String str,int n) {
if (str == null || str.length() < 2 || n < 0)
return str;
n %= str.length();
char[] ch = str.toCharArray();
//先旋转0-n-1
reverseChar(ch,0,n-1);
reverseChar(ch,n,ch.length-1);
reverseChar(ch,0,ch.length-1);
return String.valueOf(ch);
}
public static void reverseChar(char[] ch, int i, int j){
while (i < j){
swap(ch,i++,j--);
}
}
public static void swap(char[] ch, int i, int j) {
char c = ch[i];
ch[i] = ch[j];
ch[j] = c;
}

链表

反向打印链表

输入一个链表的头节点,从尾到头反过来打印出每个节点的值。

方法1:遍历链表,将值存入栈。遍历栈,弹出值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//反向打印链表
public static String reversePrint(Node node){
if (node == null)
return null;
StringBuilder sb = new StringBuilder();
Stack stack = new Stack<>();
while (node != null){
stack.push(node.value);
node = node.next;
}
while (!stack.isEmpty()){
sb.append(stack.pop()).append(" ");
}
return sb.toString();
}

方法2:既然有用到栈 ,那么也可以用递归来实现,递归函数的功能是打印此节点后面的节点。基本过程是调用递归函数,然后打印当前节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//既然有用到栈,那么就可以用递归来实现,先打印后面的,然后打印当前的
public static String reversePrint2(Node node){
if (node == null)
return null;
StringBuilder sb = new StringBuilder();
process(node,sb);
return sb.toString();
}
private static void process(Node node, StringBuilder sb) {
//递归来实现
//base case
if (node == null){
return;
}
//递归后面的节点
process(node.next,sb);
//打印当前节点
sb.append(node.value).append(" ");
}

删除链表中重复的节点

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

思路:链表删除的,如果头节点不好处理,就新建一个,设立两个指针,一个指向上一个不重复的节点pre,一个指向当前的节点cur,遍历链表,如果当前节点下一个节点的值和当前节点相同,移动cur,直到cur到末尾或者cur指向最后一个相同的节点,让pre的下一个指向cur.next,这样可以将重复的节点全部删除,然后cur再到下一个。注意,此时pre不能移动,因为不能确定cur的下一个与下下个是否相同,如果pre直接移动,遇到1->2->2->3->3这种,虽然两个2被跳过了,但是后面的3会被包含进去。只有在确定了cur与cur.next值不同,pre才移动,cur也移动。

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 Node deleteDuplication(Node head){
//基本判断
if(head == null || head.next == null)
return head;
//在现在头节点前造一个
Node newHead = new Node(0);
newHead.next = head;
Node pre = newHead;//一个不重复的节点
Node cur = head;//当前节点
//遍历链表
while (cur != null){
//如果下一个不为空且值跟当前相同,往后走
if (cur.next != null && cur.next.value == cur.value){
//让cur走到最后一个不重复的节点处
while (cur.next != null && cur.next.value == cur.value){
cur = cur.next;
}
//若重复节点一个不要,让pre下一个指向cur.next
//pre不能前进,防止后面还有重复的,只有没有重复的pre才走
pre.next = cur.next;
cur = cur.next;
}else {
//没有重复,正常走
pre = pre.next;
cur = cur.next;
}
}
Node res = newHead.next;
newHead = null;
return res;
}

题目2:删除链表中的重复节点,如给定1->2->2->3->3,返回1->2->3。

思路:比上一题简单,比如直观的做法是,让一个pre指针指向第一个,cur指针指向第二个,如果当前值与pre相等,删除当前节点。相同点是,删除节点的时候,pre均不移动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//这样更简单,只需要判断当前节点与前一个节点值是否相等,相等则删除
public Node deleteDuplicates(Node head) {
if(head == null || head.next == null)
return head;
Node pre = head;
Node cur = head.next;
Node next = null;
while(cur != null){
next = cur.next;
//如果当前的值等于前一个节点,删除
if(cur.value == pre.value){
pre.next = cur.next;
cur = cur.next;
}else{
//正常走
pre = pre.next;
cur = cur.next;
}
}
return head;
}

链表倒数第k个节点

题目1:找到链表倒数第k个节点

输入一个链表,输出该链表中倒数第k个结点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾结点是倒数第1个结点。例如一个链表有6个结点,从头结点开始它们的值依次是1、2、3、4、5、6。这个链表的倒数第3个结点是值为4的结点。

思路:比较简单的做法是两次遍历,先知道链表长度n为多少,然后找到正着第n-k+1个节点即可,但是可以只用一次遍历来完成。用两个指针,指针1先走k-1步,然后让指针2与1一起走,当走到链表末尾时,便走到了倒数第k个节点。原因是1走到末尾,2为了是倒数第k个,与1之间的距离是k-1。需要注意的是要对k和链表head做判断,如果k<=0或者head为空,直接返回空,如果指针1还没走到k-1步就到了末尾,也返回空。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public Node findKthToTail(Node head,int k) {
//基础判断,k不大于0或者头节点为空,返回空
if(k <= 0 || head == null)
return null;
//增加一个头节点
//两个指针,让一个指针先走k-1步
Node pre = head, cur = head;
k = k - 1;
while (k > 0 && cur.next != null){
cur = cur.next;
k--;
}
//k比链表长,返回空
if(k != 0)
return null;
//让两个链表一起走
while (cur.next != null){
cur = cur.next;
pre = pre.next;
}
return pre;
}

题目2:删除链表倒数第k个节点

思路:如果涉及到删除操作,因为可能删除的是头节点,因此自己造一个新的头节点会比较方便,让指针1,2都从新的头节点出发,让指针1先走k步,然后1与2一起走,当指针1走到链表末尾,2走到倒数第k+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
//删除链表末尾的第k个节点
public Node deleteKthToTail(Node head,int k) {
//基础判断
if(k <= 0 || head == null)
return head;
//增加一个头节点
Node newHead = new Node(0);
newHead.next = head;
//两个指针,让一个指针先走k步
Node pre = newHead, cur = newHead;
while (k > 0 && cur.next != null){
cur = cur.next;
k--;
}
//k比链表长,返回空
if(k != 0)
return head;
//让两个链表一起走
while (cur.next != null){
cur = cur.next;
pre = pre.next;
}
//删除pre后一个节点
pre.next = pre.next.next;
head = newHead.next;
newHead = null;
return head;
}

圆圈中最后的数字

0,1,…,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字。求出这个圆圈里剩下的最后一个数字。

例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。

思路:使用链表,每次计算下一次要删除的位置,然后进行删除,下一次被删除的位置是(当前位置+m-1)%当前元素个数,这样当总共只有一个元素的时候,将其输出即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//约瑟夫环
//用链表来做,计算每次要移除的数字,然后移除,直到剩一个
public int lastRemaining(int n, int m) {
if(n < 0 || m < 0)
return 0;
List list = new ArrayList<>();//便于查找,用linkedlist超时
for (int i = 0; i < n; i++) {
list.add(i);
}
int removeIndex = 0;//要移除的数据角标
while (list.size() > 1){
removeIndex = (removeIndex + m - 1) % list.size();
list.remove(removeIndex);
}
return list.get(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
37
//根据前序和中序构建二叉树
public static Node reConstructBinaryTree(int[] arrPre, int[] arrIn){
//如果两个长度不等或为空,直接返回
if (arrPre.length != arrIn.length || arrPre.length < 1)
return null;
//base case
if (arrPre.length == 1)
return new Node(arrPre[0]);
//在arrIn中找到与arrPre[0]相等的头节点
int index = -1;
for (int i = 0; i
if (arrIn[i] == arrPre[0]){
index = i;
break;
}
}
//没找到头节点,返回
if (index == -1)
return null;
//构造头节点
Node head = new Node(arrPre[0]);
//左子树前序遍历
int[] lPreChild = new int[index];
System.arraycopy(arrPre,1,lPreChild,0,index);
//左子树中序遍历
int[] lInChild = new int[index];
System.arraycopy(arrIn,0,lInChild,0,index);
head.left = reConstructBinaryTree(lPreChild,lInChild);
//右子树前序遍历
int[] rPreChild = new int[arrPre.length-index-1];
System.arraycopy(arrPre,index+1,rPreChild,0,rPreChild.length);
//右子树中序遍历
int[] rInChild = new int[rPreChild.length];
System.arraycopy(arrIn,index+1,rInChild,0,rPreChild.length);
head.right = reConstructBinaryTree(rPreChild,rInChild);
return head;
}

树的子结构【重要】

输入两棵二叉树A和B,判断B是不是A的子结构。

思路:遍历树A,找到与B相同值的节点。判断两个子树是否有一样的结构。递归的解法为:判断A树,A树的左子树,A树的右子树是否与B数结构相同。判断函数为:如果B树为空了,说明结构相同;如果A树空了,B树不空,肯定不同;如果当前节点的值不同,肯定不同;再判断左右子树是否相同。

这一题的坑在于,需要先遍历到每一个节点,然后再对其进行判断,递归每个节点的方法为前序,即先判断当前节点,然后递归判断左右节点,因此是process(A,B) || isSubStructure(A.left,B) || isSubStructure(A.right,B),而不是三个均使用process判断,否则就只是判断了头节点与左右节点三个节点!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 前序遍历+递归判断
public boolean isSubStructure(TreeNode A, TreeNode B) {
if(B == null || A == null)
return false;
// 前序遍历,先处理当前节点,然后递归判断其左右子树
return process(A,B) || isSubStructure(A.left,B) || isSubStructure(A.right,B);
}
private boolean process(TreeNode a, TreeNode b){
// 如果b为空,则说明是a子树
if(b == null)
return true;
// 若a为空,此时b不为空,说明不是子树
if(a == null)
return false;
// 若当前节点值不一样,则肯定不是
if(a.val != b.val)
return false;
// 看左右子树
return process(a.left,b.left) && process(a.right,b.right);
}

二叉树的镜像

请完成一个函数,输入一个二叉树,该函数输出它的镜像。

思路:递归完成。如果树为空,返回;如果树的左右子树均为空,返回,否则交换左右子树;对左右子树进行同样操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//得到一个二叉树的镜像
public void Mirror(Node root) {
//base case
if (root == null)
return;
//交换左右子树
if (root.left == null && root.right == null)
return;
Node temp = root.left;
root.left = root.right;
root.right = temp;
//递归左右子树
Mirror(root.left);
Mirror(root.right);
}

对称的二叉树

请实现一个函数,用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样,那么它是对称的。

思路:比较这棵树与自己是否为镜像。递归解决,如果两个有一个为空,若是均为空,说明为真,若一个为空,另一个不为空,说明肯定不对称。判断当前节点值是否相同。判断root的左子树与root的右子树是否对称,判断root的右子树与root的左子树是否对称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//判断一个树是否对称
//可以判断两棵树是否为镜像树
boolean isSymmetrical(Node root) {
if (root == null)
return true;
//调用递归子函数
return isSubSym(root,root);
}

private boolean isSubSym(Node root1, Node root2) {
//base case
if (root1 == null || root2 == null){
if (root1 == null && root2 == null)
return true;
//一个为空,一个不是,返回假
else
return false;
}
//判断当前节点是否相同
if (root1.value != root2.value)
return false;
//判断当前树的左子树与镜像的右子树
return isSubSym(root1.left,root2.right) && isSubSym(root1.right,root2.left);
}

从上往下打印二叉树

普通的就是层序遍历,有意思点的是从上到下按层打印二叉树,同一层结点从左至右输出。每一层输出一行。

思路:大体仍是层序遍历,但是需要一下子将同一层的全部加入或打印,这样就需要统计一层的节点个数,初始一层为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
//按层打印,一层为一行
ArrayList> print(Node root) {
ArrayList> res = new ArrayList<>();
if (root == null)
return res;
ArrayList list = null;
int pre = 0, cur = 1;
Queue queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()){
//不一下子只弹出一个,而是弹出上一层的个数
list = new ArrayList<>();
//更新当前个数,作为下一次的pre
pre = 0;
for (int i = 0; i < cur; i++) {
root = queue.poll();
list.add(root.value);
if (root.left != null){
queue.offer(root.left);
pre++;
}
if (root.right != null){
queue.offer(root.right);
pre++;
}
}
//循环完后,将一层结果加入,更新cur
res.add(list);
cur = pre;
}
return res;
}

之字型打印二叉树

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

思路:在之前从上往下打印的基础上增加了顺序判断,正常为从左到右打印,从右到左相当于是从左到右入栈的结果,因此在之前的基础上,加入判断即可,如果需要从右到左输出,加入栈中。然后将栈中元素全部添加至list中。

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
//按照之字型打印树
public static ArrayList> printZhiTree(Node root){
ArrayList> res = new ArrayList<>();
if (root == null)
return res;
//广度优先遍历
Queue queue = new LinkedList<>();
queue.offer(root);
boolean flagZhi = true;
int pre = 0, cur = 1;
Stack stack = new Stack<>();
while (!queue.isEmpty()){
//当层元素
ArrayList list = new ArrayList<>();
pre = 0;
for (int i = 0; i < cur; i++) {
//弹出元素
//如果是true,直接加入list
//如果是false,加入栈
root = queue.poll();
if (flagZhi){
list.add(root.value);
}else {
stack.add(root.value);
}
if (root.left != null) {
queue.offer(root.left);
pre++;
}
if (root.right != null) {
queue.offer(root.right);
pre++;
}
}
//将栈中或队列中元素信息取出
if (!flagZhi){
while (!stack.isEmpty()){
list.add(stack.pop());
}
}
//更新一层信息
res.add(list);
cur = pre;
flagZhi = !flagZhi;
}
return res;
}

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

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

思路:树的遍历。当遍历到当前节点的时候,将目标减去当前节点值,将当前节点值添加至list中,表示走过了,如果当前节点是叶子节点且目标值等于0,将list中值复制到结果中。递归其左,右子树。回溯,将list中最后个元素删除。

坑点有3个

  • 不能直接将list添加至res中,因为list会变化,而res中加的是list的地址,需要额外新建一个容器来将list中的值进行拷贝
  • 在叶子节点处判断而不是空节点,不然会将一个叶子节点的路径重复添加
  • 删除list的元素的时候,remove的是下标,不能直接remove当前节点的值
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
ArrayList> res = new ArrayList<>();
ArrayList list = new ArrayList<>();
public ArrayList> findPath(TreeNode root, int target) {
if (root == null)
return res;
//递归解决
process(root,target);
return res;
}
//功能,求树的路径
private void process(TreeNode root, int target) {
//base case
if (root == null){
return;
}
//处理当前情况
list.add(root.val);
target -= root.val;//用减去来表示剩下要处理的值,传递给下一次
//如果当前是叶子节点并且target=0,新建一个list,添加结果
//不在空节点处理是因为在空节点处理会有重复添加情况
if (root.left == null && root.right == null && target == 0){
//不能直接加是因为加的是地址,list会变化
ArrayList temp = new ArrayList<>();
for (Integer i : list) {
temp.add(i);
}
res.add(temp);
}
//处理左,右子树
process(root.left,target);
process(root.right,target);
//回溯,消除当前影响
//清除使用的是角标,不是元素
list.remove(list.size()-1);
return;
}

二叉搜索树与双向链表

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

思路:中序遍历二叉树,记录其前一个节点,如果前一个节点为空,当前节点为返回的头节点,让当前节点的left指向前一个节点;否则,让前序节点的right指向当前节点,当前节点left指向前序节点。更新前序节点。

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 TreeNode Convert(TreeNode root) {
if (root == null)
return root;
//Inrder
Stack stack = new Stack<>();
TreeNode pre = null;
TreeNode res = null;
while (!stack.isEmpty() || root != null){
if (root != null){
stack.push(root);
root = root.left;
}else {
root = stack.pop();
if (pre == null){
res = root;
root.left = pre;
}else {
root.left = pre;
pre.right = root;
}
pre = root;
root = root.right;
}
}
return res;
}

二叉搜索树的第k个结点

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

思路:二叉树的中序遍历,当到第k个的时候,返回即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//二叉搜索树第k小的结果
TreeNode KthNode(TreeNode root, int k){
if (k < 1 || root == null)
return null;
//中序遍历
Stack stack = new Stack<>();
while (!stack.isEmpty() || root != null){
if (root != null){
stack.push(root);
root = root.left;
}else {
//弹出一个
root = stack.pop();
k--;
if (k == 0){
return root;
}
root = root.right;
}
}
return null;
}

二叉搜索树的第k大节点

二叉搜索树按照左中右的顺序来,是递增的。而第k大节点,看的是递减的顺序,那么就先压入右边,再压入左边即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public int kthLargest(TreeNode root, int k) {
//中序遍历改动,右中左
if(root == null || k < 1)
return 0;
Stack stack = new Stack<>();
while(!stack.isEmpty() || root != null){
if(root != null){
stack.push(root);
root = root.right;
}else{
root = stack.pop();
if(--k == 0)
return root.val;
root = root.left;
}
}
return 0;
}

二叉树的深度

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

思路:深度优先遍历,一个节点的深度,等于左右子树最大深度+1

1
2
3
4
5
6
7
8
9
10
11
12
13
//求二叉树的深度
public int treeDepth(TreeNode root) {
if (root == null)
return 0;
return getInOrderDepth(root);
}
private int getInOrderDepth(TreeNode root) {
//base case
if (root == null)
return 0;
//处理当前的
return 1 + Math.max(getInOrderDepth(root.left),getInOrderDepth(root.right));
}

二叉搜索树的后序遍历

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

参考以下这颗二叉搜索树:

1
2
3
4
5
    5
/ \
2 6
/ \
1 3

输入: [1,6,3,2,5]
输出: false

二叉搜索树的定义为,其中序遍历左<中<右,而后序遍历的顺序是左,右,中,找到左,右,分别判断是否比最后个数小和大即可。递归判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public boolean verifyPostorder(int[] postorder) {
if(postorder == null || postorder.length < 1){
return true;
}
return process(postorder,0,postorder.length - 1);
}
public boolean process(int[] postorder, int l, int r){
if(l >= r){
return true;
}
int mid = l;
while(mid < r && postorder[mid] < postorder[r]){
mid++;
}
for(int i = mid; i < r; i++){
if(postorder[i] < postorder[r]){
return false;
}
}
return process(postorder,l,mid - 1) && process(postorder,mid,r - 1);
}

最小的K个数

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

思路:最小的K个数的类型一般用堆,遍历数组,如果堆容量小于k,直接放入k,不然如果当前数小于堆顶的数,弹出堆顶的数,将当前数加入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//找到最小的K个数
public ArrayList GetLeastNumbers_Solution(int[] arr, int k) {
ArrayList res = new ArrayList<>();
if (arr == null || arr.length < 1 || k > arr.length || k < 1)
return res;
PriorityQueue queue = new PriorityQueue<>((o1, o2) -> o2 - o1);
for (int i = 0; i < arr.length; i++) {
if (queue.size() < k) {
queue.add(arr[i]);
} else if (arr[i] < queue.peek()) {
queue.poll();
queue.add(arr[i]);
}
}
for (int i : queue) {
res.add(i);
}
return res;
}

二分

找旋转数组的最小值

把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。
NOTE:给出的所有元素都大于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
25
26
27
28
29
30
31
32
33
34
35
public static int minNumberInRotatedArray(int[] arr){
//错误情况
if (arr == null || arr.length < 1)
return 0;
if (arr.length == 1)
return arr[0];
int l = 0, r = arr.length - 1;
int mid = l + (r - l) / 2;
//如果为顺序,返回第一个
if (arr[l] < arr[r])
return arr[l];
//如果三个相等,顺序查找
if (arr[l] == arr[mid] && arr[mid] == arr[r]){
for (int i = l+1; i <= r; i++) {
if (arr[i] < arr[i-1])
return arr[i];
}
return arr[l];
}
//二分模板
while (l < r){
//base case
//r为右边的递增数组开头,l为左边递增数组末尾
if (r - l == 1)
return arr[r];
mid = l + (r - l) / 2;
//若左边有序,往右边找
if (arr[l] <= arr[mid]){
l = mid;
}else {
r = mid;
}
}
return arr[l];
}

在排序数组中查找数字出现个数

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

输入: nums = [5,7,7,8,8,10], target = 8
输出: 2

思路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
public static int search1(int[] nums, int target) {
//简单版,找到一个后,往左右两边找
if (nums == null || nums.length < 1 || nums[0] > target || nums[nums.length - 1] < target)
return 0;
//二分
int l = 0, r = nums.length - 1;
int mid = 0;
while (l < r){
mid = l + ((r-l) >> 1);
if (nums[mid] < target){
l = mid + 1;
}else {
r = mid;
}
}
if (nums[l] != target)
return 0;
int p1 = l, p2 = l;
while (p1 - 1 >= 0 && nums[p1-1]==nums[p1])
p1--;
while (p2 + 1 < nums.length && nums[p2+1]==nums[p2])
p2++;
return p2-p1+1;
}

思路2:两次二分,查找左,右边界。找左边界的思路是:mid为左中位数,因为要让右边的r不断向左收缩,避免选右中位数导致mid与r相同 ,无法收缩。而遇到mid与tar相同时,不返回,而是让r等于mid。当mid处的值小于tar的时候,让l=mid+1。

在找右边界的时候,选择用右中位数,因为要让左边界不断往右收缩,使用左中位数,会让mid=l,无法向右收缩。在遇到mid=tar的时候,不返回,而是让l=mid。当mid处的值比tar大的时候,让r=mid-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
public static int search2(int[] nums, int target) {
//找到左右边界
if (nums == null || nums.length < 1 || nums[0] > target || nums[nums.length - 1] < target)
return 0;
//二分
int l = 0, r = nums.length - 1;
int mid = 0;
while (l < r){
mid = l + ((r-l) >> 1);
//找左边界,让mid等于左中位数,避免右边无法收缩
if (nums[mid] == target)
r = mid;
if (nums[mid] < target){
l = mid + 1;
}else{
r = mid;
}
}
if (nums[l] != target)
return 0;
int p1 = l;
l = 0;
r = nums.length - 1;
while (l < r){
//找右边界,让mid等于右中位数,避免左边无法收缩
mid = l + ((r-l + 1) >> 1);
//左边界往右收缩
if (nums[mid] == target)
l = mid;
if (nums[mid] > target){
r = mid - 1;
}else{
l = mid;
}
}
return l - p1 + 1;
}

0-n-1中缺失的数字

一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。

输入: [0,1,3]
输出: 2

思路:有序,就想到二分,如果当前位置的数比其下标大,把右边收缩至mid,不然让左边等于mid+1。如果arr[l]=l,说明l在最后一个,缺失的是n

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//有序,用二分
public static int missingNumber(int[] nums) {
if (nums == null || nums.length < 1)
return 0;
int l = 0, r = nums.length - 1;
int mid = 0;
while (l < r){
mid = l + ((r-l)>>>1);
if (nums[mid] > mid){
r = mid;
}else {
l = mid + 1;
}
}
//要是l在最后一个,缺失的是n
if (l == nums[l])
return nums.length;
return l;
}

动态规划

剪绳子

描述:给你一根长度为n的绳子,请把绳子剪成整数长的m段(m、n都是整数,n>1并且m>1),每段绳子的长度记为k[0],k[1],…,k[m]。请问k[0]xk[1]x…xk[m]可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。

思路:如果长度小于等于3的,必须要切,单独给出结果,如果长度为4的,可以切成1,3,也可以切成2,2,这样得到的递归公式为f(n)=max(f(i)*f(n-i)),拆分为小问题解决。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public int cutRope(int target) {
//数据不和规格的情况
if (target < 0)
return 0;
//如果是1-3的情况,只能再分,对应的数字较小
if (target <= 2)
return 1;
if (target == 3)
return 2;
//如果是大于3的情况,1,2,3可以不用再分,这时候值会更大
int[] dp = new int[target+1];
dp[1] = 1;
dp[2] = 2;
dp[3] = 3;
for (int i = 4; i <= target; i++) {
int res = 0;
for (int j = 1; j <= i / 2; j++) {
res = Math.max(res,dp[j] * dp[i - j]);
}
dp[i] = res;
}
return dp[target];
}

二进制

二进制的用法:

&:

  • 将所有位清零 n & 0
  • 取指定位置上的数,如取二进制的后四位,n & 00001111

|:

  • 将某些位数置为1:n | 00001111

^

  • 将某些位置取反:n ^ 00001111,将后四位取反
  • 保留原值:n ^ 0
  • 交换两个数:a=a^b;b=a^b;a=a^b,完成a与b的交换

1的个数

输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。

如9的二进制为1001,得到的输出为2.

方法一:逐位相与。一个数为1的地方,与1与不为0,统计这个数每个位都与1相与,不为0的次数即可。有两种选择,一种是右移这个数,一种是左移1。如果是右移这个数,需要使用无符号位右移,因为如果是带符号的,对于负数会有死循环。而左移1,需要进行32次判断。

需要注意的是n&1结果不为0,进行统计。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
   //将要计算的数右移
public static int NumberOf1_Solution1(int n) {
int count = 0;
while (n != 0){
if ((n & 1) != 0)
++count;
n = n >>> 1;
}
return count;
}
//将1左移
//看有多少个1,可以每一位与1相与,然后统计
public static int NumberOf1_Solution2(int n) {
int count = 0;
int flag = 1;
//计算32次
while (flag != 0){
if ((n & flag) != 0)
++count;
flag = flag << 1;
}
return count;
}

方法二:将当前数与减去1后的数相与,得到的数会将最右侧的1变为0,这样有几个1进行几次操作,最后会得到0。

1
2
3
4
5
6
7
8
9
10
public static int NumberOf1_Solution3(int n) {
//将一个数和其减一后的数相与,会把最右边为1的数后均变为0
//有几个1,进行几次这样的操作
int count = 0;
while (n != 0){
++count;
n = n & (n-1);
}
return count;
}

扩展:用一条语句判断整数是不是2的整数次方,如果是,则只有一个数为1,那么将其与减去1的数相与,如果得到0,说明只有一个1。

输入两个数m与n,统计需要改变m的二进制中多少个数才能得到n

  • 得到m与n的异或,不同的位置为1
  • 统计1的个数

数学

数值的整数次方

实现函数double Power(double base, int exponent),求base的exponent次方。不得使用库函数,同时不需要考虑大数问题。

思路:需要考虑指数为整数,负数,及0的情况,同时要区分下base是不是0。0的负数次方是没有意义的。

求指数的时候,可以用递归的方式,如果是偶数,f(n)=f(n/2)*f(n/2),如果是奇数,f(n)=f(n-1)/2*f(n-1)/2*base

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
//自定义power函数
public double power(double base, int exponent) {
double res = 0;
//分三种情况,指数大于0,小于0,等于0的情况
if (exponent > 0){
//大于0正常计算
res = getPower(base,exponent);
}else if (exponent < 0){
//小于0,看是否为0,
if (base == 0){
//0的负数次方没有意义
return 0;
}else {
res = 1 / getPower(base,-exponent);
}
}else {
//指数为0,返回1
res = 1;
}
return res;
}
private double getPower(double base, int exponent) {
//递归解决
if (exponent == 0)
return 1;
if (exponent == 1)
return base;
double res = getPower(base,exponent >> 1);
res *= res;
//如果是奇数,再乘上base
if ((exponent & 1) == 1)
res *= base;
return res;
}

1-n整数中1出现的个数

求出1-13的整数中1出现的次数,1~13中包含1的数字有1、10、11、12、13因此共出现6次。

思路:从1遍历到n,分别取每个数的每一位数字,如果有1,就加上1。思路清晰简单,但是时间复杂度高。

求一个数字每一个位数的数字的规律是,当前数%10,求得低位,然后当前数/10。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public int NumberOf1Between1AndN_Solution(int n) {
//直观解法,遍历每一个数,如果有1,+1,如果没有,+0
if (n <= 0)
return 0;
int count = 0;
for (int i = 1; i <= n; i++) {
count += process(i);
}
return count;
}
//判断当前数是否含有1
private int process(int i) {
int count = 0;
while (i > 0) {
if (i % 10 == 1) {
count++;
}
i /= 10;
}
return count;
}

方法二:找规律

来源于https://www.cnblogs.com/yongh/p/9947165.html

分别取得当前位置的数,前面的数与后面的数。

对于整数n,我们将这个整数分为三部分:当前位数字cur,更高位数字high,更低位数字low,如:对于n=21034,当位数是十位时,cur=3,high=210,low=4。

  我们从个位到最高位 依次计算每个位置出现1的次数:

  1)当前位的数字等于0时,例如n=21034,在百位上的数字cur=0,百位上是1的情况有:00100-00199,01100-01199,……,20100-20199。一共有21x100种情况,即00-20,共21种,highx100;

  2)当前位的数字等于1时,例如n=21034,在千位上的数字cur=1,千位上是1的情况有:01000-01999,11000-11999,21000-21034。一共有2x1000+(34+1)种情况,即highx1000+(low+1)。

  3)当前位的数字大于1时,例如n=21034,在十位上的数字cur=3,十位上是1的情况有:00010-00019,……,21010-21019。一共有(210+1)x10种情况,即(high+1)x10。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//数学规律解法
public int NumberOf1Between1AndN_Solution2(int n) {
if (n <= 0)
return 0;
int low = 0, high = 0, cur = 0;
int count = 0;
//看每一位
for (int i = 1; i <= n; i*=10) {
high = n / (i * 10);
low = n % i;
cur = (n / i) % 10;
if (cur == 0){
count += high * i;
}else if (cur == 1){
count += high * i + low + 1;
}else {
count += (high + 1) * i;
}
}
return count;
}

不用加减乘除做加法

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

思路:编码里面提到的利用门电路来实现加法,将加法拆分为无进位加法与进位,利用位运算来完成

a b 无进位加法n 进位c
0 0 0 0
0 1 1 0
1 0 1 0
1 1 0 1

可以看出无进位加法是a^b,而进位是a&b再左移一位,这样a+b变为n+c,这样循环进行,直到没有进位为止

1
2
3
4
5
6
7
8
9
public int add(int a, int b) {
//模拟计算机相加的过程,分为无进位相加与进位,当没有进位的时候返回
while(b != 0){
int c = (a & b) << 1;
a ^= b;
b = c;
}
return a;
}

leetcode

数组

连续的子数组和

给定一个包含非负数的数组和一个目标整数 k,编写一个函数来判断该数组是否含有连续的子数组,其大小至少为 2,总和为 k 的倍数,即总和为 n*k,其中 n 也是一个整数。

输入: [23,2,4,6,7], k = 6
输出: True
解释: [2,4] 是一个大小为 2 的子数组,并且和为 6。

方法一:暴力法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public boolean checkSubarraySum1(int[] nums, int k) {
if (nums == null || nums.length < 2)
return false;
int sum = 0;
for (int i = 0; i < nums.length - 1; i++) {
sum = nums[i];
for (int j = i + 1; j < nums.length; j++) {
sum += nums[j];
if (k == 0) {
if (sum == 0)
return true;
} else if (sum % k == 0)
return true;
}
}
return false;
}

方法二:用哈希表加速

求数组的前缀和,如果当前的和%k余数为1,如果之前也出现过余数为1的数组和,那么第一次那个数到现在这个数之间的数组和%k肯定是0。则用哈希表判断即可。坑在于,k可能为0,只有在k!=0的时候,才对sum取余,只有在当前求出的数组和没有出现过的时候,才将其进行加入,避免这种[0,0]数组判断不出来的情况。第一次的时候,要把和为0的加入,因为如果有直接%k为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
25
26
//用哈希表优化
public boolean checkSubarraySum2(int[] nums, int k) {
if (nums == null || nums.length < 2)
return false;
HashMap map = new HashMap<>();
int sum = 0;//前缀和
map.put(0,-1);//放入余数为0的,避免第一次余数为0的进去无法判断
for (int i = 0; i < nums.length; i++) {
sum += nums[i];
//处理sum
if (k != 0){
sum = sum % k;
}
//如果sum已经在表中了
if (map.containsKey(sum)){
//如果此时距离大于1
if (i - map.get(sum) > 1){
return true;
}
}else {
//没有就加入
map.put(sum,i);
}
}
return false;
}

岛屿数量

给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。

岛屿总是被水包围,并且每座岛屿只能由水平方向或竖直方向上相邻的陆地连接形成。

此外,你可以假设该网格的四条边均被水包围。

示例 1:

输入:
11110
11010
11000
00000
输出: 1
思路:用二维的布尔类型矩阵来判断一个陆地是否被判断过,只有一个地方是陆地而且没进入过,才将岛屿数量+1,然后从当前陆地开始,将其四周所有的陆地都标记为已经进入过,最后返回岛屿数量。

优化:如果一个陆地进入过了,不用专门利用二维矩阵记录,而是直接将其值更改为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
public int numIslands(char[][] grid) {
if(grid == null || grid.length < 1){
return 0;
}
int res = 0;
for(int i = 0; i < grid.length; i++){
for(int j = 0; j < grid[0].length; j++){
if(grid[i][j] == '1'){
++res;
process(grid,i,j);
}
}
}
return res;
}
public void process(char[][] grid, int i, int j){
//base case
if(i < 0 || i > grid.length - 1 || j < 0 || j > grid[0].length - 1
|| grid[i][j] =='0'){
return;
}
grid[i][j] = '0';
process(grid,i-1,j);
process(grid,i+1,j);
process(grid,i,j-1);
process(grid,i,j+1);
return;
}

不同路径

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

网格中的障碍物和空位置分别用 1 和 0 来表示。

说明:m 和 n 的值均不超过 100。

输入:
[
[0,0,0],
[0,1,0],
[0,0,0]
]
输出: 2
解释:
3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:

  1. 向右 -> 向右 -> 向下 -> 向下
  2. 向下 -> 向下 -> 向右 -> 向右

解题思路:动态规划

经典的dp问题,可以采用路径压缩,先把第一排填充,如果当前位置是0,则等于左边;然后填充其他行,第一个位置等于上一个,其他位置等于左边加上面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public int uniquePathsWithObstacles(int[][] arr) {
if(arr == null || arr[0].length < 1 || arr[0][0] == 1)
return 0;
int[] dp = new int[arr[0].length];
// 检测第一排
dp[0] = 1;
int row = arr.length, col = arr[0].length;
for (int i = 1; i < col; i++) {
dp[i] = arr[0][i] == 1 ? 0 : dp[i-1];
}
// 看其他排的
for (int i = 1; i < row; i++) {
dp[0] = arr[i][0] == 1 ? 0 : dp[0];
for (int j = 1; j < col; j++) {
dp[j] = arr[i][j] == 1 ? 0 : dp[j] + dp[j-1];
}
}
return dp[dp.length-1];
}

跳水板

你正在使用一堆木板建造跳水板。有两种类型的木板,其中长度较短的木板长度为shorter,长度较长的木板长度为longer。你必须正好使用k块木板。编写一个方法,生成跳水板所有可能的长度。

返回的长度需要从小到大排列。

输入:
shorter = 1
longer = 2
k = 3
输出: {3,4,5,6}

思路:列出最小值,最大值,等差数列

需要注意边界情况,如果k为0,返回空数组,如果shorter与longer相等,数组长度为1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public int[] divingBoard(int shorter, int longer, int k) {
if(k == 0)
return new int[]{};
if(shorter == longer){
return new int[]{shorter*k};
}
int min = shorter * k, max = longer * k, gap = longer-shorter;
int[] arr = new int[(max-min)/gap+1];
int index = 0;
for(int i = min; i <= max; i += gap){
arr[index++] = i;
}
return arr;
}

计算右侧小于当前元素的个数

给定一个整数数组 nums,按要求返回一个新数组 counts。数组 counts 有该性质: counts[i] 的值是 nums[i] 右侧小于 nums[i] 的元素的数量。

示例:

输入: [5,2,6,1]
输出: [2,1,1,0]
解释:
5 的右侧有 2 个更小的元素 (2 和 1).
2 的右侧仅有 1 个更小的元素 (1).
6 的右侧有 1 个更小的元素 (1).
1 的右侧有 0 个更小的元素.

思路:merge sort

看到求数组左、右侧,有多少元素比当前元素大、小的问题,本质都是逆序和问题,那么可以靠merge sort来解决,但是merge sort会改变数组元素的位置,那么直接交换数组的位置就没法输出想要的结果,这时候想到,在排序的时候,不交换原数组元素的位置,而是使用一个下标数组,每次交换只交换索引的位置,这样可以保证原来的数组位置不变。然后就是采用顺序还是逆序排序的问题,如果采用的是顺序,那么假设左边数组为5,6,右边数组为1,2,当比较5与1时,5比1大,增加5对应索引结果个数,下一个比较2,这时候因为1也比6小,此时就会让6的结果漏算。这时候应该采用逆序,即左边为6,5,右边为2,1,当比较时,6比2大,6对应结果+2,下一个左边比较的是5,这样5对应结果+2,此时不会漏算。

大体思路是先merge,即先把数组一直对半分,边界为l>=r,此时结束,merge完毕后进行处理,采用外排的思路,用辅助数组来排序,采用左右指针,当左边指针对应结果更大,记录结果个数,移动左边指针;当右边指针对应结果更大,直接移动右边指针。两个指针都移动到最后以后,将辅助数组的结果赋值给索引数组。

其中要注意的点是,比较的时候,为num[index[i]],而不是直接比较index[i],index[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
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
class Solution {
private Integer[] res;
private int[] index;
private int[] help;
public List countSmaller(int[] nums) {
if (nums == null || nums.length < 1)
return new ArrayList<>();
// init
int n = nums.length;
res = new Integer[n];
index = new int[n];
help = new int[n];
for (int i = 0; i < n; i++) {
index[i] = i;
res[i] = 0;
}
merge(nums,0,n-1);
return Arrays.asList(res);
}

private void merge(int[] nums, int l, int r) {
if (l >= r)
return;
int mid = (l + r) >>> 1;
merge(nums,l,mid);
merge(nums,mid+1,r);
process(nums,l,mid,r);
}

// 按照从大到小的顺序排
private void process(int[] nums, int l, int mid, int r) {
int p1 = l, p2 = mid + 1, p3 = l;
while (p1 <= mid && p2 <= r){
if (nums[index[p1]] <= nums[index[p2]]){
help[p3++] = index[p2++];
}else {
res[index[p1]] += r-p2+1;
help[p3++] = index[p1++];
}
}
while (p1 <= mid){
help[p3++] = index[p1++];
}
while (p2 <= r){
help[p3++] = index[p2++];
}
// 赋值
for (int i = l; i <= r; i++) {
index[i] = help[i];
}
}
}

缺失的第一个正数

给你一个未排序的整数数组,请你找出其中没有出现的最小的正整数。

示例 1:

输入: [1,2,0]
输出: 3
示例 2:

输入: [3,4,-1,1]
输出: 2

你的算法的时间复杂度应为O(n),并且只能使用常数级别的额外空间。

思路:如果没有空间复杂度的要求,可以将正数元素存入哈希表,然后从1开始判断元素是否在哈希表中。但是因为有空间复杂度的限制,因此不能这么做。换一种思路想,如果长度为N的数组,元素1-N都有,那么缺失的元素是N+1,否则缺失的元素一定在1-N之间,那么如果可以出现一个1-N之间的元素,将其对应位置上的数做个标记,比如变成负数,之后再从头开始判断,哪个位置不是负数则那个位置就缺失元素。但是如果之前就是负数或者0怎么办,这时候可以将非正数变成一个不在1-N之间的数,比如N+1,以此来排除干扰。

总的来说,需要3轮遍历,第一轮排除干扰,将非负数变成N+1;第二轮,进行标记,将1-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 int firstMissingPositive(int[] nums) {
// 最好情况下,1-N都有,缺失N+1
// 将1-N的数将其位置坐标记,变为负数
// 原本的非正数。变为N+1
if(nums == null || nums.length < 1)
return 1;
// 第一次遍历,把非正数变成N+1
int n = nums.length;
for(int i = 0; i < n; i++){
if(nums[i] < 1){
nums[i] = n+1;
}
}
// 第二次遍历,做标记
for(int i = 0; i < n; i++){
int temp = Math.abs(nums[i]);
if(temp <= n){
// 将对应位置的数变为负数
nums[temp-1] = - Math.abs(nums[temp-1]);
}
}
// 第三次遍历,从0开始,哪个位置不是负数,返回
for(int i = 0; i < n; i++){
if(nums[i] > 0)
return i+1;
}
return n+1;
}

分割数组的最大值

给定一个非负整数数组和一个整数 m,你需要将这个数组分成 m 个非空的连续子数组。设计一个算法使得这 m 个子数组各自和的最大值最小。

注意:
数组长度 n 满足以下条件:

1 ≤ n ≤ 1000
1 ≤ m ≤ min(50, n)
示例:

输入:
nums = [7,2,5,10,8]
m = 2

输出:
18

思路:二分

二分的精髓在于找左右两边的极限情况,然后舍弃一半的可能性

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 int splitArray(int[] nums, int m) {
// 二分查找m个数
// 最多的分法,m=nums.length,每个数组都分一下,最大值是max(nums)
// 最少的分法,m=1,最大值是sum(nums)
// 则二分查找最大子数组和,如果最后找到的子数组个数比m大,说明子数组和小了;反之,说明子数组和大了
// 计算数组最大值与和
long l = 0, r = 0;
for(int num : nums){
l = Math.max(l,num);
r += num;
}
// 开始二分
long mid = 0;
while(l < r){
mid = l + (r - l) / 2;
long sum = 0;
int count = 0;
// 计算子数组和与数组个数
for(int num : nums){
sum += num;
// 如果和大于当前子数组和,计数,重置和为当前数,表示从当前开始算
if(sum > mid){
++count;
sum = num;
}
}
// 计算最后一次
++count;
if(count > m){
l = mid + 1;
}else{
r = mid;
}
}
return (int)l;
}

数组中两个最大的异或值

给定一个非空数组,数组中元素为 a0, a1, a2, … , an-1,其中 0 ≤ ai < 231 。

找到 ai 和aj 最大的异或 (XOR) 运算结果,其中0 ≤ i, j < n 。

你能在O(n)的时间解决这个问题吗?

输入: [3, 10, 5, 25, 2, 8]

输出: 28

解释: 最大的结果是 5 ^ 25 = 28.

思路:使用前缀树来解决,保存每个数字的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
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
class Solution {
Node root;
int L;
int res;
public int findMaximumXOR(int[] nums) {
if(nums == null || nums.length < 1)
return 0;
init(nums);// 构造前缀树
process(nums);// 计算
return res;
}
public void init(int[] nums){
// 计算最高位
int max = Integer.MIN_VALUE;
for(int num : nums){
max = Math.max(max,num);
}
// 得到位数
L = Integer.toBinaryString(max).length();
root = new Node();
// 遍历每个数构造树
for(int num : nums){
// 计算每一位
Node cur = root;
for(int i = L - 1; i >= 0; i--){
int nextIndex = (num >>> i) & 1;
// 看是否存在
if(cur.next[nextIndex] == null){
cur.next[nextIndex] = new Node();
}
cur = cur.next[nextIndex];
}
}
}
public void process(int[] nums){
res = Integer.MIN_VALUE;
// 遍历数组,找最大可能性
for(int num : nums){
Node cur = root;
int sum = 0;
// 计算当前的
for(int i = L - 1; i >= 0; i--){
int curIndex = (num >>> i) & 1;
int nextIndex = curIndex ^ 1;
if(cur.next[nextIndex] != null){
sum += 1 << i;
cur = cur.next[nextIndex];
}else{
cur = cur.next[curIndex];
}
}
// 更新max
res = Math.max(res,sum);
}
}
}
class Node{
Node[] next = new Node[2];
public Node(){}
}

旋转矩阵

给你一幅由 N × N 矩阵表示的图像,其中每个像素的大小为 4 字节。请你设计一种算法,将图像旋转 90 度。

不占用额外内存空间能否做到?

给定 matrix =
[
[1,2,3],
[4,5,6],
[7,8,9]
],

原地旋转输入矩阵,使其变为:
[
[7,4,1],
[8,5,2],
[9,6,3]
]

思路:两次反转,旋转等价于,先水平翻转,再对着对角线进行翻转,这样就可以实现旋转的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void rotate(int[][] matrix) {
// 水平翻转
int m = matrix.length, n = matrix[0].length;
for(int i = 0; i < (m+1)/2;i++){
for(int j = 0; j < n; j++){
swap(matrix,i,j,m-i-1,j);
}
}
for(int i = 0; i < m;i++){
for(int j = i+1; j < n; j++){
swap(matrix,i,j,j,i);
}
}
}
public void swap(int[][] matrix, int i1, int j1, int i2, int j2){
int temp = matrix[i1][j1];
matrix[i1][j1] = matrix[i2][j2];
matrix[i2][j2] = temp;
}

被围绕的区域

给定一个二维的矩阵,包含 ‘X’ 和 ‘O’(字母 O)。

找到所有被 ‘X’ 围绕的区域,并将这些区域里所有的 ‘O’ 用 ‘X’ 填充。

示例:

X X X X
X O O X
X X O X
X O X X
运行你的函数后,矩阵变为:

X X X X
X X X X
X X X X
X O X X
解释:

被围绕的区间不会存在于边界上,换句话说,任何边界上的 ‘O’ 都不会被填充为 ‘X’。 任何不在边界上,或不与边界上的 ‘O’ 相连的 ‘O’ 最终都会被填充为 ‘X’。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。

思路:深度遍历,如果直接在中间dfs O比较麻烦,这时候发现边界的O是不被影响的,所以可以把所有的和边界的O相连的O给染色,这样剩下的O就是不可达,改成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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
int n;
int m;
public void solve(char[][] board) {
if(board == null || board.length < 1){
return;
}
n = board.length;
m = board[0].length;
// 遍历四周
for(int i = 0;i < n; i++){
dfs(board,i,0);
dfs(board,i,m-1);
}
for(int i = 0;i < m; i++){
dfs(board,0,i);
dfs(board,n-1,i);
}
// 遍历所有
for(int i = 0; i < n; i++){
for(int j = 0;j < m; j++){
if(board[i][j] == 'O'){
board[i][j] = 'X';
}else if(board[i][j] == 'A'){
board[i][j] = 'O';
}
}
}
}
public void dfs(char[][] board, int i, int j){
// base case
if(i < 0 || i >= n || j < 0 || j >= m || board[i][j] != 'O'){
return;
}
// 改变
board[i][j] = 'A';
dfs(board,i-1,j);
dfs(board,i+1,j);
dfs(board,i,j-1);
dfs(board,i,j+1);
}

解数独

编写一个程序,通过已填充的空格来解决数独问题。

一个数独的解法需遵循如下规则:

数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。
空白格用 ‘.’ 表示。

思路:本质就是枚举,从1到9,看当前的数字是否能放在这个位置,如果不行就回溯。需要记录当前数在这一行,这一列,这个3x3的格子中有没有出现。然后遍历数组,如果当前为空格,记录下棋x,y坐标,如果是数字,记录其出现的行,列及3x3格子中的位置。记录完成后,从第一个空格处开始枚举,base case是如果所有空格都处理完成,返回。然后处理当前的,获取到当前的空格位置,从1-9遍历数字,如果这个数字没有出现过,将其标记为出现,在空格处填充这个数字,然后进行下一个位置的处理,回溯完后,将状态改变回来。

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
// 数字在当前行是否出现
boolean[][] row = new boolean[9][9];
// 数字在当前列是否出现
boolean[][] col = new boolean[9][9];
// 数字在当前九宫格是否出现
boolean[][][] sudu = new boolean[3][3][9];
// 是空格的地方
List<int[]> list = new ArrayList<>();
boolean valid = false;
public void solveSudoku(char[][] board) {
// 遍历数组,进行状态填充
for(int i = 0; i < 9; i++){
for(int j = 0; j < 9; j++){
if(board[i][j] == '.'){
list.add(new int[]{i,j});
}else{
int num = board[i][j] - '0' - 1;
row[i][num] = col[j][num] = sudu[i / 3][j / 3][num] = true;
}
}
}
dfs(board,0);
}
public void dfs(char[][] board, int index){
// base case
if(index == list.size()){
valid = true;
return;
}
// 取出当前位置
int[] now = list.get(index);
int curR = now[0];
int curC = now[1];
// 从1-9遍历
for(int i = 0; i < 9 && !valid; i++){
// 如果行没出现,列没出现,九宫格中没出现
if(!row[curR][i] && !col[curC][i] && !sudu[curR/3][curC/3][i]){
// 出现过
row[curR][i] = col[curC][i] = sudu[curR/3][curC/3][i] = true;
board[curR][curC] = (char)(i + '0' + 1);
// 处理下一个
dfs(board,index+1);
// 回溯
row[curR][i] = col[curC][i] = sudu[curR/3][curC/3][i] = false;
}
}
}

颜色分类

给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。

注意:
不能使用代码库中的排序函数来解决这道题。

示例:

输入: [2,0,2,1,1,0]
输出: [0,0,1,1,2,2]

直观的解决思路是:分别统计0,1,2的个数,然后按顺序去填充即可。

但这样需要两次遍历。按照0,1,2的顺序,数组实际上被分成了3块区域,这样其实是荷兰国旗问题,因此可以使用快排的思路去解决。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void sortColors(int[] nums) {
// 荷兰国旗
int less = -1, more = nums.length;
int cur = 0;
while(cur < more){
if(nums[cur] == 0){
swap(nums,++less,cur++);
}else if(nums[cur] == 2){
swap(nums,--more,cur);
}else{
cur++;
}
}
}
public void swap(int[] nums, int i, int j){
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}

根据身高重建队列

假设有打乱顺序的一群人站成一个队列。 每个人由一个整数对 (h, k) 表示,其中 h 是这个人的身高,k 是应该排在这个人前面且身高大于或等于 h 的人数。 例如:[5,2] 表示前面应该有 2 个身高大于等于 5 的人,而 [5,0] 表示前面不应该存在身高大于等于 5 的人。

编写一个算法,根据每个人的身高 h 重建这个队列,使之满足每个整数对 (h, k) 中对人数 k 的要求。

输入:[[7,0], [4,4], [7,1], [5,0], [6,1], [5,2]]
输出:[[5,0], [7,0], [5,2], [6,1], [4,4], [7,1]]

思路:对于高的人来说,站在他后面的人不对他的k造成影响,站的矮的人在他前面不对k造成影响,因此把高的人排好后,矮的人站在哪里都对之前的结果没有影响,所以先按照高矮排序,如果身高相同,就按照k排序,因为k大的人需要后排。然后按照排序的顺序,把这个人放到第k个,这样他前面的都是比他高的,而且后面不会有更高的人,所以后面的人就算排到了他前面也没有影响。

LinkedList可以存放int[]类型的数据

LinkedList public void add(int index, E element)可以将元素放到指定的位置

LinkedList public T[] toArray(T[] a) 返回一个由链表元素转换类型而成的数组。

1
2
3
4
5
6
7
8
9
10
public int[][] reconstructQueue(int[][] people) {
// 按照从高到矮排列,要是身高相同,排在前面人数更多的在后面
Arrays.sort(people, (o1, o2)-> o1[0] != o2[0] ? o2[0] - o1[0] : o1[1] - o2[1]);
List<int[]> list = new LinkedList<>();
// 将这个数放到第k个,因为后面都是比他矮的,所以放到后面本来就没影响,放到前面因为比较矮也没影响
for(int[] i : people){
list.add(i[1], i);
}
return list.toArray(new int[list.size()][2]);
}

区间合并问题

区间合并问题,即是给出一系列区间,看多少区间是不相交的,基本思路是按照数组的end排序,如果当前的start小于之间的end,说明这两个区间是相交的,如果start大于end,说明是不相交的。

之所以用右边来排序,而不是用左边来排序,是因为有可能虽然左边很小,但是右边很大区间很长,类似于做项目,应该优先选先结束的,所以用右边来排序。

核心思路是将大问题划分为子问题,先选出一个区间,然后剩下的区间都是与当前区间不相交的部分,总的不相交区间数量等于当前区间数+剩下区间数,而剩下的区间问题与当前子问题相同,因此是进行了规模的削减,而解决思路相同。

用最少数量的箭引爆气球

在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以纵坐标并不重要,因此只要知道开始和结束的横坐标就足够了。开始坐标总是小于结束坐标。

一支弓箭可以沿着 x 轴从不同点完全垂直地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。

给你一个数组 points ,其中 points [i] = [xstart,xend] ,返回引爆所有气球所必须射出的最小弓箭数。

输入:points = [[10,16],[2,8],[1,6],[7,12]]
输出:2
解释:对于该样例,x = 6 可以射爆 [2,8],[1,6] 两个气球,以及 x = 11 射爆另外两个气球

思路:先让气球从左到右排列,如果一个箭可以击穿一个气球,那就可以让这个箭到此气球的最右边,然后看可以打到其他哪些气球,如果打不到了,换一个箭。因此让气球按照右边坐标从小到大排列,如果下一个气球的开始位置在上一个气球的左边则也可以被打到,否则打不到。

为了避免越界,此处避免使用o1[1] - o2[1]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public int findMinArrowShots(int[][] points) {
if(points == null || points.length < 1){
return 0;
}
// 直接写可能会越界
//Arrays.sort(points,(o1, o2)->o1[1] - o2[1]);
Arrays.sort(points,(o1, o2)->o1[1] < o2[1] ? -1 : 1);
int res = 1;
long pre = points[0][1];
for(int i = 1; i < points.length; i++){
if(points[i][0] > pre){
++res;
pre = points[i][1];
}
}
return res;
}

无重叠区间

给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。

注意:

可以认为区间的终点总是大于它的起点。
区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。

输入: [ [1,2], [2,3], [3,4], [1,3] ]

输出: 1

解释: 移除 [1,3] 后,剩下的区间没有重叠。

思路:先按照数组的end排序,然后看有多少区间是没有重叠的,用n减去无重叠区间数量,就是要移除的区间数量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public int eraseOverlapIntervals(int[][] intervals) {
// 计算不重叠区间的个数
if(intervals == null || intervals.length < 2){
return 0;
}
// 按照右边升序
Arrays.sort(intervals,(o1,o2)->o1[1] - o2[1]);
int count = 1;
int n = intervals.length;
int pre = intervals[0][1];
for(int[] interval : intervals) {
if(interval[0] >= pre) {
// 更新
count++;
pre = interval[1];
}
}
return n - count;
}

上升下降字符串

给你一个字符串 s ,请你根据下面的算法重新构造字符串:

从 s 中选出 最小 的字符,将它 接在 结果字符串的后面。
从 s 剩余字符中选出 最小 的字符,且该字符比上一个添加的字符大,将它 接在 结果字符串后面。
重复步骤 2 ,直到你没法从 s 中选择字符。
从 s 中选出 最大 的字符,将它 接在 结果字符串的后面。
从 s 剩余字符中选出 最大 的字符,且该字符比上一个添加的字符小,将它 接在 结果字符串后面。
重复步骤 5 ,直到你没法从 s 中选择字符。
重复步骤 1 到 6 ,直到 s 中所有字符都已经被选过。
在任何一步中,如果最小或者最大字符不止一个 ,你可以选择其中任意一个,并将其添加到结果字符串。

请你返回将 s 中字符重新排序后的 结果字符串 。

1
2
3
4
5
6
7
输入:s = "aaaabbbbcccc"
输出:"abccbaabccba"
解释:第一轮的步骤 1,2,3 后,结果字符串为 result = "abc"
第一轮的步骤 4,5,6 后,结果字符串为 result = "abccba"
第一轮结束,现在 s = "aabbcc" ,我们再次回到步骤 1
第二轮的步骤 1,2,3 后,结果字符串为 result = "abccbaabc"
第二轮的步骤 4,5,6 后,结果字符串为 result = "abccbaabccba"

思路1:使用大根堆和小根堆,从大根堆中弹出元素到小根堆,要是有比之前元素小的就存下来,然后从小根堆中弹出到大根堆,直到两个堆都为空。

思路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
public String sortString(String s) {
if(s == null || s.length() < 2){
return s;
}
int[] arr = new int[26];
for(int i = 0; i < s.length(); i++){
arr[s.charAt(i) - 'a']++;
}
StringBuilder sb = new StringBuilder();
while(sb.length() < s.length()){
for(int i = 0; i < arr.length; i++){
if(arr[i] > 0){
sb.append((char)(i + 'a'));
arr[i]--;
}
}
for(int i = arr.length - 1; i >= 0; i--){
if(arr[i] > 0){
sb.append((char)(i + 'a'));
arr[i]--;
}
}
}
return sb.toString();
}

分发饼干

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。

对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

输入: g = [1,2,3], s = [1,1]
输出: 1
解释:
你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
所以你应该输出1。

思路:把孩子胃口和饼干尺寸排序,优先满足小胃口的孩子,找到第i个孩子能满足的饼干,然后找第i+1个,直到所有孩子满足了或者饼干不够了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int i = 0, j = 0;
int n1 = g.length, n2 = s.length;
int res = 0;
while(i < n1 && j < n2){
// 找给第i给孩子吃的
while(j < n2 && s[j] < g[i]){
j++;
}
if(j >= n2){
break;
}
res++;
i++;
j++;
}
return res;
}

只出现一次的数字

给定一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。

输入: [1,2,1,3,2,5]
输出: [3,5]

思路:将所有的数进行异或,这样出现两次的数信息会消失,然后只留下出现一次的x与y的不同处的信息bitmask,这样出现1的地方一定是x或y中一个为1,一个为0。然后从bitmask中取出一个1,最低位的1比较好取。

bitmask & (-bitmask)或者bitmask ^ (bitmask & (bitmask - 1))

这样,当其他位数的1 &取反后的0,结果为0,只有最低位的1,取反后+1,最低位的1相与后还是1,如1100,取反后+1位0100,相与后为0100。这个结果为diff。然后遍历所有数,如果这个数&diff不为0,说明可以对x做贡献,x^num,这样只会有出现两次的数与x才会被留下,这样就将x与y尽心了区分,x ^ bitmask就是y

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public int[] singleNumber(int[] nums) {
int bitmask = 0;
for(int num : nums) {
bitmask ^= num;
}
// 取出最右边的1
int diff = 0;
diff = bitmask & (-bitmask);
int x = 0;
for(int num : nums) {
if((num & diff) != 0) {
x ^= num;
}
}
return new int[]{x, bitmask ^ x};
}

尽可能使字符串相等

给你两个长度相同的字符串,s 和 t。

将 s 中的第 i 个字符变到 t 中的第 i 个字符需要 |s[i] - t[i]| 的开销(开销可能为 0),也就是两个字符的 ASCII 码值的差的绝对值。

用于变更字符串的最大预算是 maxCost。在转化字符串时,总开销应当小于等于该预算,这也意味着字符串的转化可能是不完全的。

如果你可以将 s 的子字符串转化为它在 t 中对应的子字符串,则返回可以转化的最大长度。

如果 s 中没有子字符串可以转化成 t 中对应的子字符串,则返回 0。

输入:s = “abcd”, t = “bcdf”, cost = 3
输出:3
解释:s 中的 “abc” 可以变为 “bcd”。开销为 3,所以最大长度为 3。

思路:双指针,首先计算s与t的每个位置的差值,设置指针start跟end,使得start与end之间的值不大于开销,然后更新长度。end可以从0到数组最后,如果start-end之间和大了,就让start后移,然后计算并更新长度。

这样可以保证均以end结尾,且不用重复进行计算。移除的是窗口前面的值,当前窗口值可以用于下一个窗口。

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
func equalSubstring(s string, t string, maxCost int) int {
n := len(s)
arr := make([]int, n)
for i := 0; i < n; i++ {
arr[i] = abs(int(s[i]) - int(t[i]))
}
start := 0
res, sum := 0, 0
for end := 0; end < n; end++ {
sum += arr[end]
for sum > maxCost {
sum -= arr[start]
start++
}
res = max(res, end - start + 1)
}
return res
}
func abs(i int) int{
if i > 0 {
return i
} else {
return -i
}
}
func max(i, j int) int {
if i >= j {
return i
} else {
return j
}
}

数学

计算质数

统计所有小于非负整数 n 的质数的数量。

输入:n = 10
输出:4
解释:小于 10 的质数一共有 4 个, 它们是 2, 3, 5, 7 。

思路1:暴力,通过遍历每个数字,看这个数字是否有因数,来进行判断

思路2:埃氏筛,如果一个数是质数,那么他的倍数一定不是质数,那么对质数的倍数进行统计,将其标记为合数。刚开始的时候所有数都为质数,然后遍历,如果一个数是质数,则进行计数,且将其倍数的数设置为合数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public int countPrimes(int n) {
// 默认都是false,表示都是质数
boolean[] f = new boolean[n];
int count = 0;
for(int i = 2; i < n; i++){
// 若当前数是质数,进行染色,计数
if(!f[i]){
count++;
for(int j = 2 * i; j < n; j += i){
f[j] = true;
}
}
}
return count;
}

二分

寻找两个正序数组的中位数

给定两个大小为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。

请你找出这两个正序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n))。

你可以假设 nums1 和 nums2 不会同时为空。

nums1 = [1, 3]
nums2 = [2]

则中位数是 2.0

方法:看到时间复杂度log级别,一般为二分或者归并的思路,此处为二分,因为数组都是有序的,因此如果可以找到一个分割线,让左右两个数组的元素个数尽可能相等,则中位数是左边一块数组的最大值和右边一块数组的最小值决定的,此处定义两个数组的长度为m与n,如果m n 和为奇数,则由(m+n)/2的位置决定,如果和为偶数,则是(m+n+1)/2处两个数决定,定义在分割线的左右两个,左边元素等于右边元素或+1。这样找到分割线后,如果和为奇数,则中位数是左边数组的最大值;如果和为偶数,中位数是左边数组最大值与右边数组最小值的和除以2。

但分割线如何决定,需要有分割线左边,数均小于分割线右边。这样两个数组,第一个数组分割线左边的数要小于第二个数组分割线右边的数。第一个数组分割线右边的数要大于第二个数组分割线左边的数。这样找分割线的过程就可以用二分了,其中用二分的模板的时候,要注意中间数的取值问题,如果有死循环,就要+1再除以2。然后找到分割线后,因为分割线的定义为当前数组左边的数,值为0-m或n,因此需要处理边界条件,如果分界线左边没有数,让左边的数为最小值;如果分界线右边没有数,让右边的数为最大值(避免被max或min取到)

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 double findMedianSortedArrays(int[] nums1, int[] nums2) {
if(nums1.length > nums2.length){
return findMedianSortedArrays(nums2,nums1);
}
int m = nums1.length, n = nums2.length;
int leftSize = (m + n + 1) / 2;
int left = 0, right = m;
while(left < right){
int i = (left + right + 1) / 2;
int j = leftSize - i;
// 缩小范围
if(nums1[i-1] > nums2[j]){
right = i - 1;
}else{
left = i;
}
}
// 找四个数
int i = left;
int j = leftSize - i;
int leftNum1 = i == 0 ? Integer.MIN_VALUE : nums1[i-1];
int rightNum1 = i == m ? Integer.MAX_VALUE : nums1[i];
int leftNum2 = j == 0 ? Integer.MIN_VALUE : nums2[j-1];
int rightNum2 = j == n ? Integer.MAX_VALUE : nums2[j];
if((m+n) % 2 == 1){
// 返回左边最大的
return Math.max(leftNum1,leftNum2);
}else{
// 找左边最大的,右边最小的
return (double)((Math.max(leftNum1,leftNum2) + Math.min(rightNum1,rightNum2))/2.0);
}
}

在排序数组中查找元素的第一个和最后一个位置

给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]。

1
2
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]

思路:第一次二分,找到左边界;第二次二分找到右边界。

找左边界的时候,mid是左中位数如果当前值等于目标值,让右边界等于mid,当前值小于目标值,让左边界等于mid+1。找右边界的时候,mid是右中位数如果当前值等于目标值,让左边界等于mid,当前值大于目标值,让右边界等于mid-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
public int[] searchRange(int[] nums, int target) {
if(nums == null || nums.length < 1){
return new int[]{-1,-1};
}
int l = 0, r = nums.length - 1;
int mid = 0;
int res1 = 0, res2 = 0;
while(l < r){
mid = l + (r - l) / 2;
if(nums[mid] < target) {
l = mid + 1;
} else {
r = mid;
}
}
if(nums[l] != target){
return new int[]{-1,-1};
}
res1 = l;
l = 0;
r = nums.length - 1;
while(l < r){
mid = l + (r - l + 1) / 2;
if(nums[mid] > target) {
r = mid - 1;
} else {
l = mid;
}
}
res2 = l;
return new int[]{res1, res2};
}

dp

最长重复子数组

给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的子数组的长度。

输入:
A: [1,2,3,2,1]
B: [3,2,1,4,7]
输出:3
解释:
长度最长的公共子数组是 [3, 2, 1] 。

思路:动态规划

定义dp[i][j]的含义是a[i]到最后与b[j]到最后个数字,最长子数组的长度

然后是状态转移方程,如果a[i]=b[j],那么dp[i][j]=1+dp[i+1][j+1]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public int findLength(int[] a, int[] b) {
if(a == null || b == null)
return 0;
int[][] dp = new int[a.length+1][b.length+1];
int res = 0;
for(int i = a.length-1;i>=0;i--){
for(int j = b.length-1;j>=0;j--){
if(a[i] == b[j]){
dp[i][j] = 1 + dp[i+1][j+1];
res = Math.max(res,dp[i][j]);
}
}
}
return res;
}

买卖股票

最佳买卖股票含冷冻期

给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。

设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
示例:

输入: [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]

思路:dp

同一天有三种状态:买入,没有操作,状态,对应的,当天结束后也有三种状态,即持有股票,没有股票且在冷冻期,没有股票且不在冷冻期,因此创建二维dp数组dp[n][3],含义为

1
2
3
dp[i][0]为第i天结束后持有股票情况的最大收益
dp[i][1]为第i天结束后没有股票且不在冷冻期的最大收益
dp[i][2]为第i天结束后没有股票且在冷冻期的最大收益

然后是状态转移方程的定义

对于dp[i][0],其可以是第i-1天后持有股票收益与i-1相同,也可以是第i天购入股票(i-1天没有股票且不在冷冻期内)收益为i-1天收益减去第i天股票价格,即dp[i][0]=max(dp[i-1][0],dp[i-1][2])

对于dp[i][1],只能是第i天将股票卖出,因此收益为第i-1天收益加上第i天股票价格,即dp[i][1]=dp[i-1][0]+prices[i]

对于dp[i][2],可能是第i-1天后没有股票不在冷冻期,也可能是i-1天后没有股票且在冷冻期内(冷冻期一天,第i天后冷冻期结束),即dp[i][2]=max(dp[i-1][1],dp[i-1][2])

最后,要看的是最后天结束后的最大收益,这时候不考虑还持有股票的情况,因此要求的是max(dp[n-1][1],dp[n-1][2])

然后是边界情况,对于dp[0][0],第0天后持有股票,那么收益为0-prices[0],而dp[0][1]dp[0][2]不存在,收益表示为0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if(prices == null || prices.length < 1)
return 0;
// dp[i][0]第i天结束后持有股票
// dp[i][1]第i天结束后没有股票在冷冻期
// dp[i][2]第i天结束后没有股票,不在冷冻期
int[][] dp = new int[prices.length][3];
// 边界
dp[0][0] = -prices[0];
// 状态转移方程
// dp[i][0] = max(dp[i-1][0],dp[i-1][2]-prices[i])
// dp[i][1] = dp[i-1][0] + prices[i]
// dp[i][2] = max(dp[i-1][2],dp[i-1][1])
// 最后要max(dp[n-1][1],dp[n-1][2])
for(int i = 1; i < prices.length; i++){
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][2]-prices[i]);
dp[i][1] = dp[i-1][0] + prices[i];
dp[i][2] = Math.max(dp[i-1][1],dp[i-1][2]);
}
return Math.max(dp[prices.length-1][1],dp[prices.length-1][2]);

买卖股票的最佳时期含手续费

给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用。

你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。

返回获得利润的最大值。

注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。

思路:定义dp数组,dp[i][0]表示第i天结束后没有持有股票的最佳收益,dp[i][1]表示第i天结束后持有股票的最佳收益,状态转移方程为

1
2
3
4
// 第i-1天没有股票,或者i-1有,第i天卖出
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
// 第i-1天有股票,或者i-1没有,第i天买进
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i] - fee)

状态初值为

1
dp[0][0] = 0, dp[0][1] = -prices[0] - fee

代码为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public int maxProfit(int[] prices, int fee) {
if(prices == null || prices.length < 2){
return 0;
}
int n = prices.length;
int[][] dp = new int[n][2];
// dp[i][0]表示当天结束后没有股票的最大利益
// dp[i][0]表示当天结束后有股票的最大利益
// 初始条件
dp[0][0] = 0;
dp[0][1] = -prices[0] - fee;
for(int i = 1; i < n; i++) {
dp[i][0] = Math.max(dp[i - 1][1] + prices[i], dp[i - 1][0]);
dp[i][1] = Math.max(dp[i - 1][0] - prices[i] - fee, dp[i - 1][1]);
}
return dp[n - 1][0];
}

地下城游戏

一些恶魔抓住了公主(P)并将她关在了地下城的右下角。地下城是由 M x N 个房间组成的二维网格。我们英勇的骑士(K)最初被安置在左上角的房间里,他必须穿过地下城并通过对抗恶魔来拯救公主。

骑士的初始健康点数为一个正整数。如果他的健康点数在某一时刻降至 0 或以下,他会立即死亡。

有些房间由恶魔守卫,因此骑士在进入这些房间时会失去健康点数(若房间里的值为负整数,则表示骑士将损失健康点数);其他房间要么是空的(房间里的值为 0),要么包含增加骑士健康点数的魔法球(若房间里的值为正整数,则表示骑士将增加健康点数)。

为了尽快到达公主,骑士决定每次只向右或向下移动一步。

编写一个函数来计算确保骑士能够拯救到公主所需的最低初始健康点数。

例如,考虑到如下布局的地下城,如果骑士遵循最佳路径 右 -> 右 -> 下 -> 下,则骑士的初始健康点数至少为 7。

-2 (K) -3 3
-5 -10 1
10 30 -5 (P)

说明:

骑士的健康点数没有上限。

任何房间都可能对骑士的健康点数造成威胁,也可能增加骑士的健康点数,包括骑士进入的左上角房间以及公主被监禁的右下角房间。

思路:dp

先开始想到从上到下dp,这样需要记录的信息有最少需要的血量及当前的行走路径后的血量,而且在进行下一步的判断的时候,无法有效的确定根据哪个信息,因此考虑从下往上dp。dp[i][j]的含义为到此位置锁需要的最少生命值,对于最下一行与最右一列,其值为右边或者下面的值减去上一个房间的值与1的最大值,如果是其他位置,计算下面与右边来当前位置所需生命值中较小的,最后返回dp[0][0]位置减去当前房间值与1的较大值。

边界情况为,dp[m-1][n-1]为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
public int calculateMinimumHP(int[][] dungeon) {
if (dungeon == null || dungeon.length < 1)
return 1;
int m = dungeon.length, n = dungeon[0].length;
int[][] dp = new int[m][n];
dp[m-1][n-1] = 1;
// 最右边一列
for (int i = m-2; i >= 0; i--) {
dp[i][n-1] = Math.max(dp[i+1][n-1] - dungeon[i+1][n-1],1);
}
// 最下面一行
for (int j = n-2; j >= 0; j--) {
dp[m-1][j] = Math.max(dp[m-1][j+1] - dungeon[m-1][j+1],1);
}
// 其他位置
int right = 0, down = 0;
for (int i = m-2; i >= 0; i--) {
for (int j = n-2; j >= 0; j--) {
// 计算从右边跟下面来的血量
right = Math.max(dp[i][j+1] - dungeon[i][j+1],1);
down = Math.max(dp[i+1][j] - dungeon[i+1][j],1);
dp[i][j] = Math.min(right,down);
}
}
return Math.max(dp[0][0] - dungeon[0][0],1);
}

交错字符串

给定三个字符串 s1, s2, s3, 验证 s3 是否是由 s1 和 s2 交错组成的。

输入: s1 = “aabcc”, s2 = “dbbca”, s3 = “aadbbcbcac”
输出: true

思路:一开始可能会想到双指针,但是行不通,因为是两个字符串交错组成的,这时候需要用dp,dp一般分为两步,定义dp数组含义与写出状态转移方程

  • dp[i][j]代表s1的第1-i个与s2的第1-j个字符是否能与s3的第1-i+j个字符处匹配
  • 如果s1的第i个与s3的第i+j个字符相同,则dp[i][j]=dp[i-1][j];如果s2的第j个与s3的第i+j个字符相同,则dp[i][j]=dp[i][j-1],因此总的来看,dp[i][j]=dp[i-1][j] || dp[i][j-1]

边界条件为,dp[0][0]=true,最后要返回的结果是dp[m][n]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public boolean isInterleave(String s1, String s2, String s3) {
//dp[i][j]表示s1的第i个与s2的第j个能否与s3第i+j个匹配
int m = s1.length(), n = s2.length(), l = s3.length();
if(m + n != l)
return false;
boolean[][] dp = new boolean[m+1][n+1];
dp[0][0] = true;
// 如果s1的第i个与s3第i+j相同,dp[i][j] = dp[i-1][j],不然为false
for(int i = 0; i <= m; i++){
for(int j =0; j <= n; j++){
if(i > 0 && s1.charAt(i-1) == s3.charAt(i+j-1)){
dp[i][j] = dp[i][j] || dp[i-1][j];
}
if(j > 0 && s2.charAt(j-1) == s3.charAt(i+j-1)){
dp[i][j] = dp[i][j] || dp[i][j-1];
}
}
}
return dp[m][n];
}

接雨水

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

方法1:dp

对于一个位置来说,其能装的水是最左边最高柱子,与最右边最高柱子中较矮的那个,与当前位置柱子之差。

因此先计算出所有位置的最左边最高柱子,与最右边最高柱子,然后遍历每个位置,进行计算即可。当前位置左边最高柱子是左边的最高柱子与当前柱子的较大值,右边同理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// dp解法
if(height == null || height.length < 3)
return 0;
// 得到最左边最高的柱子
int n = height.length;
int[] left = new int[n];
int [] right = new int[n];
left[0] = height[0];
right[n-1] = height[n-1];
for(int i = 1; i < n; i++){
left[i] = Math.max(left[i-1],height[i]);
}
for(int i = n-2; i >=0; i--){
right[i] = Math.max(right[i+1],height[i]);
}
int res = 0;
for(int i = 1; i < n-1; i++){
res += Math.min(left[i],right[i]) - height[i];
}
return res;

方法2:单调栈

如果一个位置的高度比之前高度小,说明是低洼,无法接雨水,放入栈中;如果当前位置比栈顶位置高度高,说明可以与栈顶位置接住水,将栈顶元素弹出作为低洼,弹出栈中所有的低洼(与当前低洼相同),这时候栈顶值为另一侧的高墙,此时的高度为两侧高墙的最小值减去低洼的高度,宽度为两侧位置之差-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
public int trap(int[] height) {
// 单调栈
if(height == null || height.length < 3)
return 0;
// 栈中存放元素下标,如果是栈不为空且栈顶元素比当前元素小,进行接雨水,否则直接入栈
Stack stack = new Stack<>();
int res = 0;
for(int i = 0; i
while(!stack.isEmpty() && height[stack.peek()] < height[i]){
// 接雨水
// 弹栈,将相同值的都弹出,找到所有低洼
int curIndex = stack.pop();
while(!stack.isEmpty() && stack.peek() == curIndex){
stack.pop();
}
//如果栈不为空,说明左边有墙,高度为左边与右边墙较小值减去当前高度,宽度为右边-左边-1
if(!stack.isEmpty()){
int leftTop = stack.peek();
res += (Math.min(height[leftTop],height[i]) - height[curIndex])*(i-leftTop-1);
}

}
stack.push(i);
}
return res;
}

打家劫舍

打家劫舍1

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。

方法:dp
每天结束后又2种状态,可以被盗窃与不能被盗窃。如果当天结束后可以被盗窃,则值为前一天可以被盗窃与不能被盗窃较大值;如果当前结束后不能被盗窃,则值为前一天可以被盗窃+当前盗窃值。

1、定义dp数组含义

dp[i][0]为当前结束后可以被盗窃最大值,dp[i][1]为当前结束后不能被盗窃最大值

2、定义状态转移方程

1
2
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]);
dp[i][1] = dp[i-1][0] + nums[i];

最后考虑边界情况

dp[0][0] = 0. dp[0][1] = nums[0]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public int rob(int[] nums) {
// dp[i][0]表示当前结束后可以被偷窃,即当天没有偷窃
// dp[i][1]表示当前结束后不能被偷窃,即当天有被偷窃
if(nums == null || nums.length < 1)
return 0;
int[][] dp = new int[nums.length][2];
dp[0][1] = nums[0];
for(int i = 1; i < nums.length; i++){
// 当天没有偷窃,值为昨天有偷窃和没有偷窃较大值
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]);
// 当天偷窃,钱为前一天没有偷窃的值+当前偷窃值
dp[i][1] = dp[i-1][0] + nums[i];
}
return Math.max(dp[nums.length-1][0],dp[nums.length-1][1]);
}

打家劫舍2

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。

输入: [2,3,2]
输出: 3
解释: 你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

思路:两轮dp
对于你环形的房屋,有两种可能:第一个房子不偷,最后个房子偷;第一个房子偷,最后个房子不偷,看两次哪个收获更大。将环形问题转化为两轮的线性问题。

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 int rob(int[] nums) {
if(nums == null || nums.length < 1)
return 0;
int[][] dp = new int[nums.length][2];
if(nums.length < 2)
return nums[0];
// dp[i][0]表示当天结束后可以被偷
// dp[i][1]表示当天结束后不能被偷
// 第一次,偷第一家
dp[0][1] = nums[0];
int n = nums.length;
for(int i = 1; i < n - 1; i++){
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]);
dp[i][1] = dp[i-1][0] + nums[i];
}
// 最后个不偷
dp[n-1][0] = Math.max(dp[n-2][0],dp[n-2][1]);
// 记录结果
int res = dp[n-1][0];
// 第二次,不偷第一家
dp[0][1] = 0;
for(int i = 1; i < n - 1; i++){
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]);
dp[i][1] = dp[i-1][0] + nums[i];
}
// 偷最后家
dp[n-1][1] = dp[n-2][0] + nums[n-1];
dp[n-1][0] = Math.max(dp[n-2][0],dp[n-2][1]);
res = Math.max(res,Math.max(dp[n-1][0],dp[n-1][1]));
return res;
}

打家劫舍3

在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。

计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。

1
2
3
4
5
6
7
8
9
10
输入: [3,2,3,null,3,null,1]

3
/ \
2 3
\ \
3 1

输出: 7
解释: 小偷一晚能够盗取的最高金额 = 3 + 3 + 1 = 7.

思路:动态规划,用傻缓存来存储当前节点的盗取金额,一个节点有两种状态,分别为有被盗取与没有被盗取,分别用f与g表示,递归整个树,计算根节点的最大收益。如果当前节点被偷窃,其左右节点不能被偷窃;如果当前节点没有被偷窃,其左右节点可以被偷窃也可以不被偷,有四种状态。

加入傻缓存用来剪枝减少时间复杂度。

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
class Solution {
// 偷窃当前房屋所获得最大收益
Map f = new HashMap<>();
// 不偷窃当前房屋所获得最大收益
Map g = new HashMap<>();
public int rob(TreeNode root) {
// 进行处理
dfs(root);
return Math.max(f.getOrDefault(root,0),g.getOrDefault(root,0));
}
public void dfs(TreeNode root){
// base case
if(root == null)
return;
// 处理左右子树
dfs(root.left);
dfs(root.right);
// 处理当前结果
// 偷窃当前房屋,其左右子树不能偷窃
f.put(root,root.val + g.getOrDefault(root.left,0)+g.getOrDefault(root.right,0));
// 不偷窃当前房屋,左右子树可以被偷也可以不被偷
g.put(root,Math.max(f.getOrDefault(root.left,0),g.getOrDefault(root.left,0)) +
Math.max(f.getOrDefault(root.right,0),g.getOrDefault(root.right,0)));
}
}

最长公共子串与子序列【重要】

最长公共子串

给定两个字符串 text1text2,返回这两个字符串的最长公共子串。

方法:动态规划,定义dp数组含义,dp[i][j]表示text[1-i]和text[1-j]的最长公共子串的长度。状态转移方程为

1
2
dp[i][j]=dp[i-1][j-1]+1 (text[i]=text[j])
dp[i][j]=0(text[i]!=text[j])

即如果当前两个字符相等,dp值为1-i-1与1-j-1之间的最长公共子串值;如果不等,当前位置的最长公共子串长度就是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
// 求最长公共子串长度及具体字符串
public String findLongestCommonSubstring(String text1, String text2) {
if (text1 == null || text2 == null || text1.length() < 1 || text2.length() < 1)
return "";
int m = text1.length(), n = text2.length();
// dp[i][j]表示text1第i个与text2第j个之前最长公共子串长度
int[][] dp = new int[m+1][n+1];
int maxLen = 0, index = 0;
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (text1.charAt(i-1) == text2.charAt(j-1)){
dp[i][j] = dp[i-1][j-1] + 1;
// 更新最大长度与结束的index
if (maxLen < dp[i][j]){
maxLen = dp[i][j];
index = i;
}
}else {
// 不行的话为0,表示没有匹配的
dp[i][j] = 0;
}
}
}
// 输出
return text1.substring(index-maxLen,index);
}

最长公共子序列

方法:动态规划,定义dp数组含义,dp[i][j]表示text[1-i]和text[1-j]的最长公共序列的长度。状态转移方程为

1
2
dp[i][j]=dp[i-1][j-1]+1 (text[i]=text[j])
dp[i][j]=max(dp[i-1][j],dp[i][j-1])(text[i]!=text[j])

即如果当前两个字符相等,dp值为1-i-1与1-j-1之间的最长公共子串值。与最长子串不同,如果当前两个字符不等,相当于可以去掉其中一个字符不进行考虑,这样就是1-i与1-j-1或者1-i-1与1-j之间最长公共子序列长度中较长的那个。这样可以获取到最长的公共子序列的长度,接下来是或者最长公共子序列,需要用双指针指向两个text的末尾,如果两个指针指向的字符相同,就将当前字符加入结果,让两个指针均后退;如果不等,让dp数组更大的指针后退,这样可以更多的取到相同的子序列。

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
// 找到最长公共子序列是什么
public String findLongestCommonSubsequence(String text1, String text2) {
if (text1 == null || text2 == null || text1.length() < 1 || text2.length() < 1)
return "";
// 先dp算出其最长子串的长度,然后从后往前算
int m = text1.length(), n = text2.length();
// dp[i][j]表示text1第i个与text2第j个之前最长公共子序列长度
int[][] dp = new int[m+1][n+1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
// 如果相等,为dp[i-1]与dp[j-1]值+1
if (text1.charAt(i-1) == text2.charAt(j-1)){
dp[i][j] = dp[i-1][j-1] + 1;
}else {
// 不要i或者不要j
dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]);
}
}
}
// 从最后个开始算
StringBuilder sb = new StringBuilder();
int i = m-1, j = n-1;
while (i >= 0 && j >= 0){
// 如果相等,两个均-1,加入结果
if (text1.charAt(i) == text2.charAt(j)){
sb.append(text1.charAt(i));
--i;
--j;
}else {
// 看减去i更大还是减去j更大
if (dp[i][j+1] > dp[i+1][j]){
--i;
}else {
--j;
}
}
}
return sb.reverse().toString();
}

最长上升子序列

给定一个无序的整数数组,找到其中最长上升子序列的长度。

输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。

需要区分子序列与子串,子序列是可以不连续的,而子串需要是连续的。这个可以使用动态规划来做,定义动态规划数组dp[i]的含义是,以num[i]结果的最大子串的长度,而状态转移方程是,如果之前的有比num[i]小的,那么dp[i]=max(dp[j])+1,+1表示可以将之前的子序列拼接到当前的num[i]上,这样长度+1。最后不是返回最后一个dp[i]就好,因为最后的dp[i]只是表示以最后个字符结尾的最大子串长度,因此需要遍历一遍dp数组,找到最大的值。

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
//求一个序整数数组中最长上升子序列的长度
//dp解法,dp[i]定义为以nums[i]为结尾的最长子串长度
//dp[i]=max(dp[j])+1,其中dp[j]
//最后看每个dp[i]中最大的
public static int lengthOfLIS(int[] nums) {
if (nums == null || nums.length < 1)
return 0;
int[] dp = new int[nums.length];
//初始都是1
Arrays.fill(dp,1);//学习新用法
//从第二个数开始,看之前有没有比他小的
for (int i = 1; i < nums.length; i++) {
for (int j = 0; j < i; j++) {
if (nums[j] < nums[i]){
//更新
dp[i] = Math.max(dp[i],dp[j]+1);
}
}
}
//重新遍历,统计长度
int res = 0;
for (int i : dp) {
res = Math.max(res,i);
}
return res;
}

俄罗斯套娃

给定一些标记了宽度和高度的信封,宽度和高度以整数对形式 (w, h) 出现。当另一个信封的宽度和高度都比这个信封大的时候,这个信封就可以放进另一个信封里,如同俄罗斯套娃一样。

请计算最多能有多少个信封能组成一组“俄罗斯套娃”信封(即可以把一个信封放到另一个信封里面)。

说明:
不允许旋转信封。

示例:

输入: envelopes = [[5,4],[6,4],[6,7],[2,3]]
输出: 3
解释: 最多信封的个数为 3, 组合为: [2,3] => [5,4] => [6,7]。

方法:本质是一个求最长上升子序列的问题,但问题是最长上升子序列是针对一维序列的,但此问题为二维,因此需要进行排序,先堆信封宽度从小到大排序,然后如果宽度相同,将高度从大到小排列,因为题目要求要完全更小,这样相同宽度下最多只能选择一个高度。在排好序后,再按照高度寻找最长上升子序列即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public int maxEnvelopes(int[][] envelopes) {
Arrays.sort(envelopes, (o1,o2)->{
return o1[0] == o2[0] ? o2[1] - o1[1] : o1[0] - o2[0];
});
// 输出
int n = envelopes.length;
// 对高度查找
int[] dp = new int[n];
Arrays.fill(dp,1);
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
if (envelopes[j][1] < envelopes[i][1]){
dp[i] = Math.max(dp[i],dp[j]+1);
}
}
}
int res = 0;
for (int i = 0; i < n; i++) {
res = Math.max(res,dp[i]);
}
return res;
}

硬币问题

n分的表示方法

硬币。给定数量不限的硬币,币值为25分、10分、5分和1分,编写代码计算n分有几种表示法。(结果可能会很大,你需要将结果模上1000000007)

输入: n = 5
输出:2
解释: 有两种方式可以凑成总金额:
5=5
5=1+1+1+1+1

思路:完全背包,每个硬币可以选0-无穷种,使用一维的dp数组,第一重循环遍历每个硬币,然后第二重循环遍历硬币能组成的数量。

状态转移方程是dp[i] += dp[i-coin],即i可以由i-coin转移而来。

基本的情况是dp[0]=1,即没有硬币时组成情况只有1种。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public int waysToChange(int n) {
//多重背包
int[] dp = new int[n+1];
dp[0] = 1;
// dp[i] += dp[i-coin]
int[] coins = {1,5,10,25};
for(int coin : coins){
for(int i = coin; i <= n; i++){
if(i >= coin){
dp[i] = (dp[i] + dp[i-coin]) % 1000000007;
}
}
}
return dp[n];
}

零钱兑换

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1

思路:完全背包,每个coin使用无数次,第一重循环遍历硬币,第二重循环遍历金额数(正向),开始的时候将数组初始化为amount+1,即最多的数为amout+1,将dp[0]初始化为0,表示组成0需要0个硬币。状态转移方程为

dp[i] = min(dp[i],dp[i-coin]+1)

表示若可以从i-coin转移而来,则数量+1,取最小值。如果最后的结果为amount+1,说明不能转移到,返回-1。

1
2
3
4
5
6
7
8
9
10
11
12
public int coinChange(int[] coins, int amount) {
// 只能由0转移而来,其他初始化为极大值
int[] dp = new int[amount+1];// amount的最少硬币个数
Arrays.fill(dp,amount+1);// 最多要amount+1枚
dp[0] = 0;
for(int coin : coins){
for(int i = coin; i <= amount; i++){
dp[i] = Math.min(dp[i],dp[i-coin]+1);
}
}
return dp[amount] == amount + 1 ? -1 : dp[amount];
}

分割等和子集

给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

注意:

每个数组中的元素不会超过 100
数组的大小不会超过 200

输入: [1, 5, 11, 5]

输出: true

解释: 数组可以分割成 [1, 5, 5] 和 [11].

思路:本质就是背包问题,而且为0, 1背包,当前物品可以选择要或者不要。0 1背包可以只使用一维dp数组,dp[j]表示为和为j的数组和是否存在。如果当前j比当前数大,则可以由dp[j-nums[i]]转移而来。base case为dp[0]为true,dp[nums[0]]为true,最后要求的是dp[sum/2]。同时,如果数组和不为偶数,肯定不行;如果数组中最大数比和的一半要大,肯定不行。

二重循环,第一重为从前到后遍历每个数,第二重为背包体积从大到小遍历,因为需要这样确保是从i-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
public boolean canPartition(int[] nums) {
int sum = 0;
int maxNum = 0;
for(int num : nums){
sum += num;
maxNum = Math.max(maxNum,num);
}
int target = sum / 2;
if(sum % 2 == 1 || maxNum > target){
return false;
}
int n = nums.length;
boolean[] dp = new boolean[target+1];
dp[0] = true;
dp[nums[0]] = true;
// 第一层遍历货物
for(int i = 1; i < n; i++){
// 第二层遍历体积
for(int j = target; j >= nums[i]; j--){
dp[j] = dp[j - nums[i]] || dp[j];
}
}
return dp[target];
}

回溯

N皇后

n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。

每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。

对于皇后,不能在同一行,不能在同一列,不能在同一对角线,因此需要来统计每个皇后是否在不同列与不同对角线,对角线有两条,特点是对于正对角线,一条对角线上的点x-y相同;对于反对角线,一条对角线上的点x+y相同,然后base case为计算完最后个皇后,所有位置得出,对于dfs,需要遍历所有可能性,如果同一列或者同一对角线出现过,直接下一个数。然后标记当前状态出现的列与对角线,dfs下一个皇后,然后状态消除进行回溯。

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
62
63
64
65
66
67
68
69
70
71
int n;// 棋盘大小
int[] row;
List> res;
boolean[] col;// 当前列是否出现过
Set line1;// 正对角线是否出现过
Set line2;// 反对角线是否出现过

public List> solveNQueens(int n) {
// init
res = new ArrayList<>();
if(n == 0){
return res;
}
this.n = n;
res = new ArrayList<>();
col = new boolean[n];
line1 = new HashSet<>();
line2 = new HashSet<>();
row = new int[n];
Arrays.fill(row,-1);// 初始为-1,表示当前行的皇后位置
dfs(0);
return res;
}
public void dfs(int index){
// base case
if(index == n){
// 构造
List temp = process();
res.add(temp);
return;
}
// 判断所有可能性
for(int i = 0; i < n; i++){
// 当前列出现过
if(col[i]){
continue;
}
// 当前对角线出现过
int num1 = index - i;
if(line1.contains(num1)){
continue;
}
int num2 = index + i;
if(line2.contains(num2)){
continue;
}
// 改变当前状态
row[index] = i;
col[i] = true;
line1.add(num1);
line2.add(num2);
// 下一步
dfs(index + 1);
// 回溯
row[index] = -1;
col[i] = false;
line1.remove(num1);
line2.remove(num2);
}
}
// 根据数组生成结果
public List process(){
char[] ch = new char[n];
Arrays.fill(ch,'.');
List temp = new ArrayList<>();
for(int i = 0; i < n; i++){
ch[row[i]] = 'Q';
temp.add(String.valueOf(ch));
ch[row[i]] = '.';
}
return tem

有效的括号

题目描述:给定一个只包括 '('')''{''}''['']' 的字符串,判断字符串是否有效。

有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。

注意空字符串可被认为是有效字符串。

思路:先将左右括号的对应关系放入一个map中,然后如果是左括号,放入栈中;如果是右括号,若栈为空,则返回假,若不为空,弹出一个元素,如果在map中弹出元素对应的括号与当前括号不同,则返回假。遍历完字符串后,若栈不为空,返回假,否则返回真。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public boolean isValid(String s) {
//准备一个栈和一个哈希表
HashMap map = new HashMap<>();
map.put('(',')');
map.put('[',']');
map.put('{','}');
Stack stack = new Stack<>();
for(Character c : s.toCharArray()){
//如果是左括号,压入栈
if(map.containsKey(c)){
stack.push(c);
}else{
//不存在,先判断栈是否为空
if(stack.isEmpty())
return false;
//如果不符合,直接返回
if(map.get(stack.pop()) != c)
return false;
}
}
return stack.isEmpty();
}

最长有效括号

给定一个只包含 ‘(‘ 和 ‘)’ 的字符串,找出最长的包含有效括号的子串的长度。

输入: “(()”
输出: 2
解释: 最长有效括号子串为 “()”

题目理解:最长有效括号表示,在这个范围内,所有括号都是有效的。

方法一:暴力解法

比较容易想到暴力解法,从最有可能的长度(偶数长度)开始判断,如果此长度有括号全部有效就返回。然后有效长度-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
30
31
// 暴力方法,有效括号一定是偶数,从最大的偶数开始往下判断
public int longestValidParentheses1(String s) {
if(s == null || s.length() < 1)
return 0;
int n = s.length() % 2 == 0 ? s.length() : s.length()-1;
for (int i = n; i > 0; i-=2) {
for (int j = 0; j + i - 1 < s.length(); j++) {
// 判断当前的子串是否满足
if (isValid(s.substring(j,j+i))){
return i;
}
}
}
return 0;
}
private boolean isValid(String str) {
int res = 0;
Character c = null;
for (int i = 0; i < str.length(); i++) {
c = str.charAt(i);
if (c == '('){
++res;
}else {
if (res == 0){
return false;
}
--res;
}
}
return res == 0;
}

方法二:dp解法

需要明确dp数组的含义:dp[i]代表以i结尾的字符串最大有效括号长度

然后需要写状态转移方程,对于第i个字符,如果是(,则肯定不为有效,最大有效括号长度为0;如果是),则找到上一个左括号位置,dp[i-1]代表了上一串有效括号长度,i-dp[i-1]-1是第i个元素左括号应该出现的位置,如果此位置是左括号,说明这些位置的括号是有效的,这里的有效括号长度为2+dp[i-1]。然后需要判断之前相邻位置有没有有效括号,即加上dp[i-dp[i-1]-2]的长度(需要范围此位置是否越界)。

因此总的状态转移方程是

1
dp[i] = dp[i-dp[i-1]-2] + dp[i-1] + 2;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// dp解法
public int longestValidParentheses2(String s) {
// dp[i]的含义是以i结尾的最大有效括号长度
if (s == null || s.length() < 1)
return 0;
int[] dp = new int[s.length()];
int res = 0;
for (int i = 1; i < s.length(); i++) {
if (s.charAt(i) == ')'){
int len = dp[i-1];
if (i - len - 1 >= 0 && s.charAt(i - len - 1) == '('){
dp[i] = 2 + dp[i-1] + ((i-len-2) >= 0 ? dp[i-len-2] : 0);
}
res = Math.max(res,dp[i]);
}
}
return res;
}

方法三:栈

栈顶元素的含义是:上一次没有匹配到的右括号的下标,为了初始的判断,需要将-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
// 栈的解法
public int longestValidParentheses3(String s) {
// 栈底维护上一次有效的括号位置
if (s == null || s.length() < 1)
return 0;
Stack stack = new Stack<>();
stack.push(-1);
Character c = null;
int res = 0;
for (int i = 0; i < s.length(); i++) {
c = s.charAt(i);
if (c == '('){
stack.push(i);
}else {
stack.pop();
if (stack.isEmpty()){
stack.push(i);
}else {
res = Math.max(res,i-stack.peek());
}
}
}
return res;
}

方法4:贪心

从左到右遍历,记录左括号与右括号数量,如果两个数量相等,则当前有效括号数为数量x2,若右括号比左括号多,说明左边没有能可以匹配的,把两个数量更新为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
37
38
39
public int longestValidParentheses4(String s) {
if(s == null || s.length() < 1){
return 0;
}
int left = 0, right = 0, res = 0;
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == '('){
++left;
}else {
++right;
}
if (left == right){
res = Math.max(res,2*left);
continue;
}
if (right > left){
right = 0;
left = 0;
}
}
left = 0;
right = 0;
for (int i = s.length() - 1; i >= 0; i--) {
if (s.charAt(i) == '('){
++left;
}else {
++right;
}
if (left == right){
res = Math.max(res,2*left);
continue;
}
if (left > right){
right = 0;
left = 0;
}
}
return res;
}

字符串

最长回文子串

给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。

字符串这种要输出最大长度的子串的,可以用子串长度开始位置来确定其子串。

方法一:中间扩散法

遍历字符串,找到其最左边的与当前位置相同的位置,找到最右边与中间位置相同的位置,然后两边指针左右的字符如果相等,指针分别左右移动,统计两个指针夹住的最大长度及起始位置即可。

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
//中心扩散法
//先找到最左跟最右的,然后向两端开始比较
public static String longestPalindrome1(String s) {
if(s == null || s.length() < 2)
return s;
//最大数,默认第一个为最大的
int maxNum = 1;
//可以不用哈希表,记录起始位置就可以
int start = 0;
//两个指针
int p1 = 0, p2 = 0;
//循环去找
for (int i = 0; i < s.length(); i++) {
//找出最左的
p1 = i;
while (p1 >= 1 && s.charAt(p1-1) == s.charAt(p1)){
//记录开始位置
p1--;
}
//找到最右的
p2 = i;
while (p2 < s.length() - 1 && s.charAt(p2+1) == s.charAt(p2)){
//记录开始位置
p2++;
}
//向两边扩展
while(p1 >= 1 && p2 < s.length() - 1 && s.charAt(p1-1) == s.charAt(p2+1)){
p1--;
p2++;
}
//p1-p2为回文子串的长度
if(maxNum < p2 - p1 + 1){
maxNum = p2 - p1 + 1;
start = p1;
}
//更新i
}
//取出最大长度对应的子串
return s.substring(start,start+maxNum);
}

方法二:动态规划法

用二维数组统计哪些角标处的是回文子串,如dp[2][0]代表0-2角标处的是回文子串,用两个指针遍历,一个从1开始往后,第二个从0开始到第一个指针,如果第二个指针与第一个指针处字符相等,如果角标差小于等于2,说明子串长度在3以内,不用判断dp数组,不然就需要dp[i-1][j+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
//dp
//如果某两个角标之间的已经是回文的了,那么可以不用寻找
public static String longestPalindrome4(String s) {
if(s == null || s.length() < 2)
return s;
int maxNum = 1;
//可以不用哈希表,记录起始位置就可以
int start = 0;
boolean[][] dp = new boolean[s.length()][s.length()];
//统计j-i之间的情况
for (int i = 1; i < s.length(); i++) {
for (int j = 0; j < i; j++) {
//如果相等,且之间距离在2之间(3个以内),或j+1-i-1为回文,计算
if (s.charAt(j) == s.charAt(i) && (i-j<=2 || dp[i-1][j+1])){
//更改dp状态
dp[i][j] = true;
//统计信息
if (maxNum < i-j+1){
maxNum = i-j+1;
start = j;
}
}
}
}
return s.substring(start,start+maxNum);
}

通配符匹配

给定一个字符串 (s) 和一个字符模式 (p) ,实现一个支持 ‘?’ 和 ‘*’ 的通配符匹配。

‘?’ 可以匹配任何单个字符。
‘*’ 可以匹配任意字符串(包括空字符串)。
两个字符串完全匹配才算匹配成功。

说明:

s 可能为空,且只包含从 a-z 的小写字母。
p 可能为空,且只包含从 a-z 的小写字母,以及字符 ? 和 *。

方法1:dp

可以使用动态规划来解决,首先需要明确dp数组的含义,dp[i][j]表示s到第i个字符与p到第j个字符是否匹配,然后 是确定dp的状态转移方程。

1、如果s的第i个与p的第j个字符相同,或者p的第j个字符是?

1
dp[i][j] = dp[i-1][j-1]

2、如果p的第j个是*,则有两种情况,如果*表示空字符串,则dp[i][j] = dp[i][j-1],如果 *表示匹配了一个字符,则dp[i][j] = dp[i-1][j],二者只要有一个符合即可,所以

1
dp[i][j] = dp[i-1][j] || dp[i][j-1]

然后需要考虑边界情况,当p为空,s不能匹配空的字符串。当s为空,则p的前几位是*,可以匹配对应位数

当s与p均为空时可以,因此dp[0][0]=true

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
// 字符串匹配的问题
// dp[i][j]表示s第i个与p第j个元素之前是否能 匹配
public boolean isMatch(String s, String p) {
// 第i个字符,对应str.charAt(i-1)
if (p == null && s != null)
return false;
if (s == null && p == null)
return true;
boolean[][] dp = new boolean[s.length()+1][p.length()+1];
dp[0][0] = true;
// 如果s为空,p前面为*,可匹配
for (int i = 1; i <= p.length(); i++) {
if (p.charAt(i-1) != '*')
break;
dp[0][i] = true;
}
for (int i = 1; i <= s.length(); i++) {
for (int j = 1; j <= p.length(); j++) {
if (p.charAt(j-1) == '*'){
dp[i][j] = dp[i][j-1] || dp[i-1][j];
}else if (p.charAt(j-1) == '?' || s.charAt(i-1) == p.charAt(j-1)){
dp[i][j] = dp[i-1][j-1];
}
}
}
return dp[s.length()][p.length()];
}

解码方法

一条包含字母 A-Z 的消息通过以下方式进行了编码:

‘A’ -> 1
‘B’ -> 2

‘Z’ -> 26
给定一个只包含数字的非空字符串,请计算解码方法的总数。

输入: “12”
输出: 2
解释: 它可以解码为 “AB”(1 2)或者 “L”(12)。

解题思路:
采用动态规划的解法,动态规划的两大核心是dp数组含义与状态转移方程的定义
dp数组含义:采用一维数组,dp[i]的含义为从第i个字符到最后个字符所有可能的解法数量
如s为“121”,dp[1]代表s下标为1的字符‘2’开始,到最后个字符,即‘21’所有可能解法数量
然后定义状态转移方程
对于第i个字符,如果这个字符大小在1-9之间,可以映射到英文字母,dp[i]的值与以第i+1字符开始的字符串对应解法相同,即dp[i]=dp[i+1],如果第i个字符大小为0,则dp[i]=0。
然后又第i个字符与第i+1个字符可以组成新字符的情况(两字符组成的数字在10-26之间),此时dp[i]+=dp[i+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
public int numDecodings(String s) {
// dp解法,dp[i]代表i到最后的可能性,从后往前推
if (s == null || s.length() < 1)
return 0;
int[] dp = new int[s.length() + 1];
dp[s.length()-1] = isNum(s.charAt(s.length()-1)) ? 1 : 0;
// 遍历
for (int i = s.length()-2; i >= 0; i--) {
dp[i] = dp[i+1] + (isAToZ(s.charAt(i),s.charAt(i+1)) ? 1 : 0);
}
return dp[0];
}

private boolean isNum(char c){
return c > '0' && c <= '9';
}

private boolean isAToZ(char c1, char c2){
if (isNum(c1) || isNum(c2))
return false;
int num1 = Integer.valueOf(c1);
int num2 = Integer.valueOf(c2);
return 10 * num1 + num2 <= 26;
}

比较含退格的字符串

给定 S 和 T 两个字符串,当它们分别被输入到空白的文本编辑器后,判断二者是否相等,并返回结果。 # 代表退格字符。

注意:如果对空文本输入退格字符,文本继续为空。

输入:S = “ab#c”, T = “ad#c”
输出:true
解释:S 和 T 都会变成 “ac”。

思路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
public boolean backspaceCompare(String S, String T) {
String s1 = process(S.toCharArray());
String s2 = process(T.toCharArray());
return s1.equals(s2);
}
public String process(char[] ch){
for(int i = 0; i < ch.length; i++){
if(ch[i] == '#'){
int j = i - 1;
while(j >= 0 && ch[j] == '#'){
--j;
}
if(j >= 0 && ch[j] != '#'){
ch[j] = '#';
}
}
}
StringBuilder sb = new StringBuilder();
for(int i = 0; i < ch.length; i++){
if(ch[i] != '#'){
sb.append(ch[i]);
}
}
return sb.toString();
}

长按键入

你的朋友正在使用键盘输入他的名字 name。偶尔,在键入字符 c 时,按键可能会被长按,而字符可能被输入 1 次或多次。

你将会检查键盘输入的字符 typed。如果它对应的可能是你的朋友的名字(其中一些字符可能被长按),那么就返回 True。

输入:name = “alex”, typed = “aaleex”
输出:true
解释:’alex’ 中的 ‘a’ 和 ‘e’ 被长按。

思路:双指针。当前name[i]与typed[j],要么字符相同表示匹配,要么不同但typed[j]与typed[j-1]相同,j可以表示长按,j后移;要么就表示不匹配。如果两个字符相同,同时后移,如果不相同,看typed字符能否后移。如果都不行,说明不匹配,返回假。最后看i是不是到了name末尾。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public boolean isLongPressedName(String name, String typed) {
if(name.length() > typed.length()){
return false;
}
int i = 0, j = 0;
while(j < typed.length()){
if(i < name.length() && name.charAt(i) == typed.charAt(j)){
++i;
++j;
}else if(j > 0 && typed.charAt(j) == typed.charAt(j - 1)){
++j;
}else {
return false;
}
}
return i == name.length();
}

划分字母区间

字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一个字母只会出现在其中的一个片段。返回一个表示每个字符串片段的长度的列表。

输入:S = “ababcbacadefegdehijhklij”
输出:[9,7,8]
解释:
划分结果为 “ababcbaca”, “defegde”, “hijhklij”。
每个字母最多出现在一个片段中。
像 “ababcbacadefegde”, “hijhklij” 的划分是错误的,因为划分的片段数较少。

思路:找到每个字符的起始和结束位置,按照起始位置进行排序,类似于合并区间,如果是可以合并的,则合并为一个,若不能合并,则将上一个区间放到结果中。

二维数组排序,可以使用Arrays.sort,使用lambda表达式,(o1,o2)->o1[0] - o2[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
public List partitionLabels(String S) {
int[][] arr = new int[26][2];
for(int i = 0;i < S.length(); i++){
int index = S.charAt(i) - 'a';
if(arr[index][0] == 0){
arr[index][0] = i + 1;
arr[index][1] = i + 1;
}else{
arr[index][1] = i + 1;
}
}
// 二维数组排序方式
Arrays.sort(arr,(o1,o2)->o1[0] - o2[0]);
int i = 0;
while(i < 26 && arr[i][0] == 0){
i++;
}
int l = arr[i][0];
int r = arr[i][1];
List res = new ArrayList<>();
for(++i;i < 26; i++){
// 计数
if(arr[i][0] > r){
res.add(r - l + 1);
l = arr[i][0];
r = arr[i][1];
}else{
// 合并
r = Math.max(r,arr[i][1]);
}
}
// 加入最后一个
res.add(r - l + 1);
return res;
}

字符串中第一个唯一字符

给定一个字符串,找到它的第一个不重复的字符,并返回它的索引。如果不存在,则返回 -1。

s = “leetcode”
返回 0

s = “loveleetcode”
返回 2

提示:你可以假定该字符串只包含小写字母。

思路:用数组存储字符出现位置,一开始默认为-1,遍历每个字符,如果是-1,更改为当前位置,如果是小于n的数,说明出现过一次了,更改为n。然后遍历数组,记录非-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 int firstUniqChar(String s) {
if(s == null || s.length() < 1){
return -1;
}
int n = s.length();
int[] arr = new int[26];
Arrays.fill(arr, -1);
for(int i = 0; i < n; i++) {
char c = s.charAt(i);
if(arr[c - 'a'] == -1) {
arr[c - 'a'] = i;
}else {
arr[c - 'a'] = n;
}
}
int res = n;
for(int i = 0; i < 26; i++) {
if(arr[i] > -1 && arr[i] < n) {
res = Math.min(res, arr[i]);
}
}
return res == n ? -1 : res;
}

链表

LRU缓存机制

运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。

获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
写入数据 put(key, value) - 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。时间复杂度O(1)。

思路:这种数据结构要求的特点是插入删除快,获取快。获取时间复杂度O(1),想到哈希表,但是哈希表是无序的,没办法做到找到最近操作的数。为了让插入删除快,想到链表。因此将二者结合,便想到了LinkedHashMap,先自己造轮子,代码思路来自于leetcode评论区解法

方法一:自己造轮子

为了让链表与哈希表相结合,需要让哈希表与链表进行映射,哈希表存储key和对应的节点,链表为了方便删除操作,使用双向链表,链表的节点定义如下

1
2
3
4
5
6
7
8
9
10
class DoubleNode{
int key;
int value;
DoubleNode pre;
DoubleNode next;
public DoubleNode(int key, int value){
this.key = key;
this.value = value;
}
}

对于链表的操作,需要实现的功能有,在链表前面插入节点,删除一个节点,删除末尾节点,获取链表容量,双向链表的实现如下。维护头节点与尾节点,方便进行插入与删除的操作,对于插入一个元素,需要改变4个指向的关系,让size自增。对于删除一个节点,需要改变2个指向的关系,size递减,返回删除元素的key。对于删除末尾节点,调用删除节点的方法即可。

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
class DoubleList{
//头节点与尾节点
private DoubleNode head;
private DoubleNode tail;
private int size;
public DoubleList(){
head = new DoubleNode(0,0);
tail = new DoubleNode(0,0);
head.next = tail;
tail.pre = head;
size = 0;
}
//往链表头节点添加元素
public void addFirst(DoubleNode node){
//添加当前节点的指向
node.next = head.next;
node.pre = head;
head.next.pre = node;
head.next = node;
size++;
}
//在链表中移除元素,其中节点一定在链表中
public int remove(DoubleNode node){
//删除2个关系
node.pre.next = node.next;
node.next.pre = node.pre;
size--;
return node.key;
}
//移除链表末尾元素
public int removeLast(){
if (size == 0)
return 0;
return remove(tail.pre);
}
//得到长度
public int getSize(){
return size;
}
}

对于LRU缓存,需要维护一个哈希表和一个双向链表,同时需要设置一个阈值容量。get方法的逻辑为,如果哈希表中不存在key,返回-1;如果存在,调用put方法,更新哈希表和链表,返回对应的value。对于put方法,逻辑为,构造一个新节点,当这个key存在于map中,在链表中将此key对应的节点删除,将新节点插入到链表头部,更新map。如果不存在于map中,当容量满了,删除链表最后的元素,在哈希表中删除其对应的key,然后将节点添加至链表头部,更新哈希表。实现如下:

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 LRUCache {
//持有一个map和一个双向链表,根据key找到对应Node,然后在链表中操作
private HashMap map;
private DoubleList list;
private int cap;//容量
public LRUCache(int cap){
map = new HashMap<>();
list = new DoubleList();
this.cap = cap;
}
//get方法
public int get(int key){
//如果不存在,返回-1
if (!map.containsKey(key)){
return -1;
}
int res = map.get(key).value;
//更新链表
put(key,res);
return res;
}

public void put(int key, int value) {
DoubleNode node = new DoubleNode(key,value);
//如果已经存在了,进行替换
if (map.containsKey(key)){
//在链表中删除
list.remove(map.get(key));
//将新节点加入
list.addFirst(node);
//更新哈希表
map.put(key,node);
}else {
//如果链表长度等于容量,删除末尾节点,然后将新节点添加至头部
if (list.getSize() == cap){
//记录被删除节点的key,便于从哈希表中移出
int temp = list.removeLast();
map.remove(temp);
}
list.addFirst(node);
map.put(key,node);
}
}
}

方法二:用轮子

Java中有序的哈希表为LinkedHashMap,直接用一个类去继承,初始化的时候为了让取出顺序为访问顺序,传入true,然后get方法调用父类的getOrDefault方法,put方法调用父类的put方法,为了删除超出容量的节点,需要覆写removeEldestEntry方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class LRUCache2 extends LinkedHashMap<Integer,Integer> {
private int cap;
public LRUCache2(int cap){
//当为true的时候,get的顺序为访问的顺序
super(cap,0.75F,true);
this.cap = cap;
}
public int get(int key){
return getOrDefault(key,-1);
}
public void put(int key,int value){
super.put(key,value);
}
@Override
protected boolean removeEldestEntry(Map.Entry eldest){
return size() > cap;
}
}

K个一组翻转链表

给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。

k 是一个正整数,它的值小于或等于链表的长度。

如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

给你这个链表:1->2->3->4->5

当 k = 2 时,应当返回: 2->1->4->3->5

当 k = 3 时,应当返回: 3->2->1->4->5

思路:将题目拆解,找到待翻转的K个节点,进行翻转,进行下一次循环。

构造一个新头节点便于操作,需要有前驱节点pre,长度为K的子链表头节点start,尾节点end,每次将end往后移动k个,如果此时还没有到第K个就到了链表末尾,停止循环,如果此时end为null,不用反转链表,直接break。否则,反转start-end之间的链表,为了方便处理,记录下end的下一个节点,然后令end下一个节点为null,反转完成后,pre下一个指向新链表节点,然后pre与end移动到子链表新尾节点start,继续进行循环

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 ListNode reverseKGroup(ListNode head, int k) {
if(head == null || head.next == null)
return head;
ListNode newHead = new ListNode(0);
newHead.next = head;
// 前驱,当前,结束
ListNode pre = newHead, end = newHead, start = null, next = null;;
while(end!= null){
// 走k步
for(int i = 0; i < k && end!= null; i++){
end = end.next;
}
if(end== null)
break;
start = pre.next;
// 反转start - end
next = end.next;
end.next = null;
pre.next = reverse(start);
start.next = next;
pre = start;
end = pre;
}
return newHead.next;
}
private ListNode reverse(ListNode head){
// 三个指针
ListNode pre = null, next = null;
while(head != null){
next = head.next;
head.next = pre;
pre = head;
head = next;
}
return pre;
}

合并K个排序链表

合并 k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。

示例:

输入:
[
1->4->5,
1->3->4,
2->6
]
输出: 1->1->2->3->4->4->5->6

方法:归并

暴力方法是,合并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
public ListNode mergeKLists(ListNode[] lists) {
if(lists == null || lists.length < 1)
return null;
return merge(lists,0,lists.length - 1);
}
public ListNode merge(ListNode[] lists, int l, int r){
// base case
if(l == r)
return lists[l];
int mid = l + (r - l) / 2;
ListNode l1 = merge(lists,l,mid);
ListNode l2 = merge(lists,mid+1,r);
return process(l1,l2);
}
// 合并l1与l2
public ListNode process(ListNode l1, ListNode l2){
if(l1 == null)
return l2;
if(l2 == null)
return l1;
if(l1.val <= l2.val){
l1.next = process(l1.next,l2);
return l1;
}else{
l2.next = process(l1,l2.next);
return l2;
}
}

排序链表

在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序。

示例 1:

输入: 4->2->1->3
输出: 1->2->3->4
示例 2:

输入: -1->5->3->4->0
输出: -1->0->3->4->5

思路1:开辟个数组,转化为数组的排序问题,但这样空间复杂度要O(N)

思路2:基于归并的思想,先将链表拆分,然后再合并。基本思路是利用双指针找到链表的中间节点,然后将链表拆分为2个,再继续拆分。然后进行归并,谁小动谁。空间复杂度为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
public ListNode sortList(ListNode head) {
// base case
if(head == null || head.next == null){
return head;
}
// 找中间节点
ListNode slow = head, fast = head;
while(fast.next != null && fast.next.next != null){
slow = slow.next;
fast = fast.next.next;
}
ListNode temp = slow.next;
slow.next = null;
ListNode left = sortList(head);
ListNode right = sortList(temp);
// merge
ListNode newHead = new ListNode(0);
ListNode res = newHead;
while(left != null && right != null){
if(left.val <= right.val){
newHead.next = left;
left = left.next;
newHead = newHead.next;
}else{
newHead.next = right;
right = right.next;
newHead = newHead.next;
}
}
newHead.next = left != null ? left : right;
return res.next;
}

重排链表

给定一个单链表 L:L0→L1→…→Ln-1→Ln ,
将其重新排列后变为: L0→Ln→L1→Ln-1→L2→Ln-2→…

你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

给定链表 1->2->3->4, 重新排列为 1->4->2->3.

思路:先将链表后半部分逆序,然后头指向尾,尾指向头第二次,循环。

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
public void reorderList(ListNode head) {
// 找到重点后一个节点,逆序
if(head == null || head.next == null){
return;
}
ListNode p1 = head, p2 = head;
while(p2.next != null && p2.next.next != null){
p1 = p1.next;
p2 = p2.next.next;
}
if(p2.next != null){
p1 = p1.next;
}
p2 = p1.next;
p1.next = null;
ListNode p3 = null;
while(p2 != null){
p3 = p2.next;
p2.next = p1;
p1 = p2;
p2 = p3;
}
p2 = head;
ListNode p4 = null;
while(p2.next != null && p1.next != null){
p3 = p1.next;
p4 = p2.next;
p2.next = p1;
p1.next = p4;
p2 = p4;
p1 = p3;
}
}

二叉树的最近公共祖先

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

思路:采用递归解决,对于一个节点来说,p,q有可能在其左右子树上,也可能在其中一个子树上。如果节点为空,返回空,如果碰到了p或者q,返回当前节点。递归返回左,右子树中包含p或者q的节点。如果左右子树均不为空,则公共父节点是当前节点。否则返回不为空的那个节点(说明q在p的子树之中),都为空就随便返回一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//求两个节点的最近父节点
//两节点在同一子树或不同子树上
//若在同一子树上,则左右子树两个有个不为空,返回不为空的
//若在不同子树上,那么当前节点就是
public TreeNode lowestCommonAncestor1(TreeNode root, TreeNode p, TreeNode q) {
//base case
if(root == null || root == p || root == q){
return root;
}
//看左右子树
TreeNode left = lowestCommonAncestor1(root.left,p,q);
TreeNode right = lowestCommonAncestor1(root.right,p,q);
if (left != null && right != null){
return root;
}
return left != null ? left : right;
}

合并二叉树

给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠。

你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为 NULL 的节点将直接作为新二叉树的节点。

思路:如果其中一个为空,返回另一个。将当前节点合并,合并左子树,合并右子树,返回当前节点。

1
2
3
4
5
6
7
8
9
10
11
public TreeNode mergeTrees(TreeNode t1, TreeNode t2) {
//base case
if (t1 == null)
return t2;
if(t2 == null)
return t1;
t1.val += t2.val;
t1.left = mergeTrees(t1.left,t2.left);
t1.right = mergeTrees(t1.right,t2.right);
return t1;
}

不同的二叉搜索树

给定一个整数 n,求以 1 … n 为节点组成的二叉搜索树有多少种?

示例:

输入: 3
输出: 5
解释:
给定 n = 3, 一共有 5 种不同结构的二叉搜索树:

1 3 3 2 1
\ / / / \
3 2 1 1 3 2
/ / \
2 1 2 3
思路:动态规划,1,2,3这3个数,总的情况为G(n),则可以由以1为头,2为头,3为头的树种类决定,即

G(n)=sum(F(i)),i=1,2,…,n

而计算F(i),是左边的数构建树的种类与右边的数构建树的种类的乘积。即F(i)=G(i-1)xG(ni)。这样,得到G(n)的表达式为G(n)=sum(G(i-1)xG(n-i)),i=1,2,…,n。其中G(0)=1,G(1)=1.

1
2
3
4
5
6
7
8
9
10
11
12
//dp解法
public int numTrees(int n) {
int[] G = new int[n+1];
G[0] = 1;
G[1] = 1;
for (int i = 2; i <= n; i++) {
for (int j = 1; j <= i; j++) {
G[i] += G[j-1] * G[i - j];
}
}
return G[n];
}

路径总和

给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和。

说明: 叶子节点是指没有子节点的节点。

示例:
给定如下二叉树,以及目标和 sum = 22,

      5
     / \
    4   8
   /   / \
  11  13  4
 /  \      \
7    2      1

返回 true, 因为存在目标和为 22 的根节点到叶子节点的路径 5->4->11->2。

方法:深度优先遍历

如果当前节点为空,返回假。将当前的sum减去当前节点值,若当前节点为叶子节点,判断sum是否为0。递归处理当前节点的左右子树,只要有一个满足即可。

1
2
3
4
5
6
7
8
public boolean hasPathSum(TreeNode root, int sum) {
if(root == null)
return false;
sum -= root.val;
if(root.left == null && root.right == null)
return sum == 0;
return hasPathSum(root.left,sum) || hasPathSum(root.right,sum);
}

将有序数组转换为二叉搜索树

将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树。

本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。

1
2
3
4
5
6
7
8
9
给定有序数组: [-10,-3,0,5,9],

一个可能的答案是:[0,-3,9,-10,null,5],它可以表示下面这个高度平衡二叉搜索树:

0
/ \
-3 9
/ /
-10 5

思路:递归

进入递归时,选中间的节点作为root,然后处理其左右子树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 选中间节点为头节点,造左右树
public TreeNode sortedArrayToBST(int[] nums) {
if(nums == null || nums.length < 1)
return null;
return process(nums,0,nums.length-1);
}
public TreeNode process(int[] arr, int l ,int r){
// base case
if(l > r){
return null;
}
if(l == r){
return new TreeNode(arr[l]);
}
int mid = (r - l)%2 == 0 ? l+(r-l)/2 : l+(r-l+1)/2;
TreeNode root = new TreeNode(arr[mid]);
root.left = process(arr,l,mid-1);
root.right = process(arr,mid+1,r);
return root;
}

二叉树中最大路径和

给定一个非空二叉树,返回其最大路径和。

本题中,路径被定义为一条从树中任意节点出发,达到任意节点的序列。该路径至少包含一个节点,且不一定经过根节点。

输入: [1,2,3]

  1
 / \
2   3

输出: 6

思路:dfs。一个节点的左,右子树,所有可能的路径为:左中,左右,左中右,在经过一个节点的时候,计算三个路径里面最大的。因为最长路径不一定是经过根节点的,因此每经过一个节点更新下全局变量。在计算一个节点的子路径和时,如果子路径小于0,则不如舍弃,因此要将子路径和与0比较。在返回给上一级数据的时候,对于上一级只有向左跟向右两种选择。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int max = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
if(root == null)
return 0;
dfs(root);
return max;
}
private int dfs(TreeNode root){
// base case
if(root == null)
return 0;
// dfs,计算所有子节点结果
int left = Math.max(dfs(root.left),0);
int right = Math.max(dfs(root.right),0);
// 计算左,中,右的最大路径
max = Math.max(max,root.val+left+right);
// 返回给上一级的,只能是左边或者右边的一条
return root.val + Math.max(left,right);
}

二叉树中的插入操作

给定二叉搜索树(BST)的根节点和要插入树中的值,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据保证,新值和原始二叉搜索树中的任意节点值都不同。

注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回任意有效的结果。

例如,

给定二叉搜索树:

    4
   / \
  2   7
 / \
1   3

和 插入的值: 5
你可以返回这个二叉搜索树:

     4
   /   \
  2     7
 / \   /
1   3 5

方法一:递归

定义递归函数的作用是,给定树节点和插入值,返回二叉搜索树的头节点。base case是,如果当前节点为空,以插入值构造新节点,返回此节点。如果当前节点值小于插入值,说明这个值应该插入到当前节点的右子树;如果当前节点值大于插入值,说明这个值应该插入到当前节点的左子树。返回当前头节点。

1
2
3
4
5
6
7
8
9
10
11
12
public TreeNode insertIntoBST(TreeNode root, int val) {
// base case
if(root == null){
return new TreeNode(val);
}
if(root.val < val){
root.right = insertIntoBST(root.right,val);
}else{
root.left = insertIntoBST(root.left,val);
}
return root;
}

方法二:迭代

如果当前节点是空,构造新节点返回。循环,当前节点如果值比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
// base case
if(root == null){
return new TreeNode(val);
}
// 遍历节点
TreeNode cur = root;
while(true){
if(cur.val < val){
if(cur.right == null){
cur.right = new TreeNode(val);
break;
}else{
cur = cur.right;
}
}else{
if(cur.left == null){
cur.left = new TreeNode(val);
break;
}else{
cur = cur.left;
}
}
}
return root;

完全二叉树的节点个数

给出一个完全二叉树,求出该树的节点个数。

说明:

完全二叉树的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2h 个节点。

1
2
3
4
5
6
7
8
输入: 
1
/ \
2 3
/ \ /
4 5 6

输出: 6

思路1:直接利用递归,求出二叉树节点个数

1
2
3
4
5
6
public int countNodes(TreeNode root) {
if(root == null){
return 0;
}
return 1 + countNodes(root.left) + countNodes(root.right);
}

思路2:利用完全二叉树的特点,分别求出左右子树的高度,如果两个子树高度相同,说明左子树是满的,节点个数是2^left - 1,若加上当前节点个数,就是2^left,然后加上递归计算的右子树节点个数;如果左右高度不同,说明右子树是满的,节点个数是2的right次方,然后加上左子树节点个数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public int countNodes(TreeNode root) {
if(root == null){
return 0;
}
int leftCount = getCount(root.left);
int rightCount = getCount(root.right);
if(leftCount == rightCount){
// 左子树是满的
return countNodes(root.right) + (1 << leftCount);
}else {
return countNodes(root.left) + (1 << rightCount);
}
}
public int getCount(TreeNode node){
TreeNode cur = node;
int height = 0;
while(cur != null){
++height;
cur = cur.left;
}
return height;
}

bfs

魔法数字

题意:一天,牛妹找牛牛做一个游戏,牛妹给牛牛写了一个数字n,然后又给自己写了一个数字m,她希望牛牛能执行最少的操作将他的数字转化成自己的。

操作共有三种,如下:

​ 1.在当前数字的基础上加一,如:4转化为5

​ 2.在当前数字的基础上减一,如:4转化为3

​ 3.将当前数字变成它的平方,如:4转化为16

​ 你能帮牛牛解决这个问题吗?

1<=n,m<=1000

输入:给定n,m,分别表示牛牛和牛妹的数字。

输出:返回最少需要的操作数。

示例:输入3 10,输出 2

思路:如果直接暴力递归,会超时

这时候发现,对于一个数字,有三种转换方式,是一种树形结构,而一个数字出现的最小次数,一定是其第一次出现的时候,这样将n压入队列,然后从队列中弹出一个,对于其三个子情况,如果其没有出现过(用-1表示),那么将其加入队列,并记录其转换次数为当前次数+1,这样当队列中元素为空,所有情况都遍历过了,最后输出dp[m]。这题的关键是bfs(找最短路径),边界情况(0-2001)。

通过判断当前数是否出现过来剪枝

附上阿罗的图作为说明

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
// bfs写法,每次数字第一次出现,必然是次数最少的
public int solve2 (int n, int m) {
// 对于队列中的数,每次有三种情况,平方,+1,-1
int[] dp = new int[2001];
// 填充-1,表示没有进去过
Arrays.fill(dp,-1);
LinkedList queue = new LinkedList<>();
queue.addLast(n);
dp[n] = 0;
while (!queue.isEmpty()){
int num = queue.pollFirst();
if (num == m){
return dp[num];
}
if ((num - 1 >=0) && dp[num-1] == -1){
queue.addLast(num-1);
dp[num-1] = dp[num] + 1;
}
if (num + 1 < 2001 && dp[num+1] == -1){
queue.addLast(num+1);
dp[num+1] = dp[num] + 1;
}
if (num * num < 2001 && dp[num*num] == -1){
queue.addLast(num * num);
dp[num * num] = dp[num] + 1;
}
}
return dp[m];
}

背包问题

01背包

一个物品要么被选择,要么不被选择

有 N件物品和一个容量是 V 的背包。每件物品只能使用一次。

第 i件物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

输入格式

第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。

接下来有 NN行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。

输出格式

输出一个整数,表示最大价值。

数据范围

00

输入样例

1
2
3
4
5
4 5
1 2
2 4
3 4
4 5

输出样例:

1
8

思路:定义dp数组含义,dp[i][j]表示看前i个物品,总体积恰好是j时,最大总价值,最后要求的是

result = max(dp[n][o-V])

然后定义状态转移方程

1
f[i][j] = max(f[i-1][j],f[i-1][j-vi]+wi);

含义是不选第i个,f[i][j]f[i-1][j]转移而来,如果是选第i个,体积变为j-vi,可以增加价值。

初始化的过程,f[0][0]=0

因为dp数组体积的定义需要恰好为j,因此需要重新遍历dp[n][0-V]来确保更新到最大值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 二维dp
public int zeroAndOnePack1(int N, int V, int[] v, int[] w){
// dp[i][j]代表前i个物品体积为j时最大价值
// arr[0]为体积,arr[1]为价值
int[][] dp = new int[N+1][V+1];
// base case
dp[0][0] = 0;
for (int i = 1; i <= N; i++) {
for (int j = 0; j <= V; j++) {
dp[i][j] = dp[i-1][j];
if (j - v[i] >= 0){
dp[i][j] = Math.max(dp[i][j],dp[i-1][j-v[i]] + w[i]);
}
}
}
// 找dp[N][0-V最大值]
int res = 0;
for (int i = 0; i <= V; i++) {
res = Math.max(res,dp[N][i]);
}
return res;
}

观察dp发现,dp[i]只与dp[i-1]有关,因此可以使用一维数组来转移,此时dp[i]=dp[i-1]可以省略,因为数组本身保存的就是上一个商品的值,因此只要商品是从1-N遍历,就可以保证dp[i][j]dp[i-1][j]转移而来,但此时为了保证dp[i][j]dp[i-1][j-v[i]] + w[i]转移而来,需要让体积从大到小遍历,因为如果体积从小到大遍历,此时大的体积相当于是由当前的第i个商品的状态转移而来的,此时就不符合状态转移方程,而如果体积由大到小遍历,可以保证大的V是由上一个状态dp[i-1]转移而来。

而dp[i]的含义是体积最大为i时的最大价值,只需要看dp[V]的值,而不用枚举的原因是,将dp[i]均初始化为0,而如果只将dp[0]初始化为0,其他初始化为无穷,此时dp[i]含义为体积恰好为i时的最大价值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 一维dp
public int zeroAndOnePack2(int N, int V, int[] v, int[] w){
// dp[i]表示容量最大为i时的最大价值
int[] dp = new int[V+1];
// 边界
dp[0] = 0;
// 记录了上一个物品的状态
// dp[i] = max(dp[i],dp[i-v[j]]+w[j]
// 遍历每个物品
for (int i = 1; i <= N; i++) {
// 体积要从大到小遍历,因为小的状态先被计算,大的才能被转移到
for (int j = V; j >= v[i]; j--) {
dp[j] = Math.max(dp[j],dp[j-v[i]]+w[i]);
}
}
return dp[V];
}

完全背包

完全背包是在0 1背包的基础上,一个物品可以被选择多次,这样在0 1背包的基础之上,先遍历每个物品,然后从大到小遍历体积(保证从i-1转移而来),状态转移方程变为

1
dp[j]=max(dp[j],dp[j-k*v[i]]+k*w[i]);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 一维dp
public int fullPack(int N, int V, int[] v, int[] w){
// dp[i]表示容量为i时的最大价值
// 返回max(dp[0-V]
int[] dp = new int[V+1];
// 枚举每个物品
for (int i = 1; i <= N; i++) {
// 从大到小转移体积,保证状态转移方程
for (int j = V; j >= v[i]; j--) {
// 当前物品可以选0-k个
for (int k = 0; k * v[i] <= j; k++) {
dp[j] = Math.max(dp[j],dp[j-k*v[i]] + k * w[i]);
}
}
}
// 找最大值
int res = 0;
for (int i = 0; i <= V; i++) {
res = Math.max(res,dp[i]);
}
return res;
}

同时,也可以采用顺序遍历,这样相当于用当前的状态来进行更新,这样做的原因是假设最优解是包含k个当前物品,如果采用顺序遍历,若当前包含k-1个v[i],若要转移到dp[j],还需要一个v[i],则dp[j]=dp[j-v[i]]+w[i],然后dp[j-v[i]]又可以由dp[j-2*v[i]]递归而来,因此可以得到k个的结果,所以需要正序的遍历。

因为初始化的时候,dp[0-V]均初始化为0,dp[V]不一定是0转移而来,可能从任意数转移而来,可以代表体积不超过V的最大价值。

若题目问体积正好为V的最大价值,这时候在初始化的时候,将dp[0]初始化为0,其他初始化为负无穷即可,保证dp[V]只能由dp[0]转移而来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public int fullPack2(int N, int V, int[] v, int[] w) {
// dp[i]表示容量为i时的最大价值
// 返回max(dp[0-V])
int[] dp = new int[V + 1];
// 枚举每个物品
for (int i = 1; i <= N; i++) {
// 枚举体积
// 顺序来,由k-1个当前物品计算到包含k个
// 顺序是因为要由当前状态来更新
for (int j = v[i]; j <= V; j++) {
dp[j] = Math.max(dp[j],dp[j-v[i]]+w[i]);
}
}
return dp[V];
}

多重背包

多重背包1:

每个物品,可以选择的数量是有限的,由输入给出,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i种物品的体积、价值和数量。

思路:先01背包思路,遍历N个物品,然后倒序遍历体积用来保证从i-1状态转移而来,然后当前选择的物品数量可以从0-s[i],这样可以再加一层遍历,加上多个物品的选择。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public int mutiPack1(int N, int V, int[] v, int[] w, int[] s) {
// dp数组记录体积最大为V的最大价值
int[] dp = new int[V+1];
// 遍历物品
for (int i = 1; i <= N; i++) {
// 逆序遍历物品
for (int j = V; j >= v[i]; j--) {
// 0-s[i]体积
for (int k = 1; k <= s[i] && j >= k * v[i]; k++) {
dp[j] = Math.max(dp[j],dp[j-k*v[i]]+k*w[i]);
}
}
}
return dp[V];
}

多重背包2:优化解法

在解法1中,对每个物品需要有s的遍历,这样整体的时间复杂度是N*V*S,可以对S进行优化。如果要将多重背包转化为0 1背包,可以将数量为S的物品拆分为S个物品,但这样时间复杂度没有得到优化。借用这个思想,完全表示大小为S的数,最少需要log(S)个数,这样可以将S用1 2 4 8…数来完全表示,一个数如果不是2的整数次幂,可以将剩下的数进行拆解,这样1 2 4 8 …可以完全表示比S小的最近的一个数,然后将剩下的偏移量单独用一个数表示,这样就可以基于这些数将S进行全表示,然后可以将S进行拆分,这样就转化为了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
24
25
26
27
28
29
30
31
32
33
34
35
class Pair{
int v;
int w;

public Pair(int v, int w) {
this.v = v;
this.w = w;
}
}
private int mutiPack2(int N, int V, int[] v, int[] w, int[] s) {
// 将s个物品拆分,转换为0,1背包问题
// 拆成1 2 4 8 ...,不够的直接放进去
List list = new ArrayList<>();
// 拆物品
for (int i = 1; i <= N; i++) {
for (int k = 1; k <= s[i]; k *= 2) {
// key是体积,value是价值
s[i] -= k;
list.add(new Pair(k*v[i],k*w[i]));
}
// 剩下的放进去
if (s[i] > 0){
list.add(new Pair(s[i]*v[i],s[i]*w[i]));
}
}
// 0 1背包
int[] dp = new int[V+1];
for (Pair pair : list) {
// 体积从大到小
for (int i = V; i >= pair.v; i--) {
dp[i] = Math.max(dp[i],dp[i-pair.v] + pair.w);
}
}
return dp[V];
}

混合背包

物品一共有三类:

  • 第一类物品只能用1次(01背包);
  • 第二类物品可以用无限次(完全背包);
  • 第三类物品最多只能用 si 次(多重背包);

每种体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。

混合背包相当于0 1背包与完全背包和多重背包的组合问题,0 1背包与完全背包在代码中只有遍历体积顺序的不同,而多重背包可以基于二进制计数来进行优化变为 0 1背包,这样先将物品进行拆解,简化为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
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Pair{
int v;
int w;
int s;
public Pair(int v, int w, int s) {
this.v = v;
this.w = w;
this.s = s;
}
}
private int mixPack(int N, int V, int[] v, int[] w, int[] s) {
// 将物品拆分
List list = new ArrayList<>();
for (int i = 1; i <= N; i++) {
if (s[i] < 0){
list.add(new Pair(v[i],w[i],-1));
}else if (s[i] == 0){
list.add(new Pair(v[i],w[i],0));
} else {
// 拆分
for (int k = 1; k <= s[i]; k*=2) {
s[i] -= k;
list.add(new Pair(k*v[i],k*w[i],-1));
}
if (s[i] > 0){
list.add(new Pair(s[i]*v[i],s[i]*w[i],-1));
}
}
}
// 转为0 1背包
int[] dp = new int[V+1];
// 遍历商品
for (Pair pair : list) {
// 如果为0 1,逆序遍历,如果为完全,顺序遍历
if (pair.s == -1){
for (int i = V; i >= pair.v; i--) {
dp[i] = Math.max(dp[i],dp[i-pair.v]+pair.w);
}
}else {
for (int i = pair.v; i <=V ; i++) {
dp[i] = Math.max(dp[i],dp[i-pair.v]+pair.w);
}
}
}
return dp[V];
}

二维费用的背包

有 N件物品和一个容量是 V 的背包,背包能承受的最大重量是 M。

每件物品只能用一次。体积是 vi,重量是 mi,价值是 wi。

求解将哪些物品装入背包,可使物品总体积不超过背包容量,总重量不超过背包可承受的最大重量,且价值总和最大。
输出最大价值。

输入格式

第一行两个整数,N,V,M用空格隔开,分别表示物品件数、背包容积和背包可承受的最大重量。

接下来有 N行,每行三个整数 vi,mi,wi用空格隔开,分别表示第 i件物品的体积、重量和价值。

思路:将dp维度从一维增加到二维即可,dp[i][j]表示体积最大为i,重量最大为j的最大价值,三种循环,第一层循环N件物品,第二层从大到小循环体积,第三层从大到小循环重量,本质还是0 1背包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private int twoDimensionPack(int N, int V, int M, int[] v, int[] m, int[] w) {
// 三重循环即可
int[][] dp = new int[V+1][M+1];
// choose things
for (int i = 1; i <= N; i++) {
// 逆序选体积
for (int j = V; j >= v[i]; j--) {
// 逆序选重量
for (int k = M; k >= m[i]; k--) {
dp[j][k] = Math.max(dp[j][k],dp[j-v[i]][k-m[i]]+w[i]);
}
}
}
return dp[V][M];
}

分组背包

有 N组物品和一个容量是 V 的背包。

每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 vij,价值是 wij,其中 i是组号,j是组内编号。

求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。

输出最大价值。

思路:有N个组,每个组要么不选物品,要么从所有物品中选择一个,其实本质就是0 1背包。第一重循环遍历N组,第二重从大到小遍历体积,第三重循环每个物品。

dp数组为dp[i][j],表示为第i组的物品,容量是j的最大价值。

状态转移方程为

1
2
dp[i][j]=dp[i-1][j];// 不选当前组的物品
dp[i][j]=max(dp[i][j],dp[i-1][j-vi]+wi);// 选一个物品
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
private int multiGroupPack(int N, int V, int[][] v, int[][] w) {
// 每个组要么不选,要么选第一个,第二个...
int[][] dp = new int[N+1][V+1];// N个组体积最大为V的最大价值
// 遍历每个组
for (int i = 1; i <= N; i++) {
// 遍历体积
for (int j = V; j >= 0; j--) {
// 一个商品都不选
dp[i][j] = dp[i-1][j];
// 遍历当前组的商品,k个商品
for (int k = 0; k < v[i].length; k++) {
// 每个商品都选一遍
if (j >= v[i][k]){
// 选择当前的商品
dp[i][j] = Math.max(dp[i][j],dp[i-1][j-v[i][k]] + w[i][k]);
}
}
}
}
// 找最大值
int res = 0;
for (int i = 0; i <= V; i++) {
res = Math.max(res,dp[N][i]);
}
return res;
}

背包问题求方案数

求背包问题的方案

有依赖的背包问题