部分数据结构与算法分析

排序

插入排序

直接插入排序

直接插入排序基本思想是每一步将一个待排序的记录,插入到前面已经排好序的有序序列中去,直到插完所有元素为止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 插入排序
*
* @param arr
*/
public static void insertionSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int j = i;
while (j > 0 && arr[j] < arr[j - 1]) {
swap(arr,j,j-1);
j--;
}
}
}

简单插入排序在最好情况下,需要比较n-1次,无需交换元素,时间复杂度为O(n);在最坏情况下,时间复杂度依然为O(n2)。
但是在数组元素随机排列的情况下,插入排序还是要优于上面两种排序的。


折半插入排序

折半插入排序是对直接插入排序的简单改进,对于直接插入排序而言,当第i-1趟需要将第i个元素插入前面的0~i-1个元素序列中时,总是需要从i-1个元素开始,逐个比较每个元素,直到找到它的位置。这显然没有利用前面0~i-1个元素已经有序这个特点,而折半插入排序则改进了这一点。

对于折半插入排序而言,当需要插入第i个元素时,它不会逐个进行比较每个元素,而是:

  • (1)计算0~i-1索引的中间点,也就是用i索引处的元素和(0+i-1)/2索引处的元素进行比较,如果i索引处的元素值大,就直接在(0+i-1)/2~i-1半个范围内进行搜索;反之在0~(0+i-1)/2半个范围内搜索,这就是所谓的折半;
  • (2)在半个范围内搜索时,按照(1)的方法不断地进行折半搜索,这样就可以将搜索范围缩小到1/2、1/4、1/8…,从而快速的确定插入位置;

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
package com.liuhao.sort;

import java.util.Arrays;

public class BinaryInsertSort {

public static void binaryInsertSort(DataWrap[] data){
int arrayLength = data.length;

for(int i=1; i<arrayLength; i++){
DataWrap tmp = data[i];

int low = 0;
int high = i-1;

// 不断折半,寻找合适的插入位置
while(low <= high){
int mid = (low + high) / 2;

if(tmp.compareTo(data[mid]) > 0){
low = mid + 1;
}
else{
high = mid - 1;
}
}

// 依次后移
for(int j=i; j>low ; j--){
data[j] = data[j-1];
}

data[low] = tmp;
System.out.println(Arrays.toString(data));
}
}

public static void main(String[] args) {
DataWrap[] data = {
new DataWrap(21, "")
,new DataWrap(30, "")
,new DataWrap(49, "")
,new DataWrap(30, "*")
,new DataWrap(16, "")
,new DataWrap(9, "") };

System.out.println("排序之前:" + Arrays.toString(data));

binaryInsertSort(data);

System.out.println("排序之后:" + Arrays.toString(data));
}

}

折半插入排序减少了关键字的比较次数,但是记录的移动次数不变,其时间复杂度与直接插入排序相同。


希尔排序

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
package sortdemo;

import java.util.Arrays;

/**
* Created by chengxiao on 2016/11/24.
*/
public class ShellSort {
public static void main(String []args){
int []arr ={1,4,2,7,9,8,3,6};
sort(arr);
System.out.println(Arrays.toString(arr));
int []arr1 ={1,4,2,7,9,8,3,6};
sort1(arr1);
System.out.println(Arrays.toString(arr1));
}

/**
* 希尔排序 针对有序序列在插入时采用交换法
* @param arr
*/
public static void sort(int []arr){
//增量gap,并逐步缩小增量
for(int gap=arr.length/2;gap>0;gap/=2){
//从第gap个元素,逐个对其所在组进行直接插入排序操作
for(int i=gap;i<arr.length;i++){
int j = i;
while(j-gap>=0 && arr[j]<arr[j-gap]){
//插入排序采用交换法
swap(arr,j,j-gap);
j-=gap;
}
}
}
}

/**
* 希尔排序 针对有序序列在插入时采用移动法。
* @param arr
*/
public static void sort1(int []arr){
//增量gap,并逐步缩小增量
for(int gap=arr.length/2;gap>0;gap/=2){
//从第gap个元素,逐个对其所在组进行直接插入排序操作
for(int i=gap;i<arr.length;i++){
int j = i;
int temp = arr[j];
if(arr[j]<arr[j-gap]){
while(j-gap>=0 && temp<arr[j-gap]){
//移动法
arr[j] = arr[j-gap];
j-=gap;
}
arr[j] = temp;
}
}
}
}
/**
* 交换数组元素
* @param arr
* @param a
* @param b
*/
public static void swap(int []arr,int a,int b){
arr[a] = arr[a]+arr[b];
arr[b] = arr[a]-arr[b];
arr[a] = arr[a]-arr[b];
}
}




交换排序

冒泡排序

冒泡排序的基本思想是,对相邻的元素进行两两比较,顺序相反则进行交换,这样,每一趟会将最小或最大的元素“浮”到顶端,最终达到完全有序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 冒泡排序
*
* @param arr
*/
public static void bubbleSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
boolean flag = true;//设定一个标记,若为true,则表示此次循环没有进行交换,也就是待排序列已经有序,排序已然完成。
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr,j,j+1);
flag = false;
}
}
if (flag) {
break;
}
}
}

根据上面这种冒泡实现,若原数组本身就是有序的(这是最好情况),仅需n-1次比较就可完成;若是倒序,比较次数为 n-1+n-2+…+1=n(n-1)/2,交换次数和比较次数等值。
所以其时间复杂度依然为O(n2)。

综合来看冒泡排序性能还还是稍差于上面那种选择排序的。


快速排序

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
/*
* 快速排序
*
* 参数说明:
* a -- 待排序的数组
* l -- 数组的左边界(例如,从起始位置开始排序,则l=0)
* r -- 数组的右边界(例如,排序截至到数组末尾,则r=a.length-1)
*/
void quick_sort(int a[], int l, int r)
{
if (l < r)
{
int i,j,x;

i = l;
j = r;
x = a[i];
while (i < j)
{
while(i < j && a[j] > x)
j--; // 从右向左找第一个小于x的数
if(i < j)
a[i++] = a[j];
while(i < j && a[i] < x)
i++; // 从左向右找第一个大于x的数
if(i < j)
a[j--] = a[i];
}
a[i] = x;
quick_sort(a, l, i-1); /* 递归调用 */
quick_sort(a, i+1, r); /* 递归调用 */
}
}

快速排序稳定性

快速排序是不稳定的算法,它不满足稳定算法的定义。
算法稳定性 – 假设在数列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;并且排序之后,a[i]仍然在a[j]前面。则这个排序算法是稳定的!

快速排序时间复杂度

快速排序的时间复杂度在最坏情况下是O(N2),平均的时间复杂度是O(N*lgN)。
这句话很好理解:假设被排序的数列中有N个数。遍历一次的时间复杂度是O(N),需要遍历多少次呢?至少lg(N+1)次,最多N次。

  • (01) 为什么最少是lg(N+1)次?快速排序是采用的分治法进行遍历的,我们将它看作一棵二叉树,它需要遍历的次数就是二叉树的深度,而根据完全二叉树的定义,它的深度至少是lg(N+1)。因此,快速排序的遍历次数最少是lg(N+1)次。
  • (02) 为什么最多是N次?这个应该非常简单,还是将快速排序看作一棵二叉树,它的深度最大是N。因此,快读排序的遍历次数最多是N次。




选择排序

简单选择排序

简单选择排序是最简单直观的一种算法,基本思想为每一趟从待排序的数据元素中选择最小(或最大)的一个元素作为首元素,直到所有元素排完为止,简单选择排序是不稳定排序。

在算法实现时,每一趟确定最小元素的时候会通过不断地比较交换来使得首位置为当前最小,交换是个比较耗时的操作。其实我们很容易发现,在还未完全确定当前最小元素之前,这些交换都是无意义的。我们可以通过设置一个变量min,每一次比较仅存储较小元素的数组下标,当轮循环结束之后,那这个变量存储的就是当前最小元素的下标,此时再执行交换操作即可。代码实现很简单,一起来看下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 简单选择排序
*
* @param arr
*/
public static void selectSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
int min = i;//每一趟循环比较时,min用于存放较小元素的数组下标,这样当前批次比较完毕最终存放的就是此趟内最小的元素的下标,避免每次遇到较小元素都要进行交换。
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[min]) {
min = j;
}
}
//进行交换,如果min发生变化,则进行交换
if (min != i) {
swap(arr,min,i);
}
}
}

堆排序

堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆

堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-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
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
package sortdemo;

import java.util.Arrays;

/**
* Created by chengxiao on 2016/12/17.
* 堆排序demo
*/
public class HeapSort {
public static void main(String []args){
int []arr = {9,8,7,6,5,4,3,2,1};
sort(arr);
System.out.println(Arrays.toString(arr));
}
public static void sort(int []arr){
//1.构建大顶堆
for(int i=arr.length/2-1;i>=0;i--){
//从第一个非叶子结点从下至上,从右至左调整结构
adjustHeap(arr,i,arr.length);
}
//2.调整堆结构+交换堆顶元素与末尾元素
for(int j=arr.length-1;j>0;j--){
swap(arr,0,j);//将堆顶元素与末尾元素进行交换
adjustHeap(arr,0,j);//重新对堆进行调整
}

}

/**
* 调整大顶堆(仅是调整过程,建立在大顶堆已构建的基础上)
* @param arr
* @param i
* @param length
*/
public static void adjustHeap(int []arr,int i,int length){
int temp = arr[i];//先取出当前元素i
for(int k=i*2+1;k<length;k=k*2+1){//从i结点的左子结点开始,也就是2i+1处开始
if(k+1<length && arr[k]<arr[k+1]){//如果左子结点小于右子结点,k指向右子结点
k++;
}
if(arr[k] >temp){//如果子节点大于父节点,将子节点值赋给父节点(不用进行交换)
arr[i] = arr[k];
i = k;
}else{
break;
}
}
arr[i] = temp;//将temp值放到最终的位置
}

/**
* 交换元素
* @param arr
* @param a
* @param b
*/
public static void swap(int []arr,int a ,int b){
int temp=arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
}


计数排序

计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

算法的步骤如下

  • (1)找出待排序的数组中最大和最小的元素
  • (2)统计数组中每个值为i的元素出现的次数,存入数组C的第i项
  • (3)对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
  • (4)反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class CountingSort implements IArraySort {

@Override
public int[] sort(int[] sourceArray) throws Exception {
// 对 arr 进行拷贝,不改变参数内容
int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);

int maxValue = getMaxValue(arr);

return countingSort(arr, maxValue);
}

private int[] countingSort(int[] arr, int maxValue) {
int bucketLen = maxValue + 1;
int[] bucket = new int[bucketLen];

for (int value : arr) {
bucket[value]++;
}

int sortedIndex = 0;
for (int j = 0; j < bucketLen; j++) {
while (bucket[j] > 0) {
arr[sortedIndex++] = j;
bucket[j]--;
}
}
return arr;
}

private int getMaxValue(int[] arr) {
int maxValue = arr[0];
for (int value : arr) {
if (maxValue < value) {
maxValue = value;
}
}
return maxValue;
}

}

桶排序

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:

在额外空间充足的情况下,尽量增大桶的数量
使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。

  1. 什么时候最快

当输入的数据可以均匀的分配到每一个桶中。

  1. 什么时候最慢

当输入的数据被分配到了同一个桶中。

元素分布在桶中

元素在每个桶中排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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
public class BucketSort implements IArraySort {

private static final InsertSort insertSort = new InsertSort();

@Override
public int[] sort(int[] sourceArray) throws Exception {
// 对 arr 进行拷贝,不改变参数内容
int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);

return bucketSort(arr, 5);
}

private int[] bucketSort(int[] arr, int bucketSize) throws Exception {
if (arr.length == 0) {
return arr;
}

int minValue = arr[0];
int maxValue = arr[0];
for (int value : arr) {
if (value < minValue) {
minValue = value;
} else if (value > maxValue) {
maxValue = value;
}
}

int bucketCount = (int) Math.floor((maxValue - minValue) / bucketSize) + 1;
int[][] buckets = new int[bucketCount][0];

// 利用映射函数将数据分配到各个桶中
for (int i = 0; i < arr.length; i++) {
int index = (int) Math.floor((arr[i] - minValue) / bucketSize);
buckets[index] = arrAppend(buckets[index], arr[i]);
}

int arrIndex = 0;
for (int[] bucket : buckets) {
if (bucket.length <= 0) {
continue;
}
// 对每个桶进行排序,这里使用了插入排序
bucket = insertSort.sort(bucket);
for (int value : bucket) {
arr[arrIndex++] = value;
}
}

return arr;
}

/**
* 自动扩容,并保存数据
*
* @param arr
* @param value
*/
private int[] arrAppend(int[] arr, int value) {
arr = Arrays.copyOf(arr, arr.length + 1);
arr[arr.length - 1] = value;
return 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
package sortdemo;

import java.util.Arrays;

/**
* Created by chengxiao on 2016/12/8.
*/
public class MergeSort {
public static void main(String []args){
int []arr = {9,8,7,6,5,4,3,2,1};
sort(arr);
System.out.println(Arrays.toString(arr));
}
public static void sort(int []arr){
int []temp = new int[arr.length];//在排序前,先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间
sort(arr,0,arr.length-1,temp);
}
private static void sort(int[] arr,int left,int right,int []temp){
if(left<right){
int mid = (left+right)/2;
sort(arr,left,mid,temp);//左边归并排序,使得左子序列有序
sort(arr,mid+1,right,temp);//右边归并排序,使得右子序列有序
merge(arr,left,mid,right,temp);//将两个有序子数组合并操作
}
}
private static void merge(int[] arr,int left,int mid,int right,int[] temp){
int i = left;//左序列指针
int j = mid+1;//右序列指针
int t = 0;//临时数组指针
while (i<=mid && j<=right){
if(arr[i]<=arr[j]){
temp[t++] = arr[i++];
}else {
temp[t++] = arr[j++];
}
}
while(i<=mid){//将左边剩余元素填充进temp中
temp[t++] = arr[i++];
}
while(j<=right){//将右序列剩余元素填充进temp中
temp[t++] = arr[j++];
}
t = 0;
//将temp中的元素全部拷贝到原数组中
while(left <= right){
arr[left++] = temp[t++];
}
}
}

基数排序

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
/**
* 基数排序
* 考虑负数的情况还可以参考: https://code.i-harness.com/zh-CN/q/e98fa9
*/
public class RadixSort implements IArraySort {

@Override
public int[] sort(int[] sourceArray) throws Exception {
// 对 arr 进行拷贝,不改变参数内容
int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);

int maxDigit = getMaxDigit(arr);
return radixSort(arr, maxDigit);
}

/**
* 获取最高位数
*/
private int getMaxDigit(int[] arr) {
int maxValue = getMaxValue(arr);
return getNumLenght(maxValue);
}

private int getMaxValue(int[] arr) {
int maxValue = arr[0];
for (int value : arr) {
if (maxValue < value) {
maxValue = value;
}
}
return maxValue;
}

protected int getNumLenght(long num) {
if (num == 0) {
return 1;
}
int lenght = 0;
for (long temp = num; temp != 0; temp /= 10) {
lenght++;
}
return lenght;
}

private int[] radixSort(int[] arr, int maxDigit) {
int mod = 10;
int dev = 1;

for (int i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
// 考虑负数的情况,这里扩展一倍队列数,其中 [0-9]对应负数,[10-19]对应正数 (bucket + 10)
int[][] counter = new int[mod * 2][0];

for (int j = 0; j < arr.length; j++) {
int bucket = ((arr[j] % mod) / dev) + mod;
counter[bucket] = arrayAppend(counter[bucket], arr[j]);
}

int pos = 0;
for (int[] bucket : counter) {
for (int value : bucket) {
arr[pos++] = value;
}
}
}

return arr;
}

/**
* 自动扩容,并保存数据
*
* @param arr
* @param value
*/
private int[] arrayAppend(int[] arr, int value) {
arr = Arrays.copyOf(arr, arr.length + 1);
arr[arr.length - 1] = value;
return arr;
}
}




数组

二分法

总结

链表

链表相加

21. 合并两个有序链表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

1
2
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,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
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
// 类似归并排序中的合并过程
ListNode dummyHead = new ListNode(0);
ListNode cur = dummyHead;
while (l1 != null && l2 != null) {
if (l1.val < l2.val) {
cur.next = l1;
cur = cur.next;
l1 = l1.next;
} else {
cur.next = l2;
cur = cur.next;
l2 = l2.next;
}
}
// 任一为空,直接连接另一条链表
if (l1 == null) {
cur.next = l2;
} else {
cur.next = l1;
}
return dummyHead.next;
}
}

23. 合并K个升序链表

给你一个链表数组,每个链表都已经按升序排列。

请你将所有链表合并到一个升序链表中,返回合并后的链表。

1
2
3
4
5
6
7
8
9
10
输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
1->4->5,
1->3->4,
2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Solution {
public ListNode mergeKLists(ListNode[] lists) {

if (lists.length == 0) {
return null;
}

ListNode dummyHead = new ListNode(0);
ListNode curr = dummyHead;
PriorityQueue<ListNode> pq = new PriorityQueue<>(new Comparator<ListNode>() {
@Override
public int compare(ListNode o1, ListNode o2) {
return o1.val - o2.val;
}
});

for (ListNode list : lists) {
if (list == null) {
continue;
}
pq.add(list);
}

while (!pq.isEmpty()) {
ListNode nextNode = pq.poll();
curr.next = nextNode;
curr = curr.next;
if (nextNode.next != null) {
pq.add(nextNode.next);
}
}
return dummyHead.next;
}
}


两数相加

2.两数相加

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

1
2
3
4
5
6
7
8
9
输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807

输入:l1 = [0], l2 = [0]
输出:[0]

输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
输出:[8,9,9,9,0,0,0,1]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode root = new ListNode(0);
ListNode cursor = root;
int carry = 0;
while(l1 != null || l2 != null || carry != 0) {
int l1Val = l1 != null ? l1.val : 0;
int l2Val = l2 != null ? l2.val : 0;
int sumVal = l1Val + l2Val + carry;
carry = sumVal / 10;

ListNode sumNode = new ListNode(sumVal % 10);
cursor.next = sumNode;
cursor = sumNode;

if(l1 != null) l1 = l1.next;
if(l2 != null) l2 = l2.next;
}

return root.next;
}
}

445. 两数相加 II

给你两个 非空 链表来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储一位数字。将这两数相加会返回一个新的链表。
你可以假设除了数字 0 之外,这两个数字都不会以零开头。

1
2
3
4
5
6
7
8
输入:l1 = [7,2,4,3], l2 = [5,6,4]
输出:[7,8,0,7]

输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[8,0,7]

输入:l1 = [0], l2 = [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
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
Stack<Integer> stack1 = new Stack<>();
Stack<Integer> stack2 = new Stack<>();
while (l1 != null) {
stack1.push(l1.val);
l1 = l1.next;
}
while (l2 != null) {
stack2.push(l2.val);
l2 = l2.next;
}

int carry = 0;
ListNode head = null;
while (!stack1.isEmpty() || !stack2.isEmpty() || carry > 0) {
int sum = carry;
sum += stack1.isEmpty()? 0: stack1.pop();
sum += stack2.isEmpty()? 0: stack2.pop();
ListNode node = new ListNode(sum % 10);
node.next = head;
head = node;
carry = sum / 10;
}
return head;
}
}




链表反转

206. 反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

1
2
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
/**
* 迭代方法
* 1 -> 2 -> 3 -> 4 -> null
* null <- 1 <- 2 <- 3 <- 4
*
* @param head
* @return
*/
public ListNode reverseList(ListNode head) {
ListNode prev = null; //前指针节点
ListNode curr = head; //当前指针节点
//每次循环,都将当前节点指向它前面的节点,然后当前节点和前节点后移
while (curr != null) {
ListNode nextTemp = curr.next; //临时节点,暂存当前节点的下一节点,用于后移
curr.next = prev; //将当前节点指向它前面的节点
prev = curr; //前指针后移
curr = nextTemp; //当前指针后移
}
return prev;
}
}

92. 反转链表 II

给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。

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 {
public:

ListNode * successor = nullptr;

ListNode* reverseN(ListNode* head, int end) {
if (head->next == NULL || end == 1) {
successor = head->next;
return head;
};
ListNode* last = reverseN(head->next, end - 1);
head->next->next = head;
head->next = successor;
return last;
}


ListNode* reverseBetween(ListNode* head, int start, int end){
if (start == 1){
return reverseN(head, end);
}
head->next = reverseBetween(head->next, start-1, end-1);
return head;
}
};

总结

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

ListNode* reverse(ListNode* head) {
if (head->next == NULL) return head;
ListNode* last = reverse(head->next);
head->next->next = head;
head->next= nullptr;
return last;
}

ListNode * successor = nullptr;

ListNode* reverseN(ListNode* head, int end) {
if (head->next == NULL || end == 1) {
successor = head->next;
return head;
};
ListNode* last = reverseN(head->next, end - 1);
head->next->next = head;
head->next = successor;
return last;
}

ListNode* reverseAll(ListNode* head){
if (head->next == nullptr){
return head;
}
ListNode* last = reverseAll(head->next);
head->next->next = head;
head->next = nullptr;
return last;
}

ListNode* reverseNEnd(ListNode* head, int n){
if (n == 1){
return reverseAll(head);
}
head->next = reverseNEnd(head->next, n-1);
return head;
}

ListNode* reverseBetween(ListNode* head, int start, int end){
if (start == 1){
return reverseN(head, end);
}
head->next = reverseBetween(head->next, start-1, end-1);
return head;
}

ListNode* reverseNGroup(ListNode* head, int end) {
if (head->next == NULL || end == 1) {
successor = head->next;
return head;
};
ListNode* last = reverseN(head->next, end - 1);
head->next->next = head;
head->next = successor;
return last;
}


链表旋转

61. 旋转链表

给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。

1
2
输入:head = [1,2,3,4,5], k = 2
输出:[4,5,1,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
class Solution {
public ListNode rotateRight(ListNode head, int k) {
if (head == null || head.next == null || k == 0) return head;

int count = 1; // 用来统计链表总结点数
ListNode tmp = head;
while (tmp.next != null) {
count++;
tmp = tmp.next;
}
k %= count;
// 当k为0时,不需要旋转,
if (k == 0) return head;

// 不满足上述条件,必将进行旋转,所以先将首尾相连
tmp.next = head;
// 现在只需要找到倒数第k+1个节点
for (int i = 0; i < count - k; i++) {
tmp = tmp.next;
}
ListNode newHead = tmp.next;
tmp.next = null;
return newHead;
}
}


判断环形

141. 环形链表

给你一个链表的头节点 head ,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

如果链表中存在环,则返回 true 。 否则,返回 false 。

1
2
3
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

快慢指针

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
bool hasCycle(ListNode* head) {
ListNode* fast = head, * slow = head;
while (fast && fast->next) {
fast = fast->next->next;
slow = slow->next;
if (slow == fast)
return true;
}
return false;
}
};

142. 环形链表 II

给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表。

1
2
3
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {// 有环
ListNode index1 = fast;
ListNode index2 = head;
// 两个指针,从头结点和相遇结点,各走一步,直到相遇,相遇点即为环入口
while (index1 != index2) {
index1 = index1.next;
index2 = index2.next;
}
return index1;
}
}
return null;
}
}


链表划分

725. 分隔链表

给你一个头结点为 head 的单链表和一个整数 k ,请你设计一个算法将链表分隔为 k 个连续的部分。
每部分的长度应该尽可能的相等:任意两部分的长度差距不能超过 1 。这可能会导致有些部分为 null 。

这 k 个部分应该按照在链表中出现的顺序排列,并且排在前面的部分的长度应该大于或等于排在后面的长度。
返回一个由上述 k 部分组成的数组。

1
2
3
4
输入:head = [1,2,3,4,5,6,7,8,9,10], k = 3
输出:[[1,2,3,4],[5,6,7],[8,9,10]]
解释:
输入被分成了几个连续的部分,并且每部分的长度相差不超过 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
class Solution {
public ListNode[] splitListToParts(ListNode head, int k) {
// 1. 求长度
int len = 0;
ListNode p = head;
while(p != null) {
len++;
p = p.next;
}
// 2. 求具体数量, 前 b 个长度是 a + 1
int a = len / k, b = len % k;
// 3. 添加
p = head;
ListNode[] ans = new ListNode[k];
for(int i = 0; i < k; ++i) {
ans[i] = p;
if(p == null) continue; // ans[i] = [], 则剩余的将都是 []
ListNode temp = p;
for(int j = 1; j < a + (b > 0? 1 : 0); ++j) { // 已经添加了一个
temp = temp.next;
}
p = temp.next; // 因为 len = a * k + b, temp 一定不是 null
temp.next = null;
b--; // 最小值是 len % k - k, 不会出现下越界的情况
}
return ans;
}
}

链表判&去重

234. 回文链表

给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。

1
2
输入:head = [1,2,2,1]
输出: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
30
31
32
33
34
35
class Solution {
public boolean isPalindrome(ListNode head) {
// 要实现 O(n) 的时间复杂度和 O(1) 的空间复杂度,需要翻转后半部分
if (head == null || head.next == null) {
return true;
}
ListNode fast = head;
ListNode slow = head;
// 根据快慢指针,找到链表的中点
while(fast.next != null && fast.next.next != null) {
fast = fast.next.next;
slow = slow.next;
}
slow = reverse(slow.next);
while(slow != null) {
if (head.val != slow.val) {
return false;
}
head = head.next;
slow = slow.next;
}
return true;
}

private ListNode reverse(ListNode head){
// 递归到最后一个节点,返回新的新的头结点
if (head.next == null) {
return head;
}
ListNode newHead = reverse(head.next);
head.next.next = head;
head.next = null;
return newHead;
}
}

字符串

字符串变换查找

滑动窗口

3.无重复字符的最长字串

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

1
2
3
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
1
2
3
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
1
2
3
4
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
  请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public int lengthOfLongestSubstring(String s) {
// 记录字符上一次出现的位置
int[] last = new int[128];
for(int i = 0; i < 128; i++) {
last[i] = -1;
}
int n = s.length();

int res = 0;
int start = 0; // 窗口开始位置
for(int i = 0; i < n; i++) {
int index = s.charAt(i);
start = Math.max(start, last[index] + 1);
res = Math.max(res, i - start + 1);
last[index] = i;
}

return res;
}
}


76.最小覆盖子串

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。

注意
对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
如果 s 中存在这样的子串,我们保证它是唯一的答案

1
2
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
1
2
输入:s = "a", t = "a"
输出:"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
class Solution {
public:
string minWindow(string s, string t) {
unordered_map<char, int> h;
for (const auto&c : t) h[c]++;

int n = s.size();
int cnt = 0;
int i = 0, j = 0;
int len = INT_MAX;
int minst = 0;

while (j < n) {
char c = s[j];
if (h.count(c)) {
h[c]--;
if (h[c] == 0) cnt++;
}

while (cnt == h.size()) {
int size = j - i + 1;
if (size < len) {
len = size;
minst = i;
}

char b = s[i];
if (h.count(b)) {
h[b]++;
if (h[b] > 0) cnt--;
}
i++;
}

j++;
}

string res = len == INT_MAX ? "" : s.substr(minst, len);
return res;
}
};


567.字符串的排列

给你两个字符串 s1 和 s2 ,写一个函数来判断 s2 是否包含 s1 的排列。如果是,返回 true;否则,返回 false 。
换句话说,s1 的排列之一是 s2 的 子串 。

1
2
3
输入:s1 = "ab" s2 = "eidbaooo"
输出:true
解释:s2 包含 s1 的排列之一 ("ba").
1
2
输入:s1= "ab" s2 = "eidboaoo"
输出:false
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
class Solution {
public:
bool checkInclusion(string s1, string s2) {
// 排除异常的边界情况,也限定了模式串的长度
if(s1.size() > s2.size()) return false;

// 匹配采用的窗口大小为模式串大小
int windowSize = s1.size();

// 模式串的字典:可以看做一种频率分布
vector<int> hashmap1(26, 0);
// 动态更新的匹配窗口字典
vector<int> hashmap2(26, 0);

// 构建字典
for(int i = 0; i < windowSize; i++) {
hashmap1[s1[i] - 'a']++;
hashmap2[s2[i] - 'a']++;
}

// 对于每一轮滑窗查询,如果两个字典相等(频率分布一致),则命中
for(int i = windowSize; i < s2.size(); i++) {
// 两个字典相等(频率分布一致),则命中
if(hashmap1 == hashmap2) return true;

// 否则,向右滑窗:滑窗对于 hash 表的操作变为对应频率的增减
hashmap2[s2[i - windowSize] - 'a']--;
hashmap2[s2[i] - 'a']++;
}

// 整个算法采用左闭右开区间,因此最后还有一个窗口没有判断
return hashmap1 == hashmap2;
}
};


239.滑动窗口最大值

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。

1
2
3
4
5
6
7
8
9
10
11
输入: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
1
2
输入:nums = [1], k = 1
输出:[1]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/*
思路: 遍历数组 L R 为滑窗左右边界 只增不减
双向队列保存当前窗口中最大的值的数组下标 双向队列中的数从大到小排序,
新进来的数如果大于等于队列中的数 则将这些数弹出 再添加
当R-L+1=k 时 滑窗大小确定 每次R前进一步L也前进一步 保证此时滑窗中最大值的
数组下标在[L,R]中,并将当前最大值记录
举例: nums[1,3,-1,-3,5,3,6,7] k=3
1:L=0,R=0,队列【0】 R-L+1 < k
队列代表值【1】
2: L=0,R=1, 队列【1】 R-L+1 < k
队列代表值【3】
解释:当前数为3 队列中的数为【1】 要保证队列中的数从大到小 弹出1 加入3
但队列中保存的是值对应的数组下标 所以队列为【1】 窗口长度为2 不添加记录
3: L=0,R=2, 队列【1,2】 R-L+1 = k ,result={3}
队列代表值【3,-1】
解释:当前数为-1 队列中的数为【3】 比队列尾值小 直接加入 队列为【3,-1】
窗口长度为3 添加记录记录为队首元素对应的值 result[0]=3
4: L=1,R=3, 队列【1,2,3】 R-L+1 = k ,result={3,3}
队列代表值【3,-1,-3】
解释:当前数为-3 队列中的数为【3,-1】 比队列尾值小 直接加入 队列为【3,-1,-3】
窗口长度为4 要保证窗口大小为3 L+1=1 此时队首元素下标为1 没有失效
添加记录记录为队首元素对应的值 result[1]=3
5: L=2,R=4, 队列【4】 R-L+1 = k ,result={3,3,5}
队列代表值【5】
解释:当前数为5 队列中的数为【3,-1,-3】 保证从大到小 依次弹出添加 队列为【5】
窗口长度为4 要保证窗口大小为3 L+1=2 此时队首元素下标为4 没有失效
添加记录记录为队首元素对应的值 result[2]=5
依次类推 如果队首元素小于L说明此时值失效 需要弹出
*/
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if(nums==null||nums.length<2) return nums;
// 双向队列 保存当前窗口最大值的数组位置 保证队列中数组位置的数按从大到小排序
LinkedList<Integer> list = new LinkedList();
// 结果数组
int[] result = new int[nums.length-k+1];
for(int i=0;i<nums.length;i++){
// 保证从大到小 如果前面数小 弹出
while(!list.isEmpty()&&nums[list.peekLast()]<=nums[i]){
list.pollLast();
}
// 添加当前值对应的数组下标
list.addLast(i);
// 初始化窗口 等到窗口长度为k时 下次移动在删除过期数值
if(list.peek()<=i-k){
list.poll();
}
// 窗口长度为k时 再保存当前窗口中最大值
if(i-k+1>=0){
result[i-k+1] = nums[list.peek()];
}
}
return result;
}
}


异位词

242.有效的字母异位词

给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。

1
2
输入: s = "anagram", t = "nagaram"
输出: true
1
2
输入: s = "rat", t = "car"
输出: false
1
2
3
class Solution(object):
def isAnagram(self, s, t):
return sorted(s) == sorted(t)


438.找字符串中所有的字母异位词

给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。

1
2
3
4
5
输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。
1
2
3
4
5
6
输入: s = "abab", p = "ab"
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 "ab", 它是 "ab" 的异位词。
起始索引等于 1 的子串是 "ba", 它是 "ab" 的异位词。
起始索引等于 2 的子串是 "ab", 它是 "ab" 的异位词。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
if(s.size()<p.size())
return {};
int l = 0, r = -1;
vector<int> freq_s(26, 0), freq_p(26, 0), res;
// 初始化代码值
for( int i = 0 ; i < p.size() ; i++ ){
freq_p[p[i] - 'a' ]++;
freq_s[s[++r] - 'a' ]++;
}
if ( freq_s == freq_p )
res.push_back( l );
// 固定长度的滑动窗口
while( r < s.size()-1 ){
freq_s[s[++r] - 'a' ]++;
freq_s[s[l++] - 'a' ]--;
if ( freq_s == freq_p )
res.push_back( l );
}
return res;
}
};


旋转词

796.旋转字符串

给定两个字符串, A 和 B。
A 的旋转操作就是将 A 最左边的字符移动到最右边。 例如, 若 A = ‘abcde’,在移动一次之后结果就是’bcdea’ 。如果在若干次旋转操作之后,A 能变成B,那么返回True。

1
2
3
4
5
6
7
示例 1:
输入: A = 'abcde', B = 'cdeab'
输出: true

示例 2:
输入: A = 'abcde', B = 'abced'
输出: false
1
2
3
4
5
class Solution {
public boolean rotateString(String A, String B) {
return A.length() == B.length() && (A + A).contains(B);
}
}


58.最后一个单词的长度

给你一个字符串 s,由若干单词组成,单词前后用一些空格字符隔开。返回字符串中最后一个单词的长度。
单词 是指仅由字母组成、不包含任何空格字符的最大子字符串。

1
2
3
4
5
6
7
8
输入:s = "Hello World"
输出:5

输入:s = " fly me to the moon "
输出:4

输入:s = "luffy is still joyboy"
输出:6
1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public int lengthOfLastWord(String s) {
int length = 0;
for (int i = s.length() - 1; i >= 0; i--) {
if (s.charAt(i) != ' ') {
length++;
} else if (length != 0) {
return length;
}
}
return length;
}
}


逆序

344.反转字符串

编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。
不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。

1
2
3
4
5
6
输入:s = ["h","e","l","l","o"]
输出:["o","l","l","e","h"]


输入:s = ["H","a","n","n","a","h"]
输出:["h","a","n","n","a","H"]
1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
void swap(char[] s, int i, int j){
s[i] ^= s[j];
s[j] ^= s[i];
s[i] ^= s[j];
}

void reverseString(char[] s) {
for (int i = 0, j = s.length - 1; i < s.length/2; i++, j--) {
swap(s, i,j);
}
}
}

541.反转字符串2

给定一个字符串 s 和一个整数 k,从字符串开头算起,每计数至 2k 个字符,就反转这 2k 字符中的前 k 个字符。
如果剩余字符少于 k 个,则将剩余字符全部反转。
如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样。

1
2
3
4
5
输入:s = "abcdefg", k = 2
输出:"bacdfeg"

输入:s = "abcd", k = 2
输出:"bacd"
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 {
public String reverseStr(String s, int k) {
StringBuffer res = new StringBuffer();
int length = s.length();
int start = 0;
while (start < length) {
// 找到k处和2k处
StringBuffer temp = new StringBuffer();
// 与length进行判断,如果大于length了,那就将其置为length
int firstK = (start + k > length) ? length : start + k;
int secondK = (start + (2 * k) > length) ? length : start + (2 * k);

//无论start所处位置,至少会反转一次
temp.append(s.substring(start, firstK));
res.append(temp.reverse());

// 如果firstK到secondK之间有元素,这些元素直接放入res里即可。
if (firstK < secondK) { //此时剩余长度一定大于k。
res.append(s.substring(firstK, secondK));
}
start += (2 * k);
}
return res.toString();
}
}


动态规划问题

最长公共子串

14.最长公共前缀

编写一个函数来查找字符串数组中的最长公共前缀。
如果不存在公共前缀,返回空字符串 “”。

1
2
3
4
5
6
7
输入:strs = ["flower","flow","flight"]
输出:"fl"


输入:strs = ["dog","racecar","car"]
输出:""
解释:输入不存在公共前缀。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public String longestCommonPrefix(String[] strs) {
if(strs.length==0)return "";
//公共前缀比所有字符串都短,随便选一个先
String s=strs[0];
for (String string : strs) {
while(!string.startsWith(s)){
if(s.length()==0)return "";
//公共前缀不匹配就让它变短!
s=s.substring(0,s.length()-1);
}
}
return s;
}
}


最长公共子序列LCS

1143.最长公共子序列

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

例如,”ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

1
2
3
4
5
6
7
8
9
10
11
12
输入:text1 = "abcde", text2 = "ace" 
输出:3
解释:最长公共子序列是 "ace" ,它的长度为 3 。

输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc" ,它的长度为 3 。


输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int m = text1.length(), n = text2.length();
int[][] dp = new int[m + 1][n + 1];
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;
}
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j]);
dp[i][j] = Math.max(dp[i][j - 1], dp[i][j]);
}
}
return dp[m][n];
}
}


最长上升子序列

300. 最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

1
2
3
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
1
2
输入:nums = [0,1,0,3,2,3]
输出:4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
if (nums.size() <= 1) return nums.size();
vector<int> dp(nums.size(), 1);
int result = 0;
for (int i = 1; i < nums.size(); i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
}
if (dp[i] > result) result = dp[i]; // 取长的子序列
}
return result;
}
};


673.最长递增子序列的个数

给定一个未排序的整数数组,找到最长递增子序列的个数。

1
2
3
输入: [1,3,5,4,7]
输出: 2
解释: 有两个最长递增子序列,分别是 [1, 3, 4, 7] 和[1, 3, 5, 7]。
1
2
3
输入: [2,2,2,2,2]
输出: 5
解释: 最长递增子序列的长度是1,并且存在5个子序列的长度为1,因此输出5。
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 Solution {
public int findNumberOfLIS(int[] nums) {

if (nums.length == 0) {
return 0;
}

int[] dp = new int[nums.length];
int[] combination = new int[nums.length];

Arrays.fill(dp, 1);
Arrays.fill(combination, 1);

int max = 1, res = 0;

for (int i = 1; i < dp.length; i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
if (dp[j] + 1 > dp[i]) { //如果+1长于当前LIS 则组合数不变
dp[i] = dp[j] + 1;
combination[i] = combination[j];
} else if (dp[j] + 1 == dp[i]) { //如果+1等于当前LIS 则说明找到了新组合
combination[i] += combination[j];
}
}
}
max = Math.max(max, dp[i]);
}

for (int i = 0; i < nums.length; i++)
if (dp[i] == max) res += combination[i];

return res;
}
}


674. 最长连续递增序列

给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
连续递增的子序列 可以由两个下标 l 和 r(l < r)确定
如果对于每个l <= i < r,都有nums[i] < nums[i + 1],那么子序列[nums[l], nums[l + 1], ..., nums[r - 1], nums[r]]就是连续递增子序列。

1
2
3
4
输入:nums = [1,3,5,4,7]
输出:3
解释:最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。
1
2
3
输入:nums = [2,2,2,2,2]
输出:1
解释:最长连续递增序列是 [2], 长度为1。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
if (nums.size() == 0) return 0;
int result = 1;
vector<int> dp(nums.size() ,1);
for (int i = 0; i < nums.size() - 1; i++) {
if (nums[i + 1] > nums[i]) { // 连续记录
dp[i + 1] = dp[i] + 1;
}
if (dp[i + 1] > result) result = dp[i + 1];
}
return result;
}
};


括号匹配问题

22. 括号生成

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且有效的括号组合。

示例 1

1
2
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]

示例 2

1
2
输入:n = 1
输出:["()"]

每新增一对括号,就是在上一次的结果的各个位置插入一个”()”,用集合防止重复

1
2
3
4
5
6
7
8
9
10
class Solution(object):
def generateParenthesis(self, n):
result = {''}
for i in range(n):
temp = set()
for s in result: # 在上一次的结果的所有字符串的各个位置上插入'()'
for j in range(len(s) + 1):
temp.add(s[:j] + '()' + s[j:])
result = temp
return list(result)


32. 最长有效括号

给你一个只包含 ‘(‘ 和 ‘)’ 的字符串,找出最长有效(格式正确且连续)括号子串的长度。

1
2
3
输入:s = "(()"
输出:2
解释:最长有效括号子串是 "()"
1
2
3
输入:s = ")()())"
输出:4
解释:最长有效括号子串是 "()()"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution(object):
def longestValidParentheses(self, s):
length = len(s)
if length == 0:
return 0
dp = [0] * length
for i in range(1,length):
#当遇到右括号时,尝试向前匹配左括号
if s[i] == ')':
pre = i - dp[i-1] -1;
#如果是左括号,则更新匹配长度
if pre>=0 and s[pre] == '(':
dp[i] = dp[i-1] + 2
#处理独立的括号对的情形 类似()()、()(())
if pre>0:
dp[i] += dp[pre-1]
return max(dp);


总结


贪心与动态规划

动态规划

动态规划处理步骤

1
2
3
4
5
6
7
# 初始化 base case
dp[0][0][...] = base
# 进行状态转移
for 状态1 in 状态1的所有取值:
for 状态2 in 状态2的所有取值:
for ...
dp[状态1][状态2][...] = 求最值(选择1,选择2...)

那么既然知道了这是个动态规划问题,就要思考如何列出正确的状态转移方程?

  • 1、确定 base case,这个很简单,显然目标金额 amount 为 0 时算法返回 0,因为不需要任何硬币就已经凑出目标金额了。
  • 2、确定「状态」,也就是原问题和子问题中会变化的变量。由于硬币数量无限,硬币的面额也是题目给定的,只有目标金额会不断地向 base case 靠近,所以唯一的「状态」就是目标金额 amount。
  • 3、确定「选择」,也就是导致「状态」产生变化的行为。目标金额为什么变化呢,因为你在选择硬币,你每选择一枚硬币,就相当于减少了目标金额。所以说所有硬币的面值,就是你的「选择」。
  • 4、明确 dp 函数/数组的定义。我们这里讲的是自顶向下的解法,所以会有一个递归的 dp 函数,一般来说函数的参数就是状态转移中会变化的量,也就是上面说到的「状态」;函数的返回值就是题目要求我们计算的量。就本题来说,状态只有一个,即「目标金额」,题目要求我们计算凑出目标金额所需的最少硬币数量。所以我们可以这样定义 dp 函数:

重点在状态转移方程

斐波那契数列原暴力递归

1
2
3
4
int fib(int N) {
if (N == 1 || N == 2) return 1;
return fib(N - 1) + fib(N - 2);
}

dp 数组的迭代解法

1
2
3
4
5
6
7
8
9
10
11
12
int fib(int N) {
if (N == 0) return 0;
int[] dp = new int[N + 1];
// base case
dp[0] = 0; dp[1] = 1;
// 状态转移
for (int i = 2; i <= N; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}

return dp[N];
}

凑零钱问题

给你 k 种面值的硬币,面值分别为 c1, c2 ... ck,每种硬币的数量无限,再给一个总金额 amount,问你最少需要几枚硬币凑出这个金额,如果不可能凑出,算法返回 -1

找零钱问题:通熟易懂,特别是从暴力法到带备忘录的递归,顿时明白了动态规划的核心了。其实暴力法,相当于我们心中有一颗多叉树(或者是一个图),这个二叉树恰好能够用来解决这个问题,而且找零钱问题类似于是在求解多叉树的深度。

特别是下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 伪码框架
int coinChange(int[] coins, int amount) {
// 题目要求的最终结果是 dp(amount)
return dp(coins, amount)
}

// 定义:要凑出金额 n,至少要 dp(coins, n) 个硬币
int dp(int[] coins, int n) {
// 做选择,选择需要硬币最少的那个结果
for (int coin : coins) {
res = min(res, 1 + dp(n - coin))
}
return res
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int coinChange(int[] coins, int amount) {
// 题目要求的最终结果是 dp(amount)
return dp(coins, amount)
}

int dp(int[] coins, int amount) {
// base case
if (amount == 0) return 0;
if (amount < 0) return -1;

int res = Integer.MAX_VALUE;
for (int coin : coins) {
// 计算子问题的结果
int subProblem = dp(coins, amount - coin);
// 子问题无解则跳过
if (subProblem == -1) continue;
// 在子问题中选择最优解,然后加一
res = Math.min(res, subProblem + 1);
}

return res == Integer.MAX_VALUE ? -1 : res;
}

那么动态规划解法(带备忘录的递归),就是利用空间(数组)来记录一些这棵树已经遍历过的结点。

通过下面的代码

dp 数组的定义:当目标金额为 i 时,至少需要 dp[i] 枚硬币凑出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
// 数组大小为 amount + 1,初始值也为 amount + 1
Arrays.fill(dp, amount + 1);

// base case
dp[0] = 0;
// 外层 for 循环在遍历所有状态的所有取值
for (int i = 0; i < dp.length; i++) {
// 内层 for 循环在求所有选择的最小值
for (int coin : coins) {
// 子问题无解,跳过
if (i - coin < 0) {
continue;
}
dp[i] = Math.min(dp[i], 1 + dp[i - coin]);
}
}
return (dp[amount] == amount + 1) ? -1 : dp[amount];
}


118. 杨辉三角

给定一个非负整数 numRows,生成「杨辉三角」的前 numRows 行。
在「杨辉三角」中,每个数是它左上方和右上方的数的和。

示例 1

1
2
输入: numRows = 5
输出: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]

示例 2

1
2
输入: numRows = 1
输出: [[1]]

1
2
3
4
5
6
7
8
9
10
class Solution(object):
def generate(self, numRows):
ans = [[1]]
for i in range(1, numRows):
lst = [0 for _ in range(i+1)]
lst[0], lst[-1] = 1, 1
for j in range(1,i):
lst[j] = ans[i-1][j-1] + ans[i-1][j]
ans.append(lst)
return ans


62.不同路径

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?


1
2
输入:m = 3, n = 7
输出:28

1
2
3
4
5
6
7
输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右
3. 向下 -> 向右 -> 向下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (i == 0 || j == 0)
dp[i][j] = 1;
else {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
}
return dp[m - 1][n - 1];
}
}


63.不同路径2

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

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

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

1
2
输入:obstacleGrid = [[0,1],[0,0]]
输出:1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int m = obstacleGrid.size();
int n = obstacleGrid[0].size();
vector<vector<int>> dp(m, vector<int>(n, 0));
for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++) dp[i][0] = 1;
for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) dp[0][j] = 1;
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (obstacleGrid[i][j] == 1) continue;
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][n - 1];
}
};


64. 最小路径和

给定一个包含非负整数的m x n网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

说明:每次只能向下或者向右移动一步。

1
2
3
输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。
1
2
输入:grid = [[1,2,3],[4,5,6]]
输出:12
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public int minPathSum(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
// base case
int result = 0;
int [][] dp = new int[m][n];
for (int i = 0; i < m; i++){
for (int j = 0; j < n; j++){
int cur = grid[i][j];
if (i == 0 && j == 0){
dp[i][j] = cur;
} else if (i == 0){
dp[i][j] = dp[i][j-1] + cur;
} else if (j == 0){
dp[i][j] = dp[i-1][j] + cur;
} else {
dp[i][j] = Math.min(dp[i-1][j] + cur, dp[i][j-1]+cur);
}
}
}
return dp[m-1][n-1];
}
}


647. 回文子串

给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。
子字符串是字符串中的由连续字符组成的一个序列。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

1
2
3
输入:s = "abc"
输出:3
解释:三个回文子串: "a", "b", "c"
1
2
3
输入:s = "aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public int countSubstrings(String s) {
int res = 0;
int n = s.length();

// dp[i][j] 表示[i,j]的字符是否为回文子串
boolean[][] dp = new boolean[n][n];

// 注意,外层循环要倒着写,内层循环要正着写
// 因为要求dp[i][j] 需要知道dp[i+1][j-1]
for(int i=n-1; i>=0; i--){
for(int j=i; j<n; j++){
// (s.charAt(i)==s.charAt(j) 时,当元素个数为1,2,3个时,一定为回文子串
if(s.charAt(i)==s.charAt(j) && (j-i<=2 || dp[i+1][j-1])){
dp[i][j] = true;
res++;
}
}
}

return res;
}
}


5. 最长回文子串

给你一个字符串 s,找到 s 中最长的回文子串。

1
2
3
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
1
2
输入:s = "cbbd"
输出:"bb"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
string longestPalindrome(string s) {
vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));
int maxlenth = 0;
int left = 0;
int right = 0;
for (int i = s.size() - 1; i >= 0; i--) {
for (int j = i; j < s.size(); j++) {
if (s[i] == s[j] && (j - i <= 1 || dp[i + 1][j - 1])) {
dp[i][j] = true;
}
if (dp[i][j] && j - i + 1 > maxlenth) {
maxlenth = j - i + 1;
left = i;
right = j;
}
}
}
return s.substr(left, maxlenth);
}
};


53. 最大子数组和

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组 是数组中的一个连续部分。

1
2
3
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
1
2
输入:nums = [5,4,-1,7,8]
输出:23
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
class Solution {
/**
* 1.dp[i]代表当前下标对应的最大值
* 2.递推公式 dp[i] = max (dp[i-1]+nums[i],nums[i]) res = max(res,dp[i])
* 3.初始化 都为 0
* 4.遍历方向,从前往后
* 5.举例推导结果。。。
*
* @param nums
* @return
*/
public static int maxSubArray(int[] nums) {
if (nums.length == 0) {
return 0;
}

int res = nums[0];
int[] dp = new int[nums.length];
dp[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
res = res > dp[i] ? res : dp[i];
}
return res;
}
}


70.爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

1
2
3
4
5
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
1
2
3
4
5
6
输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
int climbStairs(int n) {
if(n == 1){return 1;}
if(n == 2){return 2;}
int a = 1, b = 2, temp;
for(int i = 3; i <= n; i++){
temp = a;
a = b;
b = temp + b;
}
return b;
}
};


121.买卖股票最佳时机

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

1
2
3
4
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
1
2
3
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。

思路还是挺清晰的,还是DP思想:

  • 记录【今天之前买入的最小值】
  • 计算【今天之前最小值买入,今天卖出的获利】,也即【今天卖出的最大获利】
  • 比较【每天的最大获利】,取最大值即可
1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public int maxProfit(int[] prices) {
if(prices.length <= 1)
return 0;
int min = prices[0], max = 0;
for(int i = 1; i < prices.length; i++) {
max = Math.max(max, prices[i] - min);
min = Math.min(min, prices[i]);
}
return max;
}
}


122.买卖股票最佳时机2

给定一个数组 prices ,其中 prices[i] 是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

1
2
3
4
输入: prices = [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
  随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
1
2
3
4
输入: prices = [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
  注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
1
2
3
输入: prices = [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。

dp[i][0]第i天的状态是没买股票(两种情况:昨天没买,今天还是没买或者昨天是买了股票的状态,几天前买的我们不管,然后今天卖出去了)
dp[i][1]第i天的状态是买了股票(两种情况:昨天就是买了的状态或者昨天是没买状态,今天买入了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public int maxProfit(int[] prices) {
if (prices.length<1){
return prices.length;
}
int[][] dp = new int[prices.length][2];
dp[0][0] = 0;
dp[0][1] = -prices[0];
for (int i=1;i<prices.length;i++){
dp[i][0] = Math.max(dp[i-1][1]+prices[i],dp[i-1][0]);
dp[i][1] = Math.max(dp[i][0]-prices[i],dp[i-1][1]);
}
return Math.max(dp[prices.length-1][0],dp[prices.length-1][1]);
}
}


123.买卖股票最佳时机3

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

1
2
3
4
输入:prices = [3,3,5,0,0,3,1,4]
输出:6
解释:在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。
  随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。
1
2
3
4
5
输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。  
  注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。  
  因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
1
2
3
输入:prices = [7,6,4,3,1] 
输出:0
解释:在这个情况下, 没有交易完成, 所以最大利润为 0。
1
2
输入:prices = [1]
输出:0

暴力递归,会超时,写在这里是为了让大家更好的理解动态规划的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public int maxProfit(int[] prices) {
return f(prices, 0, 0, 0);
}

/**
*
* @param prices
* @param i 当前考虑第几天
* @param hasStock 是否有股票在手
* @param counts 已经交易的次数(每买一次股票就加一)
* @return
*/
private int f(int[] prices, int i, int hasStock, int counts) {
// 如果已经买了两次股票,并且手里已经没有股票了,那么后面的天数不需要考虑
if(i >= prices.length || (counts >= 2 && hasStock < 1))
return 0;
// 如果手里有股票,我可以选择卖或者不卖
if(hasStock > 0)
return Math.max(prices[i] + f(prices, i+1, 0, counts), f(prices, i+1, 1, counts));
// 如果没有股票,我可以选择买或者不买
return Math.max(-prices[i] + f(prices, i+1, 1, counts+1), f(prices, i+1, 0, counts));
}

动态规划 时间复杂度O(n)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public int maxProfit(int[] prices) {
int m = prices.length;
int[][][] dp = new int[m+1][2][3];
for(int i = m-1; i >= 0; i--) {
for(int j = 1; j >= 0; j--) {
for(int k = 2; k >= 0; k--) {
if(k == 2 && j == 0)
continue;
if(j > 0)
dp[i][j][k] = Math.max(prices[i] + dp[i+1][0][k],
dp[i+1][1][k]);
else
dp[i][j][k] = Math.max(-prices[i] + dp[i+1][1][k+1],
dp[i+1][0][k]);
}
}
}
return dp[0][0][0];
}
}


188.买卖股票最佳实际4

给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

1
2
3
输入:k = 2, prices = [2,4,1]
输出:2
解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。
1
2
3
4
输入:k = 2, prices = [3,2,6,5,0,3]
输出:7
解释:在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。
随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 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
class Solution {
public int maxProfit(int k, int[] prices) {
/**
当k大于等于数组长度一半时, 问题退化为贪心问题此时采用 买卖股票的最佳时机 II
的贪心方法解决可以大幅提升时间性能, 对于其他的k, 可以采用 买卖股票的最佳时机 III
的方法来解决, 在III中定义了两次买入和卖出时最大收益的变量, 在这里就是k租这样的
变量, 即问题IV是对问题III的推广, t[i][0]和t[i][1]分别表示第i比交易买入和卖出时
各自的最大收益
**/
if(k < 1) return 0;
if(k >= prices.length/2) return greedy(prices);
int[][] t = new int[k][2];
for(int i = 0; i < k; ++i)
t[i][0] = Integer.MIN_VALUE;
for(int p : prices) {
t[0][0] = Math.max(t[0][0], -p);
t[0][1] = Math.max(t[0][1], t[0][0] + p);
for(int i = 1; i < k; ++i) {
t[i][0] = Math.max(t[i][0], t[i-1][1] - p);
t[i][1] = Math.max(t[i][1], t[i][0] + p);
}
}
return t[k-1][1];
}

private int greedy(int[] prices) {
int max = 0;
for(int i = 1; i < prices.length; ++i) {
if(prices[i] > prices[i-1])
max += prices[i] - prices[i-1];
}
return max;
}
}


198.打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

1
2
3
4
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
  偷窃到的最高金额 = 1 + 3 = 4 。
1
2
3
4
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
  偷窃到的最高金额 = 2 + 9 + 1 = 12 。
1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public int rob(int[] nums) {
int n = nums.length;
if (n <= 1) return n == 0 ? 0 : nums[0];
int[] memo = new int[n];
memo[0] = nums[0];
memo[1] = Math.max(nums[0], nums[1]);
for (int i = 2; i < n; i++)
memo[i] = Math.max(memo[i - 1], nums[i] + memo[i - 2]);
return memo[n - 1];
}
}


213.打家劫舍2

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。

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

核心原则就是:第一个和最后一个不能同时抢。 所以:要么不抢第一个,要么不抢最后一个。 注意不抢第一个的时候,最后一个可抢可不抢;另一种情况同理 取两种情况中的最大值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution(object):
def rob(self, nums):
n = len(nums)
if n == 0:
return 0
if n <= 2:
return max(nums)
# 不抢第一个
dp1 = [0] * n
dp1[0] = 0
dp1[1] = nums[1]
for i in range(2, n):
dp1[i] = max(dp1[i-1],nums[i] + dp1[i-2])

# 不抢最后一个
dp2 = [0] * n
dp2[0] = nums[0]
dp2[1] = max(nums[0],nums[1])
for i in range(2, n-1):
dp2[i] = max(dp2[i-1],nums[i] + dp2[i-2])
return max(dp1[n-1],dp2[n-2])


337.打家劫舍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.
1
2
3
4
5
6
7
8
9
10
输入: [3,4,5,1,3,null,1]

  3
/ \
4 5
/ \ \
1 3 1

输出: 9
解释: 小偷一晚能够盗取的最高金额 = 4 + 5 = 9.

经过仔细分析(手动严肃脸),正确的解题思路大致是这样的:

  • 对于一个以 node 为根节点的二叉树而言,如果尝试偷取 node 节点,那么势必不能偷取其左右子节点,然后继续尝试偷取其左右子节点的左右子节点。
  • 如果不偷取该节点,那么只能尝试偷取其左右子节点。
  • 比较两种方式的结果,谁大取谁。

由此得到一个递归函数(务必注意该函数的定义!!!):

1
2
//尝试对以 node 为根节点的二叉树进行偷取,返回能偷取的最大值
int tryRob(LinkedList::TreeNode* 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
class Solution {
unordered_map<TreeNode *, int> sums;
public:
int rob(TreeNode* root) {
return tryRob(root);
}

int tryRob(TreeNode* node) {
if (!node) return 0;
if (sums.count(node)) return sums[node];
//偷取该节点
int res1 = 0;
if (node->left) {
res1 += (tryRob(node->left->left) + tryRob(node->left->right));
}
if (node->right) {
res1 += (tryRob(node->right->left) + tryRob(node->right->right));
}
res1 += node->val;
//不偷取该节点
int res2 = tryRob(node->left) + tryRob(node->right);
sums[node] = max(res1, res2);
return sums[node];
}
};


96.不同的二叉搜索树

给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。

1
2
输入:n = 3
输出:5
1
2
输入:n = 1
输出:1

动态规划

假设n个节点存在二叉排序树的个数是G(n),令f(i)为以i为根的二叉搜索树的个数
即有:G(n) = f(1) + f(2) + f(3) + f(4) + … + f(n)

n为根节点,当i为根节点时,其左子树节点个数为[1,2,3,…,i-1],右子树节点个数为[i+1,i+2,…n],所以当i为根节点时,其左子树节点个数为i-1个,右子树节点为n-i,即f(i) = G(i-1)*G(n-i),
上面两式可得:G(n) = G(0)*G(n-1)+G(1)*(n-2)+...+G(n-1)*G(0)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution(object):
def numTrees(self, n):
"""
:type n: int
:rtype: int
"""
dp = [0] * (n+1)
dp[0] = 1
dp[1] = 1

for i in range(2,n+1):
for j in range(1,i+1):
dp[i] += dp[j-1] * dp[i-j]
return dp[n]


95.不同的二叉搜索树2

给你一个整数 n ,请你生成并返回所有由 n 个节点组成且节点值从 1 到 n 互不相同的不同 二叉搜索树 。可以按任意顺序返回答案。

1
2
输入:n = 3
输出:[[1,null,2,null,3],[1,null,3,2],[2,1,3],[3,1,null,null,2],[3,2,null,1]]
1
2
输入:n = 1
输出:[[1]]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
public List<TreeNode> generateTrees(int n) {
if(n == 0)
return new LinkedList<TreeNode>();
return generateTrees(1,n);
}
public List<TreeNode> generateTrees(int start,int end) {
List<TreeNode> res = new LinkedList<TreeNode>();
if(start > end){
res.add(null);
return res;
}
for(int i = start;i <= end;i++){
List<TreeNode> subLeftTree = generateTrees(start,i-1);
List<TreeNode> subRightTree = generateTrees(i+1,end);
for(TreeNode left : subLeftTree){
for(TreeNode right : subRightTree){
TreeNode node = new TreeNode(i);
node.left = left;
node.right = right;
res.add(node);
}
}
}
return res;
}
}


124. 二叉树中的最大路径和

路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。
路径和是路径中各节点值的总和。
给你一个二叉树的根节点 root ,返回其最大路径和 。

1
2
3
输入:root = [1,2,3]
输出:6
解释:最优路径是 2 -> 1 -> 3 ,路径和为 2 + 1 + 3 = 6
1
2
3
输入:root = [-10,9,20,null,null,15,7]
输出:42
解释:最优路径是 15 -> 20 -> 7 ,路径和为 15 + 20 + 7 = 42
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
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
private int ret = Integer.MIN_VALUE;

public int maxPathSum(TreeNode root) {
/**
对于任意一个节点, 如果最大和路径包含该节点, 那么只可能是两种情况:
1. 其左右子树中所构成的和路径值较大的那个加上该节点的值后向父节点回溯构成最大路径
2. 左右子树都在最大路径中, 加上该节点的值构成了最终的最大路径
**/
getMax(root);
return ret;
}

private int getMax(TreeNode r) {
if(r == null) return 0;
int left = Math.max(0, getMax(r.left)); // 如果子树路径和为负则应当置0表示最大路径不包含子树
int right = Math.max(0, getMax(r.right));
ret = Math.max(ret, r.val + left + right); // 判断在该节点包含左右子树的路径和是否大于当前最大路径和
return Math.max(left, right) + r.val;
}
}


22. 括号生成

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且有效的括号组合。

示例 1

1
2
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]

示例 2

1
2
输入:n = 1
输出:["()"]

每新增一对括号,就是在上一次的结果的各个位置插入一个”()”,用集合防止重复

1
2
3
4
5
6
7
8
9
10
class Solution(object):
def generateParenthesis(self, n):
result = {''}
for i in range(n):
temp = set()
for s in result: # 在上一次的结果的所有字符串的各个位置上插入'()'
for j in range(len(s) + 1):
temp.add(s[:j] + '()' + s[j:])
result = temp
return list(result)


32. 最长有效括号

给你一个只包含 ‘(‘ 和 ‘)’ 的字符串,找出最长有效(格式正确且连续)括号子串的长度。

1
2
3
输入:s = "(()"
输出:2
解释:最长有效括号子串是 "()"
1
2
3
输入:s = ")()())"
输出:4
解释:最长有效括号子串是 "()()"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution(object):
def longestValidParentheses(self, s):
length = len(s)
if length == 0:
return 0
dp = [0] * length
for i in range(1,length):
#当遇到右括号时,尝试向前匹配左括号
if s[i] == ')':
pre = i - dp[i-1] -1;
#如果是左括号,则更新匹配长度
if pre>=0 and s[pre] == '(':
dp[i] = dp[i-1] + 2
#处理独立的括号对的情形 类似()()、()(())
if pre>0:
dp[i] += dp[pre-1]
return max(dp);


10. 正则表达式匹配

给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.''*' 的正则表达式匹配。

  • '.'匹配任意单个字符
  • '*'匹配零个或多个前面的那一个元素

所谓匹配,是要涵盖整个字符串 s的,而不是部分字符串。

1
2
3
输入:s = "aa" p = "a"
输出:false
解释:"a" 无法匹配 "aa" 整个字符串。
1
2
3
输入:s = "aa" p = "a*"
输出:true
解释:因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。
1
2
3
输入:s = "aab" p = "c*a*b"
输出:true
解释:因为 '*' 表示零个或多个,这里 'c' 为 0 个, 'a' 被重复一次。因此可以匹配字符串 "aab"。
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
class Solution {
public boolean isMatch(String s, String p) {
// 动态规划。。太多种情况了。。。。
//看了解题,重点还是状态方程的情况需要仔细的考虑,情况很多。
int m = s.length();
int n = p.length();

boolean[][] f = new boolean[m + 1][n + 1];//记录s的前i个字符是否和p的前j个字符匹配。
f[0][0] = true;//动态规划临界值考虑非常重要,都是空字符串肯定匹配。
for (int i = 0; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
if (p.charAt(j - 1) == '*') {
f[i][j] = f[i][j - 2];
if (matches(s, p, i, j - 1)) {
f[i][j] = f[i][j] || f[i - 1][j];
}
} else {
if (matches(s, p, i, j)) {
f[i][j] = f[i - 1][j - 1];
}
}
}
}
return f[m][n];
}

public boolean matches(String s, String p, int i, int j) {//匹配s的第i个和p的第j个字符是否满足条件
if (i == 0) {
return false;
}
if (p.charAt(j - 1) == '.') {
return true;
}
return s.charAt(i - 1) == p.charAt(j - 1);
}
}


44. 通配符匹配

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

1
2
'?' 可以匹配任何单个字符。
'*' 可以匹配任意字符串(包括空字符串)。

两个字符串完全匹配才算匹配成功。

说明:

  • s 可能为空,且只包含从 a-z 的小写字母。
  • p 可能为空,且只包含从 a-z 的小写字母,以及字符 ?*
1
2
3
4
5
输入:
s = "aa"
p = "a"
输出: false
解释: "a" 无法匹配 "aa" 整个字符串。
1
2
3
4
5
输入:
s = "aa"
p = "*"
输出: true
解释: '*' 可以匹配任意字符串。

动态规划的解决思路

dp[i][j]表示s到i位置,p到j位置是否匹配!

  • 初始化

    1
    2
    3
    dp[0][0]:什么都没有,所以为true
    第一行dp[0][j],换句话说,s为空,与p匹配,所以只要p开始为*才为true
    第一列dp[i][0],当然全部为False
  • 动态方程:

    1
    2
    3
    4
    如果(s[i] == p[j] || p[j] == "?") && dp[i-1][j-1] ,有dp[i][j] = true
    如果p[j] == "*" && (dp[i-1][j] = true || dp[i][j-1] = true) 有dp[i][j] = true
    ​dp[i-1][j],表示*代表是空字符,例如ab,ab*
    ​dp[i][j-1],表示*代表非空任何字符,例如abcd,ab*
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public boolean isMatch2(String s, String p) {
boolean[][] dp = new boolean[s.length() + 1][p.length() + 1];
dp[0][0] = true;
for (int j = 1; j < p.length() + 1; j++) {
if (p.charAt(j - 1) == '*') {
dp[0][j] = dp[0][j - 1];
}
}
for (int i = 1; i < s.length() + 1; i++) {
for (int j = 1; j < p.length() + 1; j++) {
if (s.charAt(i - 1) == p.charAt(j - 1) || p.charAt(j - 1) == '?') {
dp[i][j] = dp[i - 1][j - 1];
} else if (p.charAt(j - 1) == '*') {
dp[i][j] = dp[i][j - 1] || dp[i - 1][j];
}
}
}
return dp[s.length()][p.length()];
}
}


139.单词拆分

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用

1
2
3
输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。
1
2
3
4
输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。
  注意,你可以重复使用字典中的单词。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
int s_len = s.length();
boolean[] dp = new boolean[s_len + 1];
dp[0] = true;
for (int i=1; i<=s_len; i++){
for (int j=0; j<i; j++){
if (dp[j] && wordDict.contains(s.substring(j, i))){
dp[i] = true;
break;
}
}
}
return dp[s_len];
}
}

140. 单词拆分 II

给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,在字符串中增加空格来构建一个句子,使得句子中所有的单词都在词典中。返回所有这些可能的句子。
说明

  • 分隔时可以重复使用字典中的单词。
  • 你可以假设字典中没有重复的单词。
1
2
3
4
5
6
7
8
输入:
s = "catsanddog"
wordDict = ["cat", "cats", "and", "sand", "dog"]
输出:
[
  "cats and dog",
  "cat sand dog"
]
1
2
3
4
5
6
7
8
9
输入:
s = "pineapplepenapple"
wordDict = ["apple", "pen", "applepen", "pine", "pineapple"]
输出:
[
  "pine apple pen apple",
  "pineapple pen apple",
  "pine applepen apple"
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
List<String> res = new ArrayList<>();
public List<String> wordBreak(String s, List<String> wordDict) {
dfs(s,wordDict,new ArrayList<>(),0);
return res;
}
public void dfs(String s,List<String> wordDict,List<String> path,int index){
if(index==s.length()){
res.add(new String(String.join(" ",path)));
return;
}
for(int i=index;i<=s.length();i++){
if(wordDict.contains(s.substring(index,i))){
path.add(s.substring(index,i));
dfs(s,wordDict,path,i);
path.remove(path.size()-1);
}
}
}
}


72. 编辑距离

给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

插入一个字符
删除一个字符
替换一个字符

1
2
3
4
5
6
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')

问题1:如果 word1[0..i-1] 到 word2[0..j-1] 的变换需要消耗 k 步,那 word1[0..i] 到 word2[0..j] 的变换需要几步呢?
答:先使用 k 步,把 word1[0..i-1] 变换到 word2[0..j-1],消耗 k 步。再把 word1[i] 改成 word2[j],就行了。如果 word1[i] == word2[j],什么也不用做,一共消耗 k 步,否则需要修改,一共消耗 k + 1 步。

问题2:如果 word1[0..i-1] 到 word2[0..j] 的变换需要消耗 k 步,那 word1[0..i] 到 word2[0..j] 的变换需要消耗几步呢?
答:先经过 k 步,把 word1[0..i-1] 变换到 word2[0..j],消耗掉 k 步,再把 word1[i] 删除,这样,word1[0..i] 就完全变成了 word2[0..j] 了。一共 k + 1 步。

问题3:如果 word1[0..i] 到 word2[0..j-1] 的变换需要消耗 k 步,那 word1[0..i] 到 word2[0..j] 的变换需要消耗几步呢?
答:先经过 k 步,把 word1[0..i] 变换成 word2[0..j-1],消耗掉 k 步,接下来,再插入一个字符 word2[j], word1[0..i] 就完全变成了 word2[0..j] 了。

从上面三个问题来看,word1[0..i] 变换成 word2[0..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
class Solution {
public:

int minDistance(string word1, string word2) {
int m = word1.length();
int n = word2.length();

vector<vector<int>> cost(m+1, vector<int>(n+1));

for (int i = 0; i <= m; ++i) {
cost[i][0] = i;
}
for (int j = 0; j <= n; ++j) {
cost[0][j] = j;
}
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
if (word1[i-1] == word2[j-1]) {
cost[i][j] = cost[i-1][j-1];
} else {
cost[i][j] = 1 + min(cost[i-1][j-1], min(cost[i][j-1], cost[i-1][j]));
}
}
}
return cost[m][n];
}
};


42.接雨水

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

1
2
3
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
int trap(vector<int>& height) {
int res = 0;
int n = height.size();
// left[i]表示i左边的最大值,right[i]表示i右边的最大值
vector<int> left(n), right(n);
for (int i = 1; i < n; i++) {
left[i] = max(left[i - 1], height[i - 1]);
}
for (int i = n - 2; i >= 0; i--) {
right[i] = max(right[i + 1], height[i + 1]);
}
for (int i = 0; i < n; i++) {
int level = min(left[i], right[i]);
res += max(0, level - height[i]);
}
return res;
}
};


1143.最长公共子序列

给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。

1
2
3
输入:s = "bbbab"
输出:4
解释:一个可能的最长回文子序列为 "bbbb" 。
1
2
3
输入:s = "cbbd"
输出:2
解释:一个可能的最长回文子序列为 "bb" 。
1
2
3
4
5
dp[i][j]表示s[i..j]代表的字符串的最长回文子序列
d[i][i]=1
dp[i][j] = dp[i+1][j-1]+2 当s[i]=s[j]
dp[i][j]=max(dp[i+1][j],dp[i][j-1]) 当s[i]!=s[j] 取s[i+1..j] 和s[i..j-1]中最长的
由于dp[i][j]需要dp[i+1][j]所以需要逆序枚举s的长度,而又因为j是递增的,所以在求解dp[i][j]时,dp[i][j-1]肯定已经求解过了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution(object):
def longestPalindromeSubseq(self, s):
len_s = len(s)
dp = [[0] * len_s for _ in range(len_s)]
# base case 每个字符可以是一个回文串
for i in range(len_s):
dp[i][i]=1
for i in range(len_s-1,-1,-1):
for j in range(i+1,len_s):
#长度加2
if s[i]==s[j]:
dp[i][j] = dp[i+1][j-1]+2
else:
dp[i][j]=max(dp[i+1][j],dp[i][j-1])
return dp[0][-1]


300. 最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

1
2
3
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
1
2
输入:nums = [0,1,0,3,2,3]
输出:4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
if (nums.size() <= 1) return nums.size();
vector<int> dp(nums.size(), 1);
int result = 0;
for (int i = 1; i < nums.size(); i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
}
if (dp[i] > result) result = dp[i]; // 取长的子序列
}
return result;
}
};


673.最长递增子序列的个数

给定一个未排序的整数数组,找到最长递增子序列的个数。

1
2
3
输入: [1,3,5,4,7]
输出: 2
解释: 有两个最长递增子序列,分别是 [1, 3, 4, 7] 和[1, 3, 5, 7]。
1
2
3
输入: [2,2,2,2,2]
输出: 5
解释: 最长递增子序列的长度是1,并且存在5个子序列的长度为1,因此输出5。
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 Solution {
public int findNumberOfLIS(int[] nums) {

if (nums.length == 0) {
return 0;
}

int[] dp = new int[nums.length];
int[] combination = new int[nums.length];

Arrays.fill(dp, 1);
Arrays.fill(combination, 1);

int max = 1, res = 0;

for (int i = 1; i < dp.length; i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
if (dp[j] + 1 > dp[i]) { //如果+1长于当前LIS 则组合数不变
dp[i] = dp[j] + 1;
combination[i] = combination[j];
} else if (dp[j] + 1 == dp[i]) { //如果+1等于当前LIS 则说明找到了新组合
combination[i] += combination[j];
}
}
}
max = Math.max(max, dp[i]);
}

for (int i = 0; i < nums.length; i++)
if (dp[i] == max) res += combination[i];

return res;
}
}


115.不同子序列

329.矩阵中最长的递增路径

贪心算法


广度优先算法

先举例一下 BFS 出现的常见场景好吧,问题的本质就是让你在一幅「图」中找到从起点 start 到终点 target 的最近距离,这个例子听起来很枯燥,但是 BFS 算法问题其实都是在干这个事儿

解题框架

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
// 计算从起点 start 到终点 target 的最近距离
int BFS(Node start, Node target) {
Queue<Node> q; // 核心数据结构
Set<Node> visited; // 避免走回头路

q.offer(start); // 将起点加入队列
visited.add(start);
int step = 0; // 记录扩散的步数

while (q not empty) {
int sz = q.size();
/* 将当前队列中的所有节点向四周扩散 */
for (int i = 0; i < sz; i++) {
Node cur = q.poll();
/* 划重点:这里判断是否到达终点 */
if (cur is target)
return step;
/* 将 cur 的相邻节点加入队列 */
for (Node x : cur.adj()) {
if (x not in visited) {
q.offer(x);
visited.add(x);
}
}
}
/* 划重点:更新步数在这里 */
step++;
}
}

例题

102.二叉树的层序遍历

给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。

1
2
输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
if(root == null)
return new ArrayList<>();
List<List<Integer>> res = new ArrayList<>();
Queue<TreeNode> queue = new LinkedList<TreeNode>();
queue.add(root);
while(!queue.isEmpty()){
int count = queue.size();
List<Integer> list = new ArrayList<Integer>();
while(count > 0){
TreeNode node = queue.poll();
list.add(node.val);
if(node.left != null)
queue.add(node.left);
if(node.right != null)
queue.add(node.right);
count--;
}
res.add(list);
}
return res;
}
}


107.二叉树的层序遍历2

给你二叉树的根节点 root ,返回其节点值 自底向上的层序遍历 。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)

1
2
输入:root = [3,9,20,null,null,15,7]
输出:[[15,7],[9,20],[3]]
1
2
输入:root = [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
class Solution {
public List<List<Integer>> levelOrderBottom(TreeNode root) {
LinkedList<List<Integer>> result = new LinkedList<>();
if (root == null)
return result;
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
while (!queue.isEmpty()) {
List<Integer> oneLevel = new ArrayList<>();
// 每次都取出一层的所有数据
int count = queue.size();
for (int i = 0; i < count; i++) {
TreeNode node = queue.poll();
oneLevel.add(node.val);
if (node.left != null)
queue.add(node.left);
if (node.right != null)
queue.add(node.right);
}
// 每次都往队头塞
result.addFirst(oneLevel);
}
return result;
}
}






深度优先算法

解题框架

1
2
3
4
5
6
7
8
9
10
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return

for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
1
2
3
4
5
6
7
8
for 选择 in 选择列表:
# 做选择
将该选择从选择列表移除
路径.add(选择)
backtrack(路径, 选择列表)
# 撤销选择
路径.remove(选择)
将该选择再加入选择列表

例题

200. 岛屿数量

给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。

1
2
3
4
5
6
7
输入:grid = [
["1","1","1","1","0"],
["1","1","0","1","0"],
["1","1","0","0","0"],
["0","0","0","0","0"]
]
输出:1
1
2
3
4
5
6
7
输入:grid = [
["1","1","0","0","0"],
["1","1","0","0","0"],
["0","0","1","0","0"],
["0","0","0","1","1"]
]
输出: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
class Solution {
public int numIslands(char[][] grid) {
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 = res+1;
dfs(grid,i,j);
}
}
}
return res;
}

//遍历grid并使之成为2
public void dfs(char[][] grid,int i,int j){
if(i>=grid.length||j>=grid[0].length||i<0||j<0){
return;
}
if(grid[i][j]=='0'||grid[i][j]=='2'){
return;
}
grid[i][j]='2';
dfs(grid,i+1,j);
dfs(grid,i-1,j);
dfs(grid,i,j+1);
dfs(grid,i,j-1);
}

}


695. 岛屿的最大面积

给你一个大小为 m x n 的二进制矩阵 grid 。
岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在 水平或者竖直的四个方向上 相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。
岛屿的面积是岛上值为 1 的单元格的数目。
计算并返回 grid 中最大的岛屿面积。如果没有岛屿,则返回面积为 0 。

1
2
3
输入:grid = [[0,0,1,0,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,1,1,0,1,0,0,0,0,0,0,0,0],[0,1,0,0,1,1,0,0,1,0,1,0,0],[0,1,0,0,1,1,0,0,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,1,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,0,0,0,0,0,0,1,1,0,0,0,0]]
输出:6
解释:答案不应该是 11 ,因为岛屿只能包含水平或垂直这四个方向上的 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
class Solution {
public int maxAreaOfIsland(int[][] grid) {
int max = 0;
for(int i = 0; i < grid.length; i++){
for(int j = 0; j < grid[0].length; j++){
if(grid[i][j] == 1){
max = Math.max (dfs(grid, i, j), max);
}
}
}
return max;
}
int dfs(int[][] grid, int i, int j){
if(i < 0 || i >= grid.length || j < 0 || j >= grid[0].length || grid[i][j] == 0){
return 0;
}
grid[i][j] = 0;
int count = 1;
count += dfs(grid, i+1, j);
count += dfs(grid, i-1, j);
count += dfs(grid, i, j+1);
count += dfs(grid, i, j-1);
return count;
}
}


463. 岛屿的周长

给定一个 row x col 的二维网格地图 grid ,其中:grid[i][j] = 1 表示陆地, grid[i][j] = 0 表示水域。
网格中的格子 水平和垂直 方向相连(对角线方向不相连)。整个网格被水完全包围,但其中恰好有一个岛屿(或者说,一个或多个表示陆地的格子相连组成的岛屿)。
岛屿中没有“湖”(“湖” 指水域在岛屿内部且不和岛屿周围的水相连)。格子是边长为 1 的正方形。网格为长方形,且宽度和高度均不超过 100 。计算这个岛屿的周长。

1
2
3
输入:grid = [[0,1,0,0],[1,1,1,0],[0,1,0,0],[1,1,0,0]]
输出:16
解释:它的周长是上面图片中的 16 个黄色的边
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public int islandPerimeter(int[][] grid) {
//重点关注前面遍历过得方格,如果之前有相邻方格,就-2;
if (grid == null || grid.length == 0) {
return 0;
}
int rsp = 0;
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[i].length; j++) {
if (grid[i][j] == 1) {
rsp += 4;
if (i > 0 && grid[i - 1][j] == 1) {
rsp -= 2;
}
if (j > 0 && grid[i][j - 1] == 1) {
rsp -= 2;
}
}
}
}
return rsp;
}


743. 网络延迟时间

有 n 个网络节点,标记为 1 到 n。
给你一个列表 times,表示信号经过 有向 边的传递时间。 times[i] = (ui, vi, wi),其中 ui 是源节点,vi 是目标节点, wi 是一个信号从源节点传递到目标节点的时间。
现在,从某个节点 K 发出一个信号。需要多久才能使所有节点都收到信号?如果不能使所有节点收到信号,返回 -1 。

思路分析:
首先我们应该明白,从k传输到所有的的时间 = max (从k到点1所需要的最少时间,
从k到2所需要的最少时间 … 从k到n所需要的最少时间),因为传输的过程是同时的。
所以这道题就转换为图的最短路径求解问题。
请翻阅 图的最短路径:Floyd、DisjKstra、SPFA算法
我们先使用DisjKstra算法求出各个点到k的最短距离,然后求出这些距离中最大值。

1
2
输入:times = [[2,1,1],[2,3,1],[3,4,1]], n = 4, k = 2
输出:2
1
2
输入:times = [[1,2,1]], n = 2, k = 1
输出:1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//DisjKstra算法
class Solution {
public:
int networkDelayTime(vector<vector<int>>& times, int N, int K) {
queue<int> myQue;//该队列用于即将访问的节点
vector<int> distToK(N + 1, INT_MAX);//distToK[index]表示点K到点index的最短距离
vector<vector<int>> graph(N+1,vector<int>(N+1,-1));//邻接矩阵
for(auto &time:times){//构建邻接矩阵
graph[time[0]][time[1]] = time[2];
}
myQue.push(K);
distToK[K] = 0;//K到自己的最短距离为0
//开始搜索各个点到k的最短距离
while(!myQue.empty()){
int front = myQue.front();
myQue.pop();
//利用当前front节点,尝试稀疏点k到所有节点的最短距离
for(int target = 1; target <= N; ++target){
if(graph[front][target] != -1 && distToK[front] + graph[front][target] < distToK[target]){
//如果front到target有边,并且点k到front的距离distToK[front] + 点front到target距离graph[front][target]小于点k到target的距离distToK[target]
distToK[target] = distToK[front] + graph[front][target];//则进行稀疏
myQue.push(target);//放入队列
}
}
}
//寻找点k到各个点的最短距离的最大值
int maxRes = 0;
for(int i = 1; i <= N; ++i){
maxRes = max(maxRes, distToK[i]);
}
return maxRes == INT_MAX? -1 : maxRes;
}
};


787. K 站中转内最便宜的航班

有 n 个城市通过一些航班连接。给你一个数组 flights ,其中flights[i] = [fromi, toi, pricei],表示该航班都从城市 fromi 开始,以价格 pricei 抵达 toi。
现在给定所有的城市和航班,以及出发城市 src 和目的地 dst,你的任务是找到出一条最多经过 k 站中转的路线,使得从 src 到 dst 的 价格最便宜 ,并返回该价格。 如果不存在这样的路线,则输出 -1。

1
2
3
4
5
6
输入: 
n = 3, edges = [[0,1,100],[1,2,100],[0,2,500]]
src = 0, dst = 2, k = 1
输出: 200
解释:
城市航班图如下

从城市 0 到城市 2 在 1 站中转以内的最便宜价格是 200,如图中红色所示。

1
2
3
4
5
6
输入: 
n = 3, edges = [[0,1,100],[1,2,100],[0,2,500]]
src = 0, dst = 2, k = 0
输出: 500
解释:
城市航班图如下

从城市 0 到城市 2 在 0 站中转以内的最便宜价格是 500,如图中蓝色所示。

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
//方法一:深度优先搜索法(回溯法)。

class Solution {
public:
int minRes = INT_MAX;
int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int K) {
//首先利用flights构建图的信息
vector<vector<int>> graph(n, vector<int>(n, -1));
for (auto &flight : flights){
graph[flight[0]][flight[1]] = flight[2];
}
vector<bool> visited(n, false);//标记当前线路中已经走过的点
visited[src] = true;
dfs(graph, visited, n, dst, K, 0, src, 0);//从src开始搜索
return minRes == INT_MAX ? -1 : minRes;
}
//haveCost表示已经花费了的费用,nowSrc现在所处的点,myK到达nowSrc已经经过的中转站个数
void dfs(vector<vector<int>> &graph, vector<bool> &visited, int n, int dst, int K, int haveCost, int nowSrc, int myK){
if (nowSrc == dst){//到达目的地
minRes = min(minRes, haveCost);
return;
}
//剪枝:中转站个数不能超过K,花费不能超过已经找的最小花费
if (myK > K || haveCost >= minRes){
return;
}
//搜索下一个点
for (int i = 0; i < n; ++i){
if (!visited[i] && graph[nowSrc][i] != -1){
visited[i] = true;
dfs(graph, visited, n, dst, K, haveCost + graph[nowSrc][i], i, myK + 1);
visited[i] = false;
}
}
}
};



//方法二:动态规划法。
class Solution {
public:
int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int K) {
//dp[i][k]表示从src至多经过k站到达i的最少费用
vector<vector<int>> dp(n, vector<int>(K + 2, INT_MAX));
//初始化 src 到 src的费用为0
for (int k = 0; k <= K + 1; ++k){
dp[src][k] = 0;
}
//开始动态规划
for (int k = 1; k <= K + 1; ++k){
for (auto &flight : flights){
//如果从src至多经过k - 1站可达flight[0]
if (dp[flight[0]][k - 1] != INT_MAX){
//更新从src至多经过k站到达flight[1]
dp[flight[1]][k] = min(dp[flight[1]][k], dp[flight[0]][k - 1] + flight[2]);
}
}
}
return dp[dst][K+1] == INT_MAX ? -1 : dp[dst][K+1];
}
};


226. 翻转二叉树

给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。

1
2
输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]

1
2
输入:root = [2,1,3]
输出:[2,3,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
61
62
利用前序遍历
class Solution {
// 先序遍历--从顶向下交换
public TreeNode invertTree(TreeNode root) {
if (root == null) return null;
// 保存右子树
TreeNode rightTree = root.right;
// 交换左右子树的位置
root.right = invertTree(root.left);
root.left = invertTree(rightTree);
return root;
}
}

利用中序遍历
class Solution {
public TreeNode invertTree(TreeNode root) {
if (root == null) return null;
invertTree(root.left); // 递归找到左节点
TreeNode rightNode= root.right; // 保存右节点
root.right = root.left;
root.left = rightNode;
// 递归找到右节点 继续交换 : 因为此时左右节点已经交换了,所以此时的右节点为root.left
invertTree(root.left);
}
}

利用后序遍历
class Solution {
public TreeNode invertTree(TreeNode root) {
// 后序遍历-- 从下向上交换
if (root == null) return null;
TreeNode leftNode = invertTree(root.left);
TreeNode rightNode = invertTree(root.right);
root.right = leftNode;
root.left = rightNode;
return root;
}
}

利用层次遍历
class Solution {
public TreeNode invertTree(TreeNode root) {
// 层次遍历--直接左右交换即可
if (root == null) return null;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()){
TreeNode node = queue.poll();
TreeNode rightTree = node.right;
node.right = node.left;
node.left = rightTree;
if (node.left != null){
queue.offer(node.left);
}
if (node.right != null){
queue.offer(node.right);
}
}
return root;
}
}


112. 路径总和

给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false 。
叶子节点 是指没有子节点的节点。

1
2
3
输入:root = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22
输出:true
解释:等于目标和的根节点到叶节点路径如上图所示。

1
2
3
4
5
6
输入:root = [1,2,3], targetSum = 5
输出:false
解释:树中存在两条根节点到叶子节点的路径:
(1 --> 2): 和为 3
(1 --> 3): 和为 4
不存在 sum = 5 的根节点到叶子节点的路径。
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
//深度优先遍历二叉树,每深入一次,sum-根节点的值,当到达叶子节点的时候,判断sum是否等于当前的节点值,如果等于,说明找到了,否则尝试另外一条路径
class Solution {
boolean ans = false;
public boolean hasPathSum(TreeNode root, int sum) {
if (root == null) {
return false;
}
dfs(root,sum);
return ans;
}

private void dfs(TreeNode root,int sum) {
if (root == null) {
return;
}

if (root.left == null && root.right == null) {
if (sum == root.val) {
ans = true;
}
}

dfs(root.left,sum - root.val);
dfs(root.right,sum - root.val);
}
}


113. 路径总和 II

给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。
叶子节点 是指没有子节点的节点。

1
2
输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:[[5,4,11,2],[5,8,4,5]]

1
2
输入:root = [1,2,3], targetSum = 5
输出:[]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//比I多记录一下当前遍历树的数组状态即可
class Solution {
public:
vector<vector<int>> res;
void dfs(TreeNode* root,vector<int> &tmp,int sum){
if(!root) return ;
tmp.push_back(root->val);
if(root->val==sum&&(root->left==NULL&&root->right==NULL)){
res.push_back(tmp);
}
dfs(root->left,tmp,sum-root->val);
dfs(root->right,tmp,sum-root->val);
tmp.pop_back();
}
vector<vector<int>> pathSum(TreeNode* root, int sum) {
vector<int> tmp;
dfs(root,tmp,sum);
return res;
}
};


437. 路径总和 III

给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。
路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。

1
2
3
输入:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8
输出:3
解释:和等于 8 的路径有 3 条,如图所示。
1
2
输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:3

从竞赛题5346过来的,今天学到了叫做双重递归的操作,这种题目需要从每个节点开始进行类似的计算,所以第一个递归用来遍历这些节点,这二个递归用来处理这些节点,进行深度优先搜索。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
int count = 0;
int pathSum(TreeNode* root, int sum) {
if (!root) return 0;
dfs(root, sum) ;
pathSum(root->left, sum) ;
pathSum(root->right, sum);
return count;
}
void dfs(TreeNode* root, int sum) {
if (!root) return;
if (sum - root->val == 0) count++;
dfs(root->left, sum - root->val);
dfs(root->right, sum - root->val);
}
};


543. 二叉树的直径

给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。

示例 :
给定二叉树

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

返回 3, 它的长度是路径 [4,2,1,3] 或者 [5,2,1,3]。

注意:两结点之间的路径长度是以它们之间边的数目表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
int max = 0;
public int diameterOfBinaryTree(TreeNode root) {
if (root == null) {
return 0;
}
dfs(root);
return max;
}

private int dfs(TreeNode root) {
if (root.left == null && root.right == null) {
return 0;
}
int leftSize = root.left == null? 0: dfs(root.left) + 1;
int rightSize = root.right == null? 0: dfs(root.right) + 1;
max = Math.max(max, leftSize + rightSize);
return Math.max(leftSize, rightSize);
}
}


257. 二叉树的所有路径

给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。
叶子节点 是指没有子节点的节点。

1
2
输入:root = [1,2,3,null,5]
输出:["1->2->5","1->3"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
vector<string> binaryTreePaths(TreeNode* root) {
vector<string> res;
if (root == nullptr) return res;

binaryTreePaths(root, res, "");
return res;
}

void binaryTreePaths(TreeNode * root, vector<string> & res, string path) {
path += to_string(root->val);

if (root->left == nullptr && root->right == nullptr) {
res.push_back(path);
return;
}

if (root->left) binaryTreePaths(root->left, res, path + "->");
if (root->right) binaryTreePaths(root->right, res, path + "->");
}
};


110. 平衡二叉树

给定一个二叉树,判断它是否是高度平衡的二叉树。
本题中,一棵高度平衡二叉树定义为:

一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。

1
2
输入:root = [3,9,20,null,null,15,7]
输出:true

1
2
输入:root = [1,2,2,3,3,null,null,4,4]
输出:false
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
class Solution {
//这个ReturnNode是参考我描述的递归套路的第二步:思考返回值是什么
//一棵树是BST等价于它的左、右俩子树都是BST且俩子树高度差不超过1
//因此我认为返回值应该包含当前树是否是BST和当前树的高度这两个信息
private class ReturnNode{
boolean isB;
int depth;
public ReturnNode(int depth, boolean isB){
this.isB = isB;
this.depth = depth;
}
}
//主函数
public boolean isBalanced(TreeNode root) {
return isBST(root).isB;
}
//参考递归套路的第三部:描述单次执行过程是什么样的
//这里的单次执行过程具体如下:
//是否终止?->没终止的话,判断是否满足不平衡的三个条件->返回值
public ReturnNode isBST(TreeNode root){
if(root == null){
return new ReturnNode(0, true);
}
//不平衡的情况有3种:左树不平衡、右树不平衡、左树和右树差的绝对值大于1
ReturnNode left = isBST(root.left);
ReturnNode right = isBST(root.right);
if(left.isB == false || right.isB == false){
return new ReturnNode(0, false);
}
if(Math.abs(left.depth - right.depth) > 1){
return new ReturnNode(0, false);
}
//不满足上面3种情况,说明平衡了,树的深度为左右俩子树最大深度+1
return new ReturnNode(Math.max(left.depth, right.depth) + 1, true);
}
}




基础遍历

二叉树基础遍历

144.二叉树前序遍历

给你二叉树的根节点 root ,返回它节点值的 前序 遍历。

1
2
输入:root = [1,null,2,3]
输出:[1,2,3]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
void traversal(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
vec.push_back(cur->val); // 中
traversal(cur->left, vec); // 左
traversal(cur->right, vec); // 右
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
traversal(root, result);
return result;
}
};


94.二叉树的中序遍历

给定一个二叉树的根节点 root ,返回它的 中序 遍历。

1
2
输入:root = [1,null,2,3]
输出:[1,3,2]

基于栈的中序遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
while (cur != null || !stack.isEmpty()) {
if (cur != null) {
stack.push(cur);
cur = cur.left;
} else {
cur = stack.pop();
list.add(cur.val);
cur = cur.right;
}
}
return list;
}
}

145.二叉树的后序遍历

给你一棵二叉树的根节点 root ,返回其节点值的 后序遍历 。

1
2
输入:root = [1,null,2,3]
输出:[3,2,1]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
void traversal(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
traversal(cur->left, vec); // 左
traversal(cur->right, vec); // 右
vec.push_back(cur->val); // 右

}
vector<int> postorderTraversal(TreeNode* root) {
vector<int> result;
traversal(root, result);
return result;
}
};

总结-递归版前中后序遍历

递归-前序遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
void traversal(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
vec.push_back(cur->val); // 中
traversal(cur->left, vec); // 左
traversal(cur->right, vec); // 右
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
traversal(root, result);
return result;
}
};

递归-中序遍历

1
2
3
4
5
6
void traversal(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
traversal(cur->left, vec); // 左
vec.push_back(cur->val); // 中
traversal(cur->right, vec); // 右
}

递归-后序遍历

1
2
3
4
5
6
void traversal(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
traversal(cur->left, vec); // 左
traversal(cur->right, vec); // 右
vec.push_back(cur->val); // 后
}


二叉树层次遍历

102.二叉树的层序遍历

给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。

1
2
输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
if(root == null)
return new ArrayList<>();
List<List<Integer>> res = new ArrayList<>();
Queue<TreeNode> queue = new LinkedList<TreeNode>();
queue.add(root);
while(!queue.isEmpty()){
int count = queue.size();
List<Integer> list = new ArrayList<Integer>();
while(count > 0){
TreeNode node = queue.poll();
list.add(node.val);
if(node.left != null)
queue.add(node.left);
if(node.right != null)
queue.add(node.right);
count--;
}
res.add(list);
}
return res;
}
}


107.二叉树的层序遍历2

给你二叉树的根节点 root ,返回其节点值 自底向上的层序遍历 。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)

1
2
输入:root = [3,9,20,null,null,15,7]
输出:[[15,7],[9,20],[3]]
1
2
输入:root = [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
class Solution {
public List<List<Integer>> levelOrderBottom(TreeNode root) {
LinkedList<List<Integer>> result = new LinkedList<>();
if (root == null)
return result;
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
while (!queue.isEmpty()) {
List<Integer> oneLevel = new ArrayList<>();
// 每次都取出一层的所有数据
int count = queue.size();
for (int i = 0; i < count; i++) {
TreeNode node = queue.poll();
oneLevel.add(node.val);
if (node.left != null)
queue.add(node.left);
if (node.right != null)
queue.add(node.right);
}
// 每次都往队头塞
result.addFirst(oneLevel);
}
return result;
}
}


N叉树基础遍历

589.N叉树的前序遍历

给定一个 N 叉树,返回其节点值的 前序遍历 。
N 叉树 在输入中按层序遍历进行序列化表示,每组子节点由空值 null 分隔(请参见示例)。

进阶:
递归法很简单,你可以使用迭代法完成此题吗?

1
2
输入:root = [1,null,3,2,4,null,5,6]
输出:[1,3,5,6,2,4]

1
2
输入:root = [1,null,2,3,4,5,null,null,6,7,null,8,null,9,10,null,null,11,null,12,null,13,null,null,14]
输出:[1,2,3,6,7,11,14,4,8,12,5,9,13,10]

递归版解题思路:

参考二叉树的递归遍历方式:先遍历根节点,然后递归遍历左子树,再递归遍历右子树。
二N叉树的前序遍历就是先遍历根节点,然后依次递归遍历每个子树。
时间复杂度O(N),空间复杂度O(N)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
List<Integer> res = new ArrayList<Integer>();
public List<Integer> preorder(Node root) {
//递归版

if(root == null)
return res;
res.add(root.val);
for(Node child:root.children)
{
preorder(child);
}

return res;
}
}


590.N叉树的后序遍历

给定一个 N 叉树,返回其节点值的 后序遍历 。

N 叉树 在输入中按层序遍历进行序列化表示,每组子节点由空值 null 分隔(请参见示例)。

1
2
输入:root = [1,null,3,2,4,null,5,6]
输出:[5,6,3,2,4,1]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
vector<int> postorder(Node* root) {
vector<int> result;
postorder(root, result);
return result;
}

void postorder(Node* root, vector<int>& result) {
if(root == nullptr)
return;
for(auto node : root->children)
postorder(node, result);
result.push_back(root->val);
}
}


429.N叉树的层序遍历

给定一个 N 叉树,返回其节点值的层序遍历。(即从左到右,逐层遍历)。
树的序列化输入是用层序遍历,每组子节点都由 null 值分隔(参见示例)。

1
2
输入:root = [1,null,3,2,4,null,5,6]
输出:[[1],[3,2,4],[5,6]]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#DFS
class Solution:
def levelOrder(self, root: 'Node') -> List[List[int]]:

res = []

def dfs(root, depth):
if not root: return
if len(res) <= depth:
res.append([])
res[depth].append(root.val)
for ch in root.children:
dfs(ch, depth+1)

dfs(root, 0)
return res

#BFS
class Solution:
def levelOrder(self, root: 'Node') -> List[List[int]]:
if not root:return []
res = []

def bfs(root):
queue = [root]
while queue:
nxt = []
tmp = []
for node in queue:
tmp.append(node.val)
for ch in node.children:
nxt.append(ch)
res.append(tmp)
queue = nxt

bfs(root)
return res


二叉树深度

104. 二叉树的最大深度

给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。

1
2
3
4
5
6
7
8
示例:
给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
最大深度为3
1
2
3
4
5
class Solution {
public int maxDepth(TreeNode root) {
return root == null ? 0 : Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
}
}

111. 二叉树的最小深度

给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
说明:叶子节点是指没有子节点的节点。

1
2
输入:root = [3,9,20,null,null,15,7]
输出:2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public int minDepth(TreeNode root) {
if (root == null) {
return 0;
}
// null节点不参与比较
if (root.left == null && root.right != null) {
return 1 + minDepth(root.right);
}
// null节点不参与比较
if (root.right == null && root.left != null) {
return 1 + minDepth(root.left);
}

return 1 + Math.min(minDepth(root.left), minDepth(root.right));
}
}


自顶向下

104.二叉树的最大深度

给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。

1
2
3
4
5
6
7
8
示例:
给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
最大深度为3
1
2
3
4
5
class Solution {
public int maxDepth(TreeNode root) {
return root == null ? 0 : Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
}
}


112. 路径总和

给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false 。
叶子节点 是指没有子节点的节点。

1
2
3
输入:root = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22
输出:true
解释:等于目标和的根节点到叶节点路径如上图所示。

1
2
3
4
5
6
输入:root = [1,2,3], targetSum = 5
输出:false
解释:树中存在两条根节点到叶子节点的路径:
(1 --> 2): 和为 3
(1 --> 3): 和为 4
不存在 sum = 5 的根节点到叶子节点的路径。
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
//深度优先遍历二叉树,每深入一次,sum-根节点的值,当到达叶子节点的时候,判断sum是否等于当前的节点值,如果等于,说明找到了,否则尝试另外一条路径
class Solution {
boolean ans = false;
public boolean hasPathSum(TreeNode root, int sum) {
if (root == null) {
return false;
}
dfs(root,sum);
return ans;
}

private void dfs(TreeNode root,int sum) {
if (root == null) {
return;
}

if (root.left == null && root.right == null) {
if (sum == root.val) {
ans = true;
}
}

dfs(root.left,sum - root.val);
dfs(root.right,sum - root.val);
}
}


113. 路径总和 II

给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。
叶子节点 是指没有子节点的节点。

1
2
输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:[[5,4,11,2],[5,8,4,5]]

1
2
输入:root = [1,2,3], targetSum = 5
输出:[]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//比I多记录一下当前遍历树的数组状态即可
class Solution {
public:
vector<vector<int>> res;
void dfs(TreeNode* root,vector<int> &tmp,int sum){
if(!root) return ;
tmp.push_back(root->val);
if(root->val==sum&&(root->left==NULL&&root->right==NULL)){
res.push_back(tmp);
}
dfs(root->left,tmp,sum-root->val);
dfs(root->right,tmp,sum-root->val);
tmp.pop_back();
}
vector<vector<int>> pathSum(TreeNode* root, int sum) {
vector<int> tmp;
dfs(root,tmp,sum);
return res;
}
};


437. 路径总和 III

给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。
路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。

1
2
3
输入:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8
输出:3
解释:和等于 8 的路径有 3 条,如图所示。
1
2
输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:3

从竞赛题5346过来的,今天学到了叫做双重递归的操作,这种题目需要从每个节点开始进行类似的计算,所以第一个递归用来遍历这些节点,这二个递归用来处理这些节点,进行深度优先搜索。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
int count = 0;
int pathSum(TreeNode* root, int sum) {
if (!root) return 0;
dfs(root, sum) ;
pathSum(root->left, sum) ;
pathSum(root->right, sum);
return count;
}
void dfs(TreeNode* root, int sum) {
if (!root) return;
if (sum - root->val == 0) count++;
dfs(root->left, sum - root->val);
dfs(root->right, sum - root->val);
}
};


257. 二叉树的所有路径

给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。
叶子节点 是指没有子节点的节点。

1
2
输入:root = [1,2,3,null,5]
输出:["1->2->5","1->3"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
vector<string> binaryTreePaths(TreeNode* root) {
vector<string> res;
if (root == nullptr) return res;

binaryTreePaths(root, res, "");
return res;
}

void binaryTreePaths(TreeNode * root, vector<string> & res, string path) {
path += to_string(root->val);

if (root->left == nullptr && root->right == nullptr) {
res.push_back(path);
return;
}

if (root->left) binaryTreePaths(root->left, res, path + "->");
if (root->right) binaryTreePaths(root->right, res, path + "->");
}
};


129.求根节点到叶节点数字之和

给你一个二叉树的根节点 root ,树中每个节点都存放有一个 0 到 9 之间的数字。
每条从根节点到叶节点的路径都代表一个数字:

例如,从根节点到叶节点的路径 1 -> 2 -> 3 表示数字 123 。
计算从根节点到叶节点生成的 所有数字之和 。
叶节点 是指没有子节点的节点。

1
2
3
4
5
6
输入:root = [1,2,3]
输出:25
解释:
从根到叶子节点路径 1->2 代表数字 12
从根到叶子节点路径 1->3 代表数字 13
因此,数字总和 = 12 + 13 = 25
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public://一版
int helper(TreeNode* root, int sum){
if(!root)
return 0;
else if (!root->left && !root->right)
return 10*sum + root->val;
return helper(root->left, 10*sum + root->val) +
helper(root->right, 10*sum + root->val);
}
int sumNumbers(TreeNode* root) {
return helper(root, 0);
}
};


988.从叶节点开始的最小字符串

给定一颗根结点为 root 的二叉树,树中的每一个结点都有一个从 0 到 25 的值,分别代表字母 ‘a’ 到 ‘z’:值 0 代表 ‘a’,值 1 代表 ‘b’,依此类推。

找出按字典序最小的字符串,该字符串从这棵树的一个叶结点开始,到根结点结束。
(小贴士:字符串中任何较短的前缀在字典序上都是较小的:例如,在字典序上 “ab” 比 “aba” 要小。叶结点是指没有子结点的结点。)

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
输入:[0,1,2,3,4,3,4]
输出:"dba"
``` 

```java
class Solution {
public String smallestFromLeaf(TreeNode root) {
dfs(root,new StringBuilder());
return res;
}
String res = null;
public void dfs(TreeNode root,StringBuilder s){
if(root==null){
return;
}else{
char c = (char)(root.val+'a');
s.append(c);
dfs(root.left,s);
dfs(root.right,s);
if(root.left==null&&root.right==null){
String cur = s.reverse().toString();
s.reverse();
if(res==null || res.compareTo(cur)>0){
res = cur;
}
}
s.deleteCharAt(s.length()-1);
}
}
}

非自顶向下

687.最长同值路径

124.二叉树中最大的路径和

543.二叉树的直径

652.寻找重复的子树

236.二叉树的最近公共祖先

235.二叉搜索树的最近公共祖先