面试整理——算法

算法

一.综合

二.数据结构

问:讲讲红黑树?如何计算红黑树的时间复杂度?

红黑树算法:

平衡二叉树算法:插入时,需要判断是否会破坏树的平衡性,若是则要找到最小不平衡树,通过相应的旋转调整最小不平衡树中各个结点的关系,使其再度平衡。平衡因子就是左子树深度和右子树深度的差值,只能为-1、0、1。当最小不平衡子树的平衡因子和其子树的平衡因子符号相反时,要对结点先进行一次旋转使符号相同,再反向旋转一次完成平衡操作。

红黑树的时间复杂度:TODO

问:红黑树和AVL树?

红黑树本质是2-3树,属于平衡二叉树,其2-结点等价于普通二叉树结点,3-结点本质是非平衡性的缓存。红黑树本质是用空间换时间,通过牺牲平衡性减少了插入/删除时的旋转次数,所以查询会略慢于AVL树,但综合各种情况来看性能会略优于AVL树。

红黑树非严格平衡二叉树,因为其用红黑链+二叉树来实现3-结点的设计,导致无法严格控制所有空链接到根结点距离相等。

AVL树即平衡二叉查找树,要求是一个空树或左右子树高度差不能超过1,子树仍是平衡二叉树。AVL是其中一种平衡二叉树实现

AVL树查询效率会略高于红黑树。

AVL树被应用于Windows进程地址空间管理。

综合:

  • AVL是严格的平衡树,因此在增加或者删除节点的时候,根据不同情况,旋转的次数比红黑树要多;
  • 红黑树是用非严格的平衡来换取增删节点时候旋转次数的降低开销;
  • 红黑树综合性能略高,AVL查询速度略快。所以简单说,查询多选择AVL树,查询更新次数差不多选红黑树。
  • AVL树顺序插入和删除时有20%左右的性能优势,红黑树随机操作15%左右优势,现实应用当然一般都是随机情况,所以红黑树得到了更广泛的应用 索引为B+树 Hashmap为红黑树。

问:为啥redis zset使用跳跃链表而不用红黑树实现

  • skiplist的复杂度和红黑树一样,而且实现起来更简单。
  • 在并发环境下红黑树在插入和删除时需要rebalance,性能不如跳表。

问:讲下跳表怎么实现的?哈夫曼编码是怎么回事?非递归且不用额外空间(不用栈),如何遍历二叉树?

问:跳表和二叉树的区别?

  • 跳表维持结构平衡的成本较低,完全依靠随机;二叉树则在多次插入删除后,通过再平衡操作来重新调整结构平衡。
  • 跳表非树结构,分索引节点和底层链表节点,索引节点只有右边和下边两条索引,链表节点则只有下个节点索引,而二叉树节点则有左右子结点。

补充:Redis通过跳跃表的优化实现Sorted-set维护有序集合,而关系型数据库则采用B+树。

TODO

问:B 树、B+树?什么是B树?什么是B+树?二者的区别?

什么是B树:B树是一种自平衡的树,允许一个结点拥有两个以上的子结点,保证数据的有序。

什么是B+树:B+树是B树的一种变体,键值的拷贝被存储在内部结点,键值和记录则存储在叶子结点,叶子结点间相互链接组成一组有序链表。

二者的区别:

  1. B+树的层级更少:相较于B树,B+树的每个非叶子节点存储的关键字数更多,树的层级更少所以查询数据更快;
  2. B+树查询速度更稳定:B+所有关键字数据地址都存在叶子节点上,所以每次查找的次数都相同所以查询速度要比B树更稳定;
  3. B+树天然具备排序功能:B+树所有的叶子节点数据构成了一个有序链表,在查询大小区间的数据时候更方便,数据紧密性很高,缓存的命中率也会比B树高。
  4. B+树全节点遍历更快:B+树遍历整棵树只需要遍历所有的叶子节点即可,而不需要像B树一样需要对每一层进行遍历,这有利于数据库做全表扫描。

问:讲下常见的排序算法及其复杂度?怎么实现一个快排?树一般怎么实现,链表怎么实现?

问:堆是怎么实现?小根堆如何插入数据,讲一下过程?

问:判断链表是否有环?

三.算法题

leetcode面试经典150题

3.1 数组/字符串 Arrays/Strings

88.合并两个有序数组 <easy> 双指针

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
95
96
97
98
99
100
/*    给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。

请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。

注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。

示例 1:

输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
解释:需要合并 [1,2,3] 和 [2,5,6] 。
合并结果是 [1,2,2,3,5,6] ,其中斜体加粗标注的为 nums1 中的元素。
示例 2:

输入:nums1 = [1], m = 1, nums2 = [], n = 0
输出:[1]
解释:需要合并 [1] 和 [] 。
合并结果是 [1] 。
示例 3:

输入:nums1 = [0], m = 0, nums2 = [1], n = 1
输出:[1]
解释:需要合并的数组是 [] 和 [1] 。
合并结果是 [1] 。
注意,因为 m = 0 ,所以 nums1 中没有元素。nums1 中仅存的 0 仅仅是为了确保合并结果可以顺利存放到 nums1 中。


提示:

nums1.length == m + n
nums2.length == n
0 <= m, n <= 200
1 <= m + n <= 200
-109 <= nums1[i], nums2[j] <= 109


进阶:你可以设计实现一个时间复杂度为 O(m + n) 的算法解决此问题吗?*/
public class MergeTwoSortedArrays88 {

public static void merge(int[] nums1, int m, int[] nums2, int n) {
//// 1.先合并后排序
//for (int i = 0; i != n; ++i) {
// nums1[m + i] = nums2[i];
//}
//// 排序序列长度为 m+n,套用快速排序的时间复杂度即可,平均情况为 O((m+n)log(m+n))
//Arrays.sort(nums1);

//2.逆向双指针,因为数组是有序的,用两个指针分别指向两个数组的首个元素,对比大小
// 因为数组1后面是空的,所以倒叙遍历可以直接覆盖后面的元素
int i = m - 1;
int j = n - 1;
int k = m + n - 1;

// 循环直到两个指针都走完
while (i >= 0 && j >= 0) {
// 对比两个数组,较大的值放入队尾
if (nums1[i] > nums2[j]) {
nums1[k--] = nums1[i--];
} else {
nums1[k--] = nums2[j--];
}
}

// 若数组1为空则遍历数组2放入数组1
while (j >= 0) {
nums1[k--] = nums2[j--];
}
// 时间复杂大为O(m + n),因为只需要遍历一遍两个数组,进行一次比较和移动操作
}


public static void main(String[] args) {
int[] nums1_1 = {1, 2, 3, 0, 0, 0};
int[] nums2_1 = {2, 5, 6};
int m1 = 3;
int n1 = 3;

merge(nums1_1, m1, nums2_1, n1);
System.out.print("Test Case 1: ");
System.out.println(Arrays.toString(nums1_1));

int[] nums1_2 = {1};
int[] nums2_2 = {};
int m2 = 1;
int n2 = 0;

merge(nums1_2, m2, nums2_2, n2);
System.out.print("Test Case 2: ");
System.out.println(Arrays.toString(nums1_2));

int[] nums1_3 = {0};
int[] nums2_3 = {1};
int m3 = 0;
int n3 = 1;

merge(nums1_3, m3, nums2_3, n3);
System.out.print("Test Case 3: ");
System.out.println(Arrays.toString(nums1_3));
}
}

27. 移除元素 <easy> 双指针

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

/*给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。



说明:

为什么返回数值是整数,但输出的答案是数组呢?

请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。

你可以想象内部操作如下:

// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
int len = removeElement(nums, val);

// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}


示例 1:

输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。
示例 2:

输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,3,0,4]
解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。


提示:

0 <= nums.length <= 100
0 <= nums[i] <= 50
0 <= val <= 100*/

public class RemoveElement27 {

public static int removeElement(int[] nums, int val) {
// 因为不需要考虑数组中超出新长度后面的元素,并且一次遍历完成操作,所以直接覆盖原数组元素即可
int i = 0;
for (int j = 0; j < nums.length; j++) {
if (nums[j] != val) {
nums[i] = nums[j];
i++;
}
}
return i;
// 时间复杂大为O(n),因为只需要遍历一遍数组
}

public static void main(String[] args) {
int[] nums1 = {3, 2, 2, 3};
int val1 = 3;
int len1 = removeElement(nums1, val1);
System.out.println("Test Case 1:");
System.out.println("Length: " + len1 + ", nums = ");
printArray(nums1, len1);

int[] nums2 = {0, 1, 2, 2, 3, 0, 4, 2};
int val2 = 2;
int len2 = removeElement(nums2, val2);
System.out.println("Test Case 2:");
System.out.println("Length: " + len2 + ", nums = ");
printArray(nums2, len2);
}

public static void printArray(int[] arr, int len) {
System.out.print("[");
for (int i = 0; i < len; i++) {
if (i < len - 1) {
System.out.print(arr[i] + ", ");
} else {
System.out.print(arr[i]);
}
}
System.out.println("]");
}
}

26. 删除有序数组中的重复项 <easy> 双指针

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
/*给你一个 非严格递增排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。

考虑 nums 的唯一元素的数量为 k ,你需要做以下事情确保你的题解可以被通过:

更改数组 nums ,使 nums 的前 k 个元素包含唯一元素,并按照它们最初在 nums 中出现的顺序排列。nums 的其余元素与 nums 的大小不重要。
返回 k 。
判题标准:

系统会用下面的代码来测试你的题解:

int[] nums = [...]; // 输入数组
int[] expectedNums = [...]; // 长度正确的期望答案

int k = removeDuplicates(nums); // 调用

assert k == expectedNums.length;
for (int i = 0; i < k; i++) {
assert nums[i] == expectedNums[i];
}
如果所有断言都通过,那么您的题解将被 通过。

示例 1:

输入:nums = [1,1,2]
输出:2, nums = [1,2,_]
解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。
示例 2:

输入:nums = [0,0,1,1,1,2,2,3,3,4]
输出:5, nums = [0,1,2,3,4]
解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。


提示:

1 <= nums.length <= 3 * 104
-104 <= nums[i] <= 104
nums 已按 非严格递增 排列*/
public class RemoveDuplicatesFromSortedArray26 {

public static int removeDuplicates(int[] nums) {
// 有序数组的去重和合并,都可以考虑用双指针来解决
if (nums.length == 0) {
return 0;
}
int i = 0;
// 相邻元素相等时由后一位替代,不相等时不动
for(int j = 1;j < nums.length;j++) {
// 相邻元素相等时i指针不动,直到j指针标到不相等元素时再替换相邻的下一个元素
if (nums[i] != nums[j]) {
i++;
nums[i] = nums[j];
}
}
return i + 1;
// 时间复杂度是 O(n)
}


public static void printArray(int[] arr, int len) {
System.out.print("[");
for (int i = 0; i < len; i++) {
if (i < len - 1) {
System.out.print(arr[i] + ", ");
} else {
System.out.print(arr[i]);
}
}
System.out.println("]");
}

public static void main(String[] args) {
int[] nums1 = {1, 1, 2};
int k1 = removeDuplicates(nums1);
System.out.println("Test Case 1:");
System.out.println("Length: " + k1 + ", nums = ");
printArray(nums1, k1);

int[] nums2 = {0, 0, 1, 1, 1, 2, 2, 3, 3, 4};
int k2 = removeDuplicates(nums2);
System.out.println("Test Case 2:");
System.out.println("Length: " + k2 + ", nums = ");
printArray(nums2, k2);
}
}

80. 删除有序数组中的重复项 II <medium> 双指针

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
/*给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使得出现次数超过两次的元素只出现两次 ,返回删除后数组的新长度。

不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。



说明:

为什么返回数值是整数,但输出的答案是数组呢?

请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。

你可以想象内部操作如下:

// nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝
int len = removeDuplicates(nums);

// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}


示例 1:

输入:nums = [1,1,1,2,2,3]
输出:5, nums = [1,1,2,2,3]
解释:函数应返回新长度 length = 5, 并且原数组的前五个元素被修改为 1, 1, 2, 2, 3。 不需要考虑数组中超出新长度后面的元素。
示例 2:

输入:nums = [0,0,1,1,1,1,2,3,3]
输出:7, nums = [0,0,1,1,2,3,3]
解释:函数应返回新长度 length = 7, 并且原数组的前五个元素被修改为 0, 0, 1, 1, 2, 3, 3。不需要考虑数组中超出新长度后面的元素。


提示:

1 <= nums.length <= 3 * 104
-104 <= nums[i] <= 104
nums 已按升序排列*/
public class RemoveDuplicatesFromSortedArray80 {
public static int removeDuplicates(int[] nums) {
if (nums.length <= 2) {
return nums.length;
}

// 双指针
int slow = 2;
for (int fast = 2; fast < nums.length; fast++) {
// 跨两位比较,当值不匹配是双指针同速,匹配则慢指针停留,直到下一个不相等时与后面相隔位置交换
if (nums[slow - 2] != nums[fast]) {
nums[slow] = nums[fast];
slow++;
}
}
// 时间复杂度是 O(n)
return slow;
}

public static void printArray(int[] arr, int len) {
System.out.print("[");
for (int i = 0; i < len; i++) {
if (i < len - 1) {
System.out.print(arr[i] + ", ");
} else {
System.out.print(arr[i]);
}
}
System.out.println("]");
}

public static void main(String[] args) {
int[] nums1 = {1, 1, 1, 2, 2, 3};
int k1 = removeDuplicates(nums1);
System.out.println("Test Case 1:");
System.out.println("Length: " + k1 + ", nums = ");
printArray(nums1, k1);

int[] nums2 = {0, 0, 1, 1, 1, 1, 2, 3, 3};
int k2 = removeDuplicates(nums2);
System.out.println("Test Case 2:");
System.out.println("Length: " + k2 + ", nums = ");
printArray(nums2, k2);
}
}

169. 多数元素 <easy>

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
/*给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。



示例 1:

输入:nums = [3,2,3]
输出:3
示例 2:

输入:nums = [2,2,1,1,1,2,2]
输出:2


提示:
n == nums.length
1 <= n <= 5 * 104
-109 <= nums[i] <= 109


进阶:尝试设计时间复杂度为 O(n)、空间复杂度为 O(1) 的算法解决此问题。*/
public class MajorityElement169 {
public static int majorityElement(int[] nums) {
int count = 0;
int candidate = 0;

for (int num : nums) {
if (count == 0) {
candidate = num;
}
// 命中则+1,否则-1,直到count减为0则换为新的,最终留下的则是多数元素,否则说明没有超过1/2的元素
count += (num == candidate) ? 1 : -1;
}

return candidate;
}

public static void main(String[] args) {
int[] nums1 = {3, 2, 3};
int result1 = majorityElement(nums1);
System.out.println("Test Case 1:");
System.out.println("Majority Element: " + result1);

int[] nums2 = {2, 2, 1, 1, 1, 2, 2};
int result2 = majorityElement(nums2);
System.out.println("Test Case 2:");
System.out.println("Majority Element: " + result2);

int[] nums3 = {1, 2, 3};
int result3 = majorityElement(nums3);
System.out.println("Test Case 3:");
System.out.println("Majority Element: " + result3);
}
}

189. 轮转数组 <medium> 数组翻转

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
95
96
97
98
99
100
101
102
103
104
/*给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。

示例 1:

输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右轮转 1 步: [7,1,2,3,4,5,6]
向右轮转 2 步: [6,7,1,2,3,4,5]
向右轮转 3 步: [5,6,7,1,2,3,4]
示例 2:

输入:nums = [-1,-100,3,99], k = 2
输出:[3,99,-1,-100]
解释:
向右轮转 1 步: [99,-1,-100,3]
向右轮转 2 步: [3,99,-1,-100]


提示:

1 <= nums.length <= 105
-231 <= nums[i] <= 231 - 1
0 <= k <= 105


进阶:

尽可能想出更多的解决方案,至少有 三种 不同的方法可以解决这个问题。
你可以使用空间复杂度为 O(1) 的 原地 算法解决这个问题吗?*/
public class RotateArray189 {

public static void rotate(int[] nums, int k) {
int n = nums.length;
k %= n;
// 第一次倒转,将整个数组倒转 7,6,5,4,3,2,1
reverse(nums, 0, n - 1);
// 第二次倒转,将前k个元素倒转 5,6,7,4,3,2,1
reverse(nums, 0, k - 1);
// 第三次倒转,将后面的元素倒转 5,6,7,1,2,3,4
reverse(nums, k, n - 1);
}

public static void main(String[] args) {
int[] nums1 = {1, 2, 3, 4, 5, 6, 7};
int k1 = 3;
rotate(nums1, k1);
System.out.println("Test Case 1:");
System.out.println(Arrays.toString(nums1));

int[] nums2 = {-1, -100, 3, 99};
int k2 = 2;
rotate(nums2, k2);
System.out.println("Test Case 2:");
System.out.println(Arrays.toString(nums2));
}

// 使用额外数组 O(n)
public static void rotateExtraArray(int[] nums, int k) {
int n = nums.length;
int[] result = new int[n];
for (int i = 0; i < n; i++) {
// 通过取余数来实现循环右移
result[(i + k) % n] = nums[i];
}
System.arraycopy(result, 0, nums, 0, n);
}

// 暴力旋转 O(n * k)
public static void rotateBruteForce(int[] nums, int k) {
// 每次循环数组右移1位,直到右移K位
for (int i = 0;i < k;i++) {
int end = nums[nums.length - 1];
for (int j = nums.length - 1;j > -1; j--) {
if (j == 0) {
nums[j] = end;
} else {
nums[j] = nums[j - 1];
}
}
}
}

// 翻转数组 O(n)
public static void rotateReverse(int[] nums, int k) {
int n = nums.length;
k %= n;
reverse(nums, 0, n - 1);
reverse(nums, 0, k - 1);
reverse(nums, k, n - 1);
}

private static void reverse(int[] nums, int start, int end) {
// 首位调转位置
while (start < end) {
int temp = nums[start];
nums[start] = nums[end];
nums[end] = temp;
start++;
end--;
}
}
}

121. 买卖股票的最佳时机 <easy>

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
/*给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。



示例 1:

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

输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。


提示:

1 <= prices.length <= 105
0 <= prices[i] <= 104*/
public class BestTimeToBuyAndSellStock121 {

public static int maxProfit(int[] prices) {
//int buy;
//int sale;
//int result = 0;
//for (int i = 0;i < prices.length;i++) {
// buy = prices[i];
// for (int j = i+1;j < prices.length;j++) {
// sale = prices[j];
// result = Math.max((sale - buy), result);
// }
//}
//result = Math.max(0, result);
// O(n^2)
//return result;
int maxProfit = 0;
int minPrice = Integer.MAX_VALUE;
for (int price : prices) {
// 取当前最小价格
minPrice = Math.min(minPrice, price);
// 取当前最大差值
maxProfit = Math.max(maxProfit, price - minPrice);
}
// O(n)
return maxProfit;
}

public static void main(String[] args) {
int[] prices1 = {7, 1, 5, 3, 6, 4};
int maxProfit1 = maxProfit(prices1);
System.out.println("Test Case 1:");
System.out.println("Max Profit: " + maxProfit1);

int[] prices2 = {7, 6, 4, 3, 1};
int maxProfit2 = maxProfit(prices2);
System.out.println("Test Case 2:");
System.out.println("Max Profit: " + maxProfit2);

int[] prices3 = {4, 6, 3, 5, 1, 2};
int maxProfit3 = maxProfit(prices3);
System.out.println("Test Case 3:");
System.out.println("Max Profit: " + maxProfit3);
}

}

122. 买卖股票的最佳时机 II <medium>

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
/*给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

返回 你能获得的 最大 利润 。



示例 1:

输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3 。
总利润为 4 + 3 = 7 。
示例 2:

输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
总利润为 4 。
示例 3:

输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0 。


提示:

1 <= prices.length <= 3 * 104
0 <= prices[i] <= 104*/
public class BestTimeToBuyAndSellStock122 {

public static int maxProfit(int[] prices) {
//int buy = 0;
//boolean flag = false;
//int total = 0;
//for (int i = 0;i < prices.length;i++) {
// // 是否卖出
// if (flag && (i+1 == prices.length || prices[i+1] < prices[i])) {
// total += prices[i] - buy;
// flag = false;
// }
// // 是否买入
// if (!flag && i+1 != prices.length && prices[i+1] > prices[i]) {
// buy = prices[i];
// // 已持有
// flag = true;
// }
//}
//// O(n)
//return total;

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

public static void main(String[] args) {
int[] prices1 = {7, 1, 5, 3, 6, 4};
int maxProfit1 = maxProfit(prices1);
System.out.println("Test Case 1:");
System.out.println("Max Profit: " + maxProfit1);

int[] prices2 = {1, 2, 3, 4, 5};
int maxProfit2 = maxProfit(prices2);
System.out.println("Test Case 2:");
System.out.println("Max Profit: " + maxProfit2);

int[] prices3 = {4, 6, 3, 5, 1, 2};
int maxProfit3 = maxProfit(prices3);
System.out.println("Test Case 3:");
System.out.println("Max Profit: " + maxProfit3);
}
}

55. 跳跃游戏 <medium> 贪心

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
/*给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false 。



示例 1:

输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
示例 2:

输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。


提示:

1 <= nums.length <= 104
0 <= nums[i] <= 105*/
public class JumpGame55 {

public static boolean canJump(int[] nums) {
// //贪心算法
//int maxLength = 0;
//for (int i = 0;i < nums.length - 1;i++) {
// // 每次更新当前的最远距离
// maxLength = Math.max(nums[i], maxLength - 1);
// if (maxLength <= 0) {
// return false;
// }
//}
//return true;
int maxJump = 0;
for (int i = 0; i < nums.length; i++) {
if (i > maxJump) {
return false;
}
maxJump = Math.max(maxJump, i + nums[i]);
if (maxJump >= nums.length - 1) {
return true;
}
}
return true;
}

public static void main(String[] args) {
int[] nums1 = {2, 3, 1, 1, 4};
boolean result1 = canJump(nums1);
System.out.println("Test Case 1:");
System.out.println("Can Jump: " + result1);

int[] nums2 = {3, 2, 1, 0, 4};
boolean result2 = canJump(nums2);
System.out.println("Test Case 2:");
System.out.println("Can Jump: " + result2);
}

}

45. 跳跃游戏 II <medium> 贪心

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
/*给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。

每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:

0 <= j <= nums[i]
i + j < n
返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]。



示例 1:

输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
示例 2:

输入: nums = [2,3,0,1,4]
输出: 2


提示:

1 <= nums.length <= 104
0 <= nums[i] <= 1000
题目保证可以到达 nums[n-1]*/
public class JumpGame45 {
public static int jump(int[] nums) {
// 贪心算法
int jumps = 0;
int curEnd = 0;
int curFarthest = 0;

for (int i = 0; i < nums.length - 1; i++) {
// 取当前能到达的最远距离
curFarthest = Math.max(curFarthest, i + nums[i]);
// 到达跳跃处,更新跳跃次数
if (i == curEnd) {
jumps++;
curEnd = curFarthest;
}
}

// O(n)
return jumps;
}

public static void main(String[] args) {
int[] nums1 = {2, 3, 1, 1, 4};
int minJumps1 = jump(nums1);
System.out.println("Test Case 1:");
System.out.println("Minimum Jumps: " + minJumps1);

int[] nums2 = {2, 3, 0, 1, 4};
int minJumps2 = jump(nums2);
System.out.println("Test Case 2:");
System.out.println("Minimum Jumps: " + minJumps2);
}
}

274. H 指数 <medium>

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
/*给你一个整数数组 citations ,其中 citations[i] 表示研究者的第 i 篇论文被引用的次数。计算并返回该研究者的 h 指数。

根据维基百科上 h 指数的定义:h 代表“高引用次数” ,一名科研人员的 h 指数 是指他(她)至少发表了 h 篇论文,并且 至少 有 h 篇论文被引用次数大于等于 h 。如果 h 有多种可能的值,h 指数 是其中最大的那个。



示例 1:

输入:citations = [3,0,6,1,5]
输出:3
解释:给定数组表示研究者总共有 5 篇论文,每篇论文相应的被引用了 3, 0, 6, 1, 5 次。
由于研究者有 3 篇论文每篇 至少 被引用了 3 次,其余两篇论文每篇被引用 不多于 3 次,所以她的 h 指数是 3。
示例 2:

输入:citations = [1,3,1]
输出:1


提示:

n == citations.length
1 <= n <= 5000
0 <= citations[i] <= 1000*/
public class HIndex274 {

public static int hIndex(int[] citations) {
int n = citations.length;
// 存储每个引用次数对应的论文数量,引用数量超过论文数量的统计到队尾
int[] count = new int[n + 1];

for (int citation : citations) {
// count的下标对应引用次数,值则对应论文数量
if (citation >= n) {
count[n]++;
} else {
count[citation]++;
}
}

// 倒叙遍历,累加论文数量,直到满足论文数量 >= 引用次数
int total = 0;
for (int i = n; i >= 0; i--) {
total += count[i];
if (total >= i) {
return i;
}
}

// O(n)
return 0;
}

public static void main(String[] args) {
int[] citations1 = {3, 0, 6, 1, 5};
int hIndex1 = hIndex(citations1);
System.out.println("Test Case 1:");
System.out.println("H-Index: " + hIndex1);

int[] citations2 = {1, 3, 1};
int hIndex2 = hIndex(citations2);
System.out.println("Test Case 2:");
System.out.println("H-Index: " + hIndex2);
}
}

380. O(1) 时间插入、删除和获取随机元素 <medium>

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
/*实现RandomizedSet 类:

RandomizedSet() 初始化 RandomizedSet 对象
bool insert(int val) 当元素 val 不存在时,向集合中插入该项,并返回 true ;否则,返回 false 。
bool remove(int val) 当元素 val 存在时,从集合中移除该项,并返回 true ;否则,返回 false 。
int getRandom() 随机返回现有集合中的一项(测试用例保证调用此方法时集合中至少存在一个元素)。每个元素应该有 相同的概率 被返回。
你必须实现类的所有函数,并满足每个函数的 平均 时间复杂度为 O(1) 。



示例:

输入
["RandomizedSet", "insert", "remove", "insert", "getRandom", "remove", "insert", "getRandom"]
[[], [1], [2], [2], [], [1], [2], []]
输出
[null, true, false, true, 2, true, false, 2]

解释
RandomizedSet randomizedSet = new RandomizedSet();
randomizedSet.insert(1); // 向集合中插入 1 。返回 true 表示 1 被成功地插入。
randomizedSet.remove(2); // 返回 false ,表示集合中不存在 2 。
randomizedSet.insert(2); // 向集合中插入 2 。返回 true 。集合现在包含 [1,2] 。
randomizedSet.getRandom(); // getRandom 应随机返回 1 或 2 。
randomizedSet.remove(1); // 从集合中移除 1 ,返回 true 。集合现在包含 [2] 。
randomizedSet.insert(2); // 2 已在集合中,所以返回 false 。
randomizedSet.getRandom(); // 由于 2 是集合中唯一的数字,getRandom 总是返回 2 。


提示:

-231 <= val <= 231 - 1
最多调用 insert、remove 和 getRandom 函数 2 * 105 次
在调用 getRandom 方法时,数据结构中 至少存在一个 元素。*/
public class InsertDeleteGetrandomO1380 {

public static void main(String[] args) {
RandomizedSet randomizedSet = new RandomizedSet();
System.out.println(randomizedSet.insert(1)); // true
System.out.println(randomizedSet.remove(2)); // false
System.out.println(randomizedSet.insert(2)); // true
System.out.println(randomizedSet.getRandom()); // Random, either 1 or 2
System.out.println(randomizedSet.remove(1)); // true
System.out.println(randomizedSet.insert(2)); // false
System.out.println(randomizedSet.getRandom()); // Always 2
}
}

public class RandomizedSet {
private List<Integer> nums;
private Map<Integer, Integer> indexMap;
private Random random;

public RandomizedSet() {
nums = new ArrayList<>();
indexMap = new HashMap<>();
random = new Random();
}

public boolean insert(int val) {
// 当元素 val 不存在时,向集合中插入该项,并返回 true ;否则,返回 false
if (indexMap.containsKey(val)) return false;
nums.add(val);
// Map的值存放val对应数组的下标
indexMap.put(val, nums.size() - 1);
return true;
}

public boolean remove(int val) {
// 当元素 val 存在时,从集合中移除该项,并返回 true ;否则,返回 false 。
if (!indexMap.containsKey(val)) return false;
// 获取Map中val的值,对应数组中元素的下标
int index = indexMap.get(val);
// 获取数组末尾元素
int lastNum = nums.get(nums.size() - 1);
// 将末尾元素分别替换val在数组和Map中的位置
nums.set(index, lastNum);
indexMap.put(lastNum, index);
// 分别删除数组和Map的末尾
nums.remove(nums.size() - 1);
indexMap.remove(val);
return true;
}

public int getRandom() {
return nums.get(random.nextInt(nums.size()));
}
}

238. 除自身以外数组的乘积 <medium> 额外数组

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
95
96
97
98
99
100
/*给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。

题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。

请 不要使用除法,且在 O(n) 时间复杂度内完成此题。



示例 1:

输入: nums = [1,2,3,4]
输出: [24,12,8,6]
示例 2:

输入: nums = [-1,1,0,-3,3]
输出: [0,0,9,0,0]


提示:

2 <= nums.length <= 105
-30 <= nums[i] <= 30
保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内


进阶:你可以在 O(1) 的额外空间复杂度内完成这个题目吗?( 出于对空间复杂度分析的目的,输出数组 不被视为 额外空间。)*/
public class ProductOfArrayExceptSelf238 {
public static int[] productExceptSelf(int[] nums) {
// 1.两层遍历
//int[] answer = new int[nums.length];
//for (int i = 0;i < nums.length;i++) {
// answer[i] = 0;
// boolean ifSet = false;
// for (int j = 0;j < nums.length;j++) {
// if (i != j) {
// if (!ifSet) {
// answer[i] = nums[j];
// ifSet = true;
// } else {
// answer[i] *= nums[j];
// }
// }
// }
//}
// O(n^2)
//return answer;
// 2.使用2个额外数组,分别记录元素左侧和右侧的乘机,最后将两侧乘机相乘
//int n = nums.length;
//int[] leftProducts = new int[n];
//int[] rightProducts = new int[n];
//int[] answer = new int[n];
//
//// 首次遍历,记录每位元素左边元素的乘机
//leftProducts[0] = 1;
//for (int i = 1; i < n; i++) {
// leftProducts[i] = leftProducts[i - 1] * nums[i - 1];
//}
//
//// 二次遍历,记录每位元素右边元素的乘机
//rightProducts[n - 1] = 1;
//for (int i = n - 2; i >= 0; i--) {
// rightProducts[i] = rightProducts[i + 1] * nums[i + 1];
//}
//
//// 最后一次,将左右边相乘
//for (int i = 0; i < n; i++) {
// answer[i] = leftProducts[i] * rightProducts[i];
//}
//// 时间空间都为 O(n)
//return answer;
// 3.将nums和answer分别作为两个数组记录左右乘机,第二次遍历时即算出最终结果
int n = nums.length;
int[] answer = new int[n];

answer[0] = 1;
for (int i = 1; i < n; i++) {
answer[i] = answer[i - 1] * nums[i - 1];
}

int rightProduct = 1;
for (int i = n - 1; i >= 0; i--) {
answer[i] *= rightProduct;
rightProduct *= nums[i];
}

// 时间O(n) 空间O(1)
return answer;
}

public static void main(String[] args) {
int[] nums1 = {1, 2, 3, 4};
System.out.println(Arrays.toString(productExceptSelf(nums1))); // [24, 12, 8, 6]

int[] nums2 = {-1, 1, 0, -3, 3};
System.out.println(Arrays.toString(productExceptSelf(nums2))); // [0, 0, 9, 0, 0]

int[] nums3 = {4, 3, 2, 1, 2};
System.out.println(Arrays.toString(productExceptSelf(nums3))); // [12,16,24,48,24]
}
}

134. 加油站 <medium>

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
/*在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。

你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。

给定两个整数数组 gas 和 cost ,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。



示例 1:

输入: gas = [1,2,3,4,5], cost = [3,4,5,1,2]
输出: 3
解释:
从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油
开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油
开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油
开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油
开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油
开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。
因此,3 可为起始索引。
示例 2:

输入: gas = [2,3,4], cost = [3,4,3]
输出: -1
解释:
你不能从 0 号或 1 号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站。
我们从 2 号加油站出发,可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油
开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油
开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油
你无法返回 2 号加油站,因为返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。
因此,无论怎样,你都不可能绕环路行驶一周。


提示:

gas.length == n
cost.length == n
1 <= n <= 10^5
0 <= gas[i], cost[i] <= 10^4*/
public class GasStation134 {
public static int canCompleteCircuit(int[] gas, int[] cost) {
// 贪心算法
int totalGas = 0;
int currentGas = 0;
int start = 0;

for (int i = 0; i < gas.length; i++) {
totalGas += gas[i] - cost[i];
currentGas += gas[i] - cost[i];

// 油不足,跳到下个站点开始
if (currentGas < 0) {
currentGas = 0;
start = i + 1;
}
}

// 总油的正负表示是否能完成全程
// O(n) O(1)
return totalGas < 0 ? -1 : start;
}

public static void main(String[] args) {
int[] gas1 = {1, 2, 3, 4, 5};
int[] cost1 = {3, 4, 5, 1, 2};
int[] gas2 = {2, 3, 4};
int[] cost2 = {3, 4, 3};

testGasStation(gas1, cost1); // Example 1
testGasStation(gas2, cost2); // Example 2
}

public static void testGasStation(int[] gas, int[] cost) {
int result = canCompleteCircuit(gas, cost);
System.out.println("Gas: " + Arrays.toString(gas) + ", Cost: " + Arrays.toString(cost));
System.out.println("Output: " + result);
}
}

135. 分发糖果 <hard>

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
/*n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。

你需要按照以下要求,给这些孩子分发糖果:

每个孩子至少分配到 1 个糖果。
相邻两个孩子评分更高的孩子会获得更多的糖果。
请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。



示例 1:

输入:ratings = [1,0,2]
输出:5
解释:你可以分别给第一个、第二个、第三个孩子分发 2、1、2 颗糖果。
示例 2:

输入:ratings = [1,2,2]
输出:4
解释:你可以分别给第一个、第二个、第三个孩子分发 1、2、1 颗糖果。
第三个孩子只得到 1 颗糖果,这满足题面中的两个条件。


提示:

n == ratings.length
1 <= n <= 2 * 10^4
0 <= ratings[i] <= 2 * 10^4*/
public class Candy135 {
public static int candy(int[] ratings) {
int n = ratings.length;
int[] candies = new int[n];
Arrays.fill(candies, 1); // 每个孩子初始至少一个糖果

// 从左到右扫描,确保右边评分高的孩子糖果数比左边多
for (int i = 1; i < n; i++) {
if (ratings[i] > ratings[i - 1]) {
candies[i] = candies[i - 1] + 1;
}
}

// 从右到左扫描,确保左边评分高的孩子糖果数比右边多
for (int i = n - 2; i >= 0; i--) {
if (ratings[i] > ratings[i + 1]) {
candies[i] = Math.max(candies[i], candies[i + 1] + 1);
}
}

int sum = 0;
for (int candy : candies) {
sum += candy;
}

// O(n) O(n)
return sum;
}

public static void main(String[] args) {
int[] ratings1 = {1, 0, 2};
System.out.println("需要准备的最少糖果数目:" + candy(ratings1)); // 输出:5

int[] ratings2 = {1, 2, 2};
System.out.println("需要准备的最少糖果数目:" + candy(ratings2)); // 输出:4
}
}

42. 接雨水 <hard> 双指针

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
/*给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。


示例 1:

输入: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 个单位的雨水(蓝色部分表示雨水)。
示例 2:

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


提示:

n == height.length
1 <= n <= 2 * 104
0 <= height[i] <= 105*/
public class TrappingRainWater42 {

public static int trap(int[] height) {
// 使用栈或双指针的方式来解决
if (height == null || height.length == 0) {
return 0;
}

// 两个指针 left 和 right 分别位于数组的最左端和最右端
int left = 0, right = height.length - 1;
// 定义了两个变量 leftMax 和 rightMax 来分别记录左右两侧柱子的最大高度
int leftMax = 0, rightMax = 0;
int totalWater = 0;

// 利用 while 循环不断更新 left 和 right 指针位置的高度,并计算蓄水量。随着指针移动,最终可以得到能接的雨水总量。
while (left < right) {
leftMax = Math.max(leftMax, height[left]);
rightMax = Math.max(rightMax, height[right]);

// 哪边低移动哪边指针
if (leftMax < rightMax) {
// 蓄水量就是左边的最高减去当前高度
totalWater += leftMax - height[left];
left++;
} else {
// 蓄水量就是右边的最高减去当前高度
totalWater += rightMax - height[right];
right--;
}
}

//该方法的时间复杂度为 O(N),空间复杂度为 O(1),只使用了常量级别的额外空间。
return totalWater;
}

public static void main(String[] args) {
int[] height1 = {0,1,0,2,1,0,1,3,2,1,2,1};
int[] height2 = {4,2,0,3,2,5};

System.out.println("Test Case 1: " + trap(height1)); // Output: 6
System.out.println("Test Case 2: " + trap(height2)); // Output: 9
}
}

13. 罗马数字转整数 <easy>

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
/*罗马数字包含以下七种字符: I, V, X, L,C,D 和 M。

字符 数值
I 1
V 5
X 10
L 50
C 100
D 500
M 1000
例如, 罗马数字 2 写做 II ,即为两个并列的 1 。12 写做 XII ,即为 X + II 。 27 写做 XXVII, 即为 XX + V + II 。

通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII,而是 IV。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地,数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况:

I 可以放在 V (5) 和 X (10) 的左边,来表示 4 和 9。
X 可以放在 L (50) 和 C (100) 的左边,来表示 40 和 90。
C 可以放在 D (500) 和 M (1000) 的左边,来表示 400 和 900。
给定一个罗马数字,将其转换成整数。

示例 1:

输入: s = "III"
输出: 3
示例 2:

输入: s = "IV"
输出: 4
示例 3:

输入: s = "IX"
输出: 9
示例 4:

输入: s = "LVIII"
输出: 58
解释: L = 50, V= 5, III = 3.
示例 5:

输入: s = "MCMXCIV"
输出: 1994
解释: M = 1000, CM = 900, XC = 90, IV = 4.


提示:

1 <= s.length <= 15
s 仅含字符 ('I', 'V', 'X', 'L', 'C', 'D', 'M')
题目数据保证 s 是一个有效的罗马数字,且表示整数在范围 [1, 3999] 内
题目所给测试用例皆符合罗马数字书写规则,不会出现跨位等情况。
IL 和 IM 这样的例子并不符合题目要求,49 应该写作 XLIX,999 应该写作 CMXCIX 。
关于罗马数字的详尽书写规则,可以参考 罗马数字 - Mathematics 。*/
public class RomanToInteger13 {
public static int romanToInt(String s) {
if (s == null || s.length() == 0) {
return 0;
}

int result = 0;
for (int i = 0; i < s.length(); i++) {
int currentVal = getValue(s.charAt(i));

if (i < s.length() - 1) {
int nextVal = getValue(s.charAt(i + 1));
if (currentVal < nextVal) {
// 表示为特殊情况,需要减去当前的I、X、C
result -= currentVal;
} else {
// 普通情况,直接累加
result += currentVal;
}
} else {
result += currentVal;
}
}

// O(n)
return result;
}

private static int getValue(char c) {
switch (c) {
case 'I':
return 1;
case 'V':
return 5;
case 'X':
return 10;
case 'L':
return 50;
case 'C':
return 100;
case 'D':
return 500;
case 'M':
return 1000;
default:
return 0;
}
}

public static void main(String[] args) {
String s1 = "III";
String s2 = "IV";
String s3 = "IX";
String s4 = "LVIII";
String s5 = "MCMXCIV";

System.out.println("Test Case 1: " + romanToInt(s1)); // Output: 3
System.out.println("Test Case 2: " + romanToInt(s2)); // Output: 4
System.out.println("Test Case 3: " + romanToInt(s3)); // Output: 9
System.out.println("Test Case 4: " + romanToInt(s4)); // Output: 58
System.out.println("Test Case 5: " + romanToInt(s5)); // Output: 1994
}
}

12. 整数转罗马数字 <medium> 贪心

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
/*罗马数字包含以下七种字符: I, V, X, L,C,D 和 M。

字符 数值
I 1
V 5
X 10
L 50
C 100
D 500
M 1000
例如, 罗马数字 2 写做 II ,即为两个并列的 1。12 写做 XII ,即为 X + II 。 27 写做 XXVII, 即为 XX + V + II 。

通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII,而是 IV。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地,数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况:

I 可以放在 V (5) 和 X (10) 的左边,来表示 4 和 9。
X 可以放在 L (50) 和 C (100) 的左边,来表示 40 和 90。
C 可以放在 D (500) 和 M (1000) 的左边,来表示 400 和 900。
给你一个整数,将其转为罗马数字。



示例 1:

输入: num = 3
输出: "III"
示例 2:

输入: num = 4
输出: "IV"
示例 3:

输入: num = 9
输出: "IX"
示例 4:

输入: num = 58
输出: "LVIII"
解释: L = 50, V = 5, III = 3.
示例 5:

输入: num = 1994
输出: "MCMXCIV"
解释: M = 1000, CM = 900, XC = 90, IV = 4.


提示:

1 <= num <= 3999*/
public class IntegerToRoman12 {
public static String intToRoman(int num) {
// 通过一个映射表,将数字和罗马数字对应起来,然后从高位到低位按照规则逐步构造出对应的罗马数字
//TreeMap<Integer, String> romanMap = new TreeMap<>();
//romanMap.put(1, "I");
//romanMap.put(4, "IV");
//romanMap.put(5, "V");
//romanMap.put(9, "IX");
//romanMap.put(10, "X");
//romanMap.put(40, "XL");
//romanMap.put(50, "L");
//romanMap.put(90, "XC");
//romanMap.put(100, "C");
//romanMap.put(400, "CD");
//romanMap.put(500, "D");
//romanMap.put(900, "CM");
//romanMap.put(1000, "M");
//
//int[] values = {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1};
//
//StringBuilder result = new StringBuilder();
//for (int value : values) {
// while (num >= value) {
// result.append(romanMap.get(value));
// num -= value;
// }
//}
//
//// O(1)
//return result.toString();

// 可以采用贪心算法的思路,使用两个数组来存储罗马数字和对应的整数值,然后在循环中不断减去最大的值,将对应的罗马数字拼接起来。
String[] romanSymbols = {"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"};
int[] values = {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1};

StringBuilder result = new StringBuilder();
int i = 0;

while (num > 0) {
while (num >= values[i]) {
result.append(romanSymbols[i]);
num -= values[i];
}
i++;
}

return result.toString();
}

public static void main(String[] args) {
int num1 = 3;
int num2 = 4;
int num3 = 9;
int num4 = 58;
int num5 = 1994;

System.out.println("Test Case 1: " + intToRoman(num1)); // Output: "III"
System.out.println("Test Case 2: " + intToRoman(num2)); // Output: "IV"
System.out.println("Test Case 3: " + intToRoman(num3)); // Output: "IX"
System.out.println("Test Case 4: " + intToRoman(num4)); // Output: "LVIII"
System.out.println("Test Case 5: " + intToRoman(num5)); // Output: "MCMXCIV"
}
}

58. 最后一个单词的长度 <easy>

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
/*给你一个字符串 s,由若干单词组成,单词前后用一些空格字符隔开。返回字符串中 最后一个 单词的长度。

单词 是指仅由字母组成、不包含任何空格字符的最大子字符串。



示例 1:

输入:s = "Hello World"
输出:5
解释:最后一个单词是“World”,长度为5。
示例 2:

输入:s = " fly me to the moon "
输出:4
解释:最后一个单词是“moon”,长度为4。
示例 3:

输入:s = "luffy is still joyboy"
输出:6
解释:最后一个单词是长度为6的“joyboy”。


提示:

1 <= s.length <= 104
s 仅有英文字母和空格 ' ' 组成
s 中至少存在一个单词*/
public class LengthOfLastWord58 {
public static int lengthOfLastWord(String s) {
int length = 0;
for (int i = s.length() - 1;i > - 1;i--) {
if (' ' != s.charAt(i)) {
length++;
} else if (length > 0) {
return length;
}
}
return length;
}
}

14. 最长公共前缀 <easy>

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
/*编写一个函数来查找字符串数组中的最长公共前缀。

如果不存在公共前缀,返回空字符串 ""。



示例 1:

输入:strs = ["flower","flow","flight"]
输出:"fl"
示例 2:

输入:strs = ["dog","racecar","car"]
输出:""
解释:输入不存在公共前缀。


提示:

1 <= strs.length <= 200
0 <= strs[i].length <= 200
strs[i] 仅由小写英文字母组成*/
public class LongestCommonPrefix14 {
public static String longestCommonPrefix(String[] strs) {
// 可以采用横向比较的方法
if (strs == null || strs.length == 0) {
return "";
}

// 第一个字符串设为最长公共前缀,然后逐个与后面的字符串进行比较
String prefix = strs[0];
for (int i = 1; i < strs.length; i++) {
while (strs[i].indexOf(prefix) != 0) {
prefix = prefix.substring(0, prefix.length() - 1);
if (prefix.isEmpty()) {
return "";
}
}
}

// 这个算法的时间复杂度是 O(nm),其中 n 是字符串数组的长度,m 是字符串数组中的字符串的平均长度
return prefix;
}

public static void main(String[] args) {
String[] test1 = {"flower", "flow", "flight"};
String[] test2 = {"dog", "racecar", "car"};

System.out.println("Test Case 1: " + longestCommonPrefix(test1)); // Output: "fl"
System.out.println("Test Case 2: " + longestCommonPrefix(test2)); // Output: ""
}
}

151. 反转字符串中的单词 <medium>

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
/*给你一个字符串 s ,请你反转字符串中 单词 的顺序。

单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。

返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。

注意:输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。



示例 1:

输入:s = "the sky is blue"
输出:"blue is sky the"
示例 2:

输入:s = " hello world "
输出:"world hello"
解释:反转后的字符串中不能存在前导空格和尾随空格。
示例 3:

输入:s = "a good example"
输出:"example good a"
解释:如果两个单词间有多余的空格,反转后的字符串需要将单词间的空格减少到仅有一个。


提示:

1 <= s.length <= 104
s 包含英文大小写字母、数字和空格 ' '
s 中 至少存在一个 单词

进阶:如果字符串在你使用的编程语言中是一种可变数据类型,请尝试使用 O(1) 额外空间复杂度的 原地 解法。*/
public class ReverseWordsInAString151 {
public static String reverseWords(String s) {
//// 去除字符串首尾空格,并用空格分割成单词数组
//String[] words = s.trim().split("\\s+");
//StringBuilder result = new StringBuilder();
//
//// 从后往前拼接单词
//for (int i = words.length - 1; i >= 0; i--) {
// result.append(words[i]).append(" ");
//}
//
//// 删除最后一个多余的空格
//return result.toString().trim();

if (s == null || s.length() == 0) {
return "";
}

char[] str = s.toCharArray();
int start = s.length() - 1;
int end = s.length();

StringBuilder result = new StringBuilder();

// 以从字符串末尾开始遍历,找到每个单词的起始和结束位置,然后将其拼接到结果字符串上
while (start >= 0) {
if (str[start] == ' ') {
end = start;
} else if (start == 0 || str[start - 1] == ' ') {
if (result.length() != 0) {
result.append(" ");
}
result.append(s, start, end);
}
start--;
}

return result.toString();
}

public static void main(String[] args) {
String test1 = "the sky is blue";
String test2 = " hello world ";
String test3 = "a good example";

System.out.println("Test Case 1: " + reverseWords(test1)); // Output: "blue is sky the"
System.out.println("Test Case 2: " + reverseWords(test2)); // Output: "world hello"
System.out.println("Test Case 3: " + reverseWords(test3)); // Output: "example good a"
}
}

6. N 字形变换 <medium>

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
/*将一个给定字符串 s 根据给定的行数 numRows ,以从上往下、从左到右进行 Z 字形排列。

比如输入字符串为 "PAYPALISHIRING" 行数为 3 时,排列如下:

P A H N
A P L S I I G
Y I R
之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:"PAHNAPLSIIGYIR"。

请你实现这个将字符串进行指定行数变换的函数:

string convert(string s, int numRows);


示例 1:

输入:s = "PAYPALISHIRING", numRows = 3
输出:"PAHNAPLSIIGYIR"
示例 2:
输入:s = "PAYPALISHIRING", numRows = 4
输出:"PINALSIGYAHRPI"
解释:
P I N
A L S I G
Y A H R
P I
示例 3:

输入:s = "A", numRows = 1
输出:"A"


提示:

1 <= s.length <= 1000
s 由英文字母(小写和大写)、',' 和 '.' 组成
1 <= numRows <= 1000*/
public class ZigzagConversion6 {
public static String convert(String s, int numRows) {
if (numRows == 1 || s.length() <= numRows) {
return s;
}

// 定义 numRows 个字符串来存储 Z 字形中的每一行
StringBuilder[] rows = new StringBuilder[numRows];
for (int i = 0; i < numRows; i++) {
rows[i] = new StringBuilder();
}

int row = 0;
boolean goingDown = false;

// 然后遍历输入字符串,根据当前字符应该位于 Z 字形的哪一行,将字符添加到对应的行中
for (char c : s.toCharArray()) {
// 将当前 row 对应字符 c 添加到字符串构建器中
rows[row].append(c);
// 判断当前字符是否位于 Z 字形的顶部或底部。若是,则需要改变遍历方向,即从上往下或从下往上
if (row == 0 || row == numRows - 1) {
goingDown = !goingDown;
}
// 根据 goingDown 的值,确定当前字符在 Z 字形中下一个位置的行数,若 goingDown 为 true,则向下移动一行;否则向上移动一行
row += goingDown ? 1 : -1;
}

// 最后按照行的顺序将这些行连接起来,就是最终的结果。
StringBuilder result = new StringBuilder();
for (StringBuilder stringBuilder : rows) {
result.append(stringBuilder);
}

// O(n)
return result.toString();
}

public static void main(String[] args) {
String test1 = "PAYPALISHIRING";
String test2 = "PAYPALISHIRING";
String test3 = "A";

System.out.println("Test Case 1: " + convert(test1, 3)); // Output: "PAHNAPLSIIGYIR"
System.out.println("Test Case 2: " + convert(test2, 4)); // Output: "PINALSIGYAHRPI"
System.out.println("Test Case 3: " + convert(test3, 1)); // Output: "A"
}
}

28. 找出字符串中第一个匹配项的下标 <easy>

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
/*给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回  -1 。

示例 1:

输入:haystack = "sadbutsad", needle = "sad"
输出:0
解释:"sad" 在下标 0 和 6 处匹配。
第一个匹配项的下标是 0 ,所以返回 0 。
示例 2:

输入:haystack = "leetcode", needle = "leeto"
输出:-1
解释:"leeto" 没有在 "leetcode" 中出现,所以返回 -1 。


提示:

1 <= haystack.length, needle.length <= 104
haystack 和 needle 仅由小写英文字符组成*/
public class FindTheIndexOfTheFirstOccurrenceInAString28 {

public static int strStr(String haystack, String needle) {
int hayLength = haystack.length();
int needleLength = needle.length();
if (needleLength == 0) return 0;
// 遍历 haystack 字符串并查找 needle 字符串
for (int i = 0; i <= hayLength - needleLength; i++) {
if (haystack.charAt(i) == needle.charAt(0)) {
int j = 0;
while (j < needleLength && haystack.charAt(i + j) == needle.charAt(j)) {
j++;
}
if (j == needleLength) {
return i;
}
}
}
// O(n⋅m) O(1)
return -1;
}

public static void main(String[] args) {
String haystack1 = "sadbutsad";
String needle1 = "sad";
System.out.println(strStr(haystack1, needle1)); // 输出: 0

String haystack2 = "leetcode";
String needle2 = "leeto";
System.out.println(strStr(haystack2, needle2)); // 输出: -1
}
}

68. 文本左右对齐 <hard> 贪心

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
/*给定一个单词数组 words 和一个长度 maxWidth ,重新排版单词,使其成为每行恰好有 maxWidth 个字符,且左右两端对齐的文本。

你应该使用 “贪心算法” 来放置给定的单词;也就是说,尽可能多地往每行中放置单词。必要时可用空格 ' ' 填充,使得每行恰好有 maxWidth 个字符。

要求尽可能均匀分配单词间的空格数量。如果某一行单词间的空格不能均匀分配,则左侧放置的空格数要多于右侧的空格数。

文本的最后一行应为左对齐,且单词之间不插入额外的空格。

注意:

单词是指由非空格字符组成的字符序列。
每个单词的长度大于 0,小于等于 maxWidth。
输入单词数组 words 至少包含一个单词。

示例 1:

输入: words = ["This", "is", "an", "example", "of", "text", "justification."], maxWidth = 16
输出:
[
"This is an",
"example of text",
"justification. "
]
示例 2:

输入:words = ["What","must","be","acknowledgment","shall","be"], maxWidth = 16
输出:
[
"What must be",
"acknowledgment ",
"shall be "
]
解释: 注意最后一行的格式应为 "shall be " 而不是 "shall be",
因为最后一行应为左对齐,而不是左右两端对齐。
第二行同样为左对齐,这是因为这行只包含一个单词。
示例 3:

输入:words = ["Science","is","what","we","understand","well","enough","to","explain","to","a","computer.","Art","is","everything","else","we","do"],maxWidth = 20
输出:
[
"Science is what we",
"understand well",
"enough to explain to",
"a computer. Art is",
"everything else we",
"do "
]


提示:

1 <= words.length <= 300
1 <= words[i].length <= 20
words[i] 由小写英文字母和符号组成
1 <= maxWidth <= 100
words[i].length <= maxWidth*/
public class TextJustification68 {

public static List<String> fullJustify(String[] words, int maxWidth) {
// 贪心算法
List<String> result = new ArrayList<>();
// 单词索引
int index = 0;

// 遍历单词列表
while (index < words.length) {
// 当前行长度
int lineLength = words[index].length();
// 下一个单词索引
int lastWordIndex = index + 1;

// 每行中尽可能多地放置单词,且不超过最大宽度
while (lastWordIndex < words.length && lineLength + 1 + words[lastWordIndex].length() <= maxWidth) {
// 更新当前行长度
lineLength += 1 + words[lastWordIndex].length();
// 移至下一个单词
lastWordIndex++;
}

// 使用StringBuilder构建当前行
StringBuilder builder = new StringBuilder();
// 当前行单词间隔数
int diff = lastWordIndex - index - 1;

if (lastWordIndex == words.length || diff == 0) {
// 最后一行或只有一个单词的行,用空格补充
for (int i = index; i < lastWordIndex; i++) {
builder.append(words[i]).append(" ");
}
builder.deleteCharAt(builder.length() - 1);
while (builder.length() < maxWidth) {
builder.append(" ");
}
} else {
// 平均空格数
int spaces = (maxWidth - lineLength) / diff;
// 额外的空格
int extraSpaces = (maxWidth - lineLength) % diff;

// 在每个单词之间均匀分配空格
for (int i = index; i < lastWordIndex - 1; i++) {
builder.append(words[i]);
for (int j = 0; j <= spaces + (i - index < extraSpaces ? 1 : 0); j++) {
builder.append(" ");
}
}
// 添加最后一个单词
builder.append(words[lastWordIndex - 1]);
}

result.add(builder.toString());
// 更新单词索引为下一个行的单词索引
index = lastWordIndex;
}

return result;
}

public static void main(String[] args) {
String[] words1 = {"This", "is", "an", "example", "of", "text", "justification."};
int maxWidth1 = 16;
System.out.println(fullJustify(words1, maxWidth1));

String[] words2 = {"What", "must", "be", "acknowledgment", "shall", "be"};
int maxWidth2 = 16;
System.out.println(fullJustify(words2, maxWidth2));

String[] words3 = {"Science", "is", "what", "we", "understand", "well", "enough", "to", "explain", "to", "a", "computer.", "Art", "is", "everything", "else", "we", "do"};
int maxWidth3 = 20;
System.out.println(fullJustify(words3, maxWidth3));
}
}

3.2 双指针 Two Pointers

使用两个指针在一次遍历中解决问题

  1. 对撞指针:从数组两端移动指针,寻找满足条件的元素。如找到两数之和、反转链表、回文判断等
  2. 快慢指针:用两个不同速度的指针遍历,通常用于检测环形链表、寻找中间节点等

125. 验证回文串 <easy>

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
/*如果在将所有大写字符转换为小写字符、并移除所有非字母数字字符之后,短语正着读和反着读都一样。则可以认为该短语是一个 回文串 。

字母和数字都属于字母数字字符。

给你一个字符串 s,如果它是 回文串 ,返回 true ;否则,返回 false 。

示例 1:

输入: s = "A man, a plan, a canal: Panama"
输出:true
解释:"amanaplanacanalpanama" 是回文串。
示例 2:

输入:s = "race a car"
输出:false
解释:"raceacar" 不是回文串。
示例 3:

输入:s = " "
输出:true
解释:在移除非字母数字字符之后,s 是一个空字符串 "" 。
由于空字符串正着反着读都一样,所以是回文串。


提示:

1 <= s.length <= 2 * 105
s 仅由可打印的 ASCII 字符组成*/
public class ValidPalindrome125 {
public static boolean isPalindrome(String s) {
if (s.isEmpty()) return true;
// 双指针
int left = 0, right = s.length() - 1;
while (left < right) {
// 非字母和数字,直接跳过
while (left < right && !Character.isLetterOrDigit(s.charAt(left))) {
left++;
}
while (left < right && !Character.isLetterOrDigit(s.charAt(right))) {
right--;
}
// 左右指针不相同,直接判定不符
if (Character.toLowerCase(s.charAt(left)) != Character.toLowerCase(s.charAt(right))) {
return false;
}
left++;
right--;
}
// 遍历全部都相等
// O(n)
return true;
}

public static void main(String[] args) {
String s1 = "A man, a plan, a canal: Panama";
System.out.println(isPalindrome(s1)); // Output: true

String s2 = "race a car";
System.out.println(isPalindrome(s2)); // Output: false

String s3 = " ";
System.out.println(isPalindrome(s3)); // Output: true
}
}

392. 判断子序列 <easy>

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
/*给定字符串 s 和 t ,判断 s 是否为 t 的子序列。

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。

进阶:

如果有大量输入的 S,称作 S1, S2, ... , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?

致谢:

特别感谢 @pbrother 添加此问题并且创建所有测试用例。



示例 1:

输入:s = "abc", t = "ahbgdc"
输出:true
示例 2:

输入:s = "axc", t = "ahbgdc"
输出:false


提示:

0 <= s.length <= 100
0 <= t.length <= 10^4
两个字符串都只由小写字符组成。*/
public class IsSubsequence392 {
public static boolean isSubsequence(String s, String t) {
int sPointer = 0, tPointer = 0;

// 循环直到某一个字符串遍历结束
while (sPointer < s.length() && tPointer < t.length()) {
// 如果两个字符相等,移动 s 的指针
if (s.charAt(sPointer) == t.charAt(tPointer)) {
sPointer++;
}
// 不管相等与否,移动 t 的指针
tPointer++;
}

// 如果 s 遍历结束,说明 s 是 t 的子序列
// O(n)
return sPointer == s.length();
}

public static void main(String[] args) {
String s1 = "abc", t1 = "ahbgdc";
System.out.println(isSubsequence(s1, t1)); // Output: true

String s2 = "axc", t2 = "ahbgdc";
System.out.println(isSubsequence(s2, t2)); // Output: false
}
}

167. 两数之和 II - 输入有序数组 <medium>

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
/*给你一个下标从 1 开始的整数数组 numbers ,该数组已按 非递减顺序排列  ,请你从数组中找出满足相加之和等于目标数 target 的两个数。如果设这两个数分别是 numbers[index1] 和 numbers[index2] ,则 1 <= index1 < index2 <= numbers.length 。

以长度为 2 的整数数组 [index1, index2] 的形式返回这两个整数的下标 index1 和 index2。

你可以假设每个输入 只对应唯一的答案 ,而且你 不可以 重复使用相同的元素。

你所设计的解决方案必须只使用常量级的额外空间。


示例 1:

输入:numbers = [2,7,11,15], target = 9
输出:[1,2]
解释:2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。返回 [1, 2] 。
示例 2:

输入:numbers = [2,3,4], target = 6
输出:[1,3]
解释:2 与 4 之和等于目标数 6 。因此 index1 = 1, index2 = 3 。返回 [1, 3] 。
示例 3:

输入:numbers = [-1,0], target = -1
输出:[1,2]
解释:-1 与 0 之和等于目标数 -1 。因此 index1 = 1, index2 = 2 。返回 [1, 2] 。


提示:

2 <= numbers.length <= 3 * 104
-1000 <= numbers[i] <= 1000
numbers 按 非递减顺序 排列
-1000 <= target <= 1000
仅存在一个有效答案*/
public class TwoSumIIInputArrayIsSorted167 {
public static int[] twoSum(int[] numbers, int target) {
int left = 0, right = numbers.length - 1;

while (left < right) {
int sum = numbers[left] + numbers[right];

if (sum == target) {
// 题目要求索引从 1 开始,所以加 1 返回
return new int[]{left + 1, right + 1};
} else if (sum < target) {
// 如果和小于目标值,增加左指针以增加和
left++;
} else {
// 如果和大于目标值,减少右指针以减少和
right--;
}
}

// 找不到符合条件的数对
// O(n)
return new int[]{-1, -1};
}

public static void main(String[] args) {
int[] numbers1 = {2, 7, 11, 15};
int target1 = 9;
int[] result1 = twoSum(numbers1, target1);
System.out.println("Result 1: [" + result1[0] + ", " + result1[1] + "]");

int[] numbers2 = {2, 3, 4};
int target2 = 6;
int[] result2 = twoSum(numbers2, target2);
System.out.println("Result 2: [" + result2[0] + ", " + result2[1] + "]");

int[] numbers3 = {-1, 0};
int target3 = -1;
int[] result3 = twoSum(numbers3, target3);
System.out.println("Result 3: [" + result3[0] + ", " + result3[1] + "]");
}
}

11. 盛最多水的容器 <medium>

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
/*给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。

找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

返回容器可以储存的最大水量。

说明:你不能倾斜容器。

示例 1:


输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
示例 2:

输入:height = [1,1]
输出:1

提示:

n == height.length
2 <= n <= 105
0 <= height[i] <= 104*/
public class ContainerWithMostWater11 {

public static int maxArea(int[] height) {
int maxArea = 0;
int left = 0, right = height.length - 1;

while (left < right) {
int currentArea = Math.min(height[left], height[right]) * (right - left);
// 记录历史最大面积
maxArea = Math.max(maxArea, currentArea);

// 哪边低移动哪边
if (height[left] < height[right]) {
left++;
} else {
right--;
}
}

// O(n)
return maxArea;
}

public static void main(String[] args) {
int[] heights1 = {1, 8, 6, 2, 5, 4, 8, 3, 7};
System.out.println("Max area for heights1: " + maxArea(heights1));

int[] heights2 = {1, 1};
System.out.println("Max area for heights2: " + maxArea(heights2));

int[] heights3 = {1, 2, 1};
System.out.println("Max area for heights3: " + maxArea(heights3));
}

}

15. 三数之和 <medium>

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
/*给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请

你返回所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复的三元组。


示例 1:

输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。
示例 2:

输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。
示例 3:

输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。


提示:

3 <= nums.length <= 3000
-105 <= nums[i] <= 105*/
public class ThreeSum15 {

public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
// 先排序
Arrays.sort(nums);

// 外层遍历固定一个数
for (int i = 0; i < nums.length - 2; i++) {
// 跳过重复元素
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}

// 里层遍历,用双指针找另外两个数
int left = i + 1;
int right = nums.length - 1;

while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum == 0) {
// 命中
result.add(Arrays.asList(nums[i], nums[left], nums[right]));

// 跳过重复元素
while (left < right && nums[left] == nums[left + 1]) {
left++;
}
while (left < right && nums[right] == nums[right - 1]) {
right--;
}

left++;
right--;
} else if (sum < 0) {
// 和小于0,则往右移
left++;
} else {
right--;
}
}
}
// O(n^2),空间复杂度为 O(log n) 到 O(n)

return result;
}

public static void main(String[] args) {
ThreeSum15 solution = new ThreeSum15();
int[] nums1 = {-1, 0, 1, 2, -1, -4};
System.out.println(solution.threeSum(nums1)); // Output: [[-1, -1, 2], [-1, 0, 1]]

int[] nums2 = {0, 1, 1};
System.out.println(solution.threeSum(nums2)); // Output: []

int[] nums3 = {0, 0, 0};
System.out.println(solution.threeSum(nums3)); // Output: [[0, 0, 0]]
}
}

3.3 滑动窗口 Sliding Window

(双指针变形)使用两个指针定义一个窗口,通过移动右指针扩大窗口,移动左指针缩小窗口

  1. 找到满足条件的数组(字符串)的最大/最小子数组(子串)
  2. 给定数组移动固定大小窗口,查找其中的固定值

209. 长度最小的子数组 <medium>

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
95
96
97
98
99
100
101
/*给定一个含有 n 个正整数的数组和一个正整数 target 。

找出该数组中满足其总和大于等于 target 的长度最小的 连续子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。

示例 1:

输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
示例 2:

输入:target = 4, nums = [1,4,4]
输出:1
示例 3:

输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0


提示:

1 <= target <= 10^9
1 <= nums.length <= 10^5
1 <= nums[i] <= 10^5

进阶:

如果你已经实现 O(n) 时间复杂度的解法, 请尝试设计一个 O(n log(n)) 时间复杂度的解法。*/
public class MinimumSizeSubarraySum209 {

public int minSubArrayLen(int target, int[] nums) {
// 双指针,left和right分别表示滑动窗口的边界
//int left = 0;
//int sum = 0;
//int minLength = Integer.MAX_VALUE;
//
//for (int right = 0; right < nums.length; right++) {
// // 先连续相加,直到窗口内的总和大于等于目标值
// sum += nums[right];
//
// // 然后记录窗口的大小,并缩小窗口
// while (sum >= target) {
// minLength = Math.min(minLength, right - left + 1);
// // 减去左边界的元素看看是否仍然符合要求
// sum -= nums[left];
// left++;
// }
// // sum减到不再大于目标值时,再继续向右移动右边界
//}
//
//// O(n)
//return minLength == Integer.MAX_VALUE ? 0 : minLength;

// 2.使用二分查找和前缀和
int n = nums.length;
if (n == 0) return 0;

// 维护一个前缀和数组
int[] prefixSum = new int[n + 1];
// 遍历一次数组,每个元素都是前面元素的和
for (int i = 1; i <= n; i++) {
prefixSum[i] = prefixSum[i - 1] + nums[i - 1];
}

int minLen = Integer.MAX_VALUE;
// 遍历前缀和数组
for (int i = 1; i <= n; i++) {
int left = 0, right = i - 1;

// 通过二分查找在前面已计算的前缀和中寻找一个位置,使得两个前缀和之差满足条件
while (left <= right) {
int mid = left + (right - left) / 2;
int sum = prefixSum[i] - prefixSum[mid];

if (sum >= target) {
minLen = Math.min(minLen, i - mid);
left = mid + 1;
} else {
right = mid - 1;
}
}
}

// O(n log(n))
return minLen == Integer.MAX_VALUE ? 0 : minLen;
}

public static void main(String[] args) {
MinimumSizeSubarraySum209 solution = new MinimumSizeSubarraySum209();
int[] nums1 = {2, 3, 1, 2, 4, 3};
int[] nums2 = {1, 4, 4};
int[] nums3 = {1, 1, 1, 1, 1, 1, 1, 1};
int target1 = 7;
int target2 = 4;
int target3 = 11;

System.out.println(solution.minSubArrayLen(target1, nums1)); // Output: 2
System.out.println(solution.minSubArrayLen(target2, nums2)); // Output: 1
System.out.println(solution.minSubArrayLen(target3, nums3)); // Output: 0
}
}

3. 无重复字符的最长子串 <medium>

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
/*给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

示例 1:

输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:

输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:

输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。


提示:

0 <= s.length <= 5 * 104
s 由英文字母、数字、符号和空格组成*/
public class LongestSubstringWithoutRepeatingCharacters3 {

public static int lengthOfLongestSubstring(String s) {
int n = s.length();
// 滑动窗口左右边界
int left = 0, right = 0;
int maxLen = 0;
// 通过哈希集合来存放窗口内的元素
HashSet<Character> set = new HashSet<>();

while (right < n) {
if (!set.contains(s.charAt(right))) {
// 右指针右移,将元素加入set
set.add(s.charAt(right++));
// 更新最大长度
maxLen = Math.max(maxLen, set.size());
} else {
// 右指针指到重复元素,尝试移除左指针元素直到清除干净重复元素
set.remove(s.charAt(left++));
}
}
// O(n)
return maxLen;
}

public static void main(String[] args) {
String s1 = "abcabcbb";
String s2 = "bbbbb";
String s3 = "pwwkew";

System.out.println("Length of longest substring without repeating characters in s1: " + lengthOfLongestSubstring(s1)); // Output: 3
System.out.println("Length of longest substring without repeating characters in s2: " + lengthOfLongestSubstring(s2)); // Output: 1
System.out.println("Length of longest substring without repeating characters in s3: " + lengthOfLongestSubstring(s3)); // Output: 3
}
}

30. 串联所有单词的子串 <hard>

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
/*给定一个字符串 s 和一个字符串数组 words。 words 中所有字符串 长度相同。

s 中的 串联子串 是指一个包含 words 中所有字符串以任意顺序排列连接起来的子串。

例如,如果 words = ["ab","cd","ef"], 那么 "abcdef", "abefcd","cdabef", "cdefab","efabcd", 和 "efcdab" 都是串联子串。 "acdbef" 不是串联子串,因为他不是任何 words 排列的连接。
返回所有串联子串在 s 中的开始索引。你可以以 任意顺序 返回答案。



示例 1:

输入:s = "barfoothefoobarman", words = ["foo","bar"]
输出:[0,9]
解释:因为 words.length == 2 同时 words[i].length == 3,连接的子字符串的长度必须为 6。
子串 "barfoo" 开始位置是 0。它是 words 中以 ["bar","foo"] 顺序排列的连接。
子串 "foobar" 开始位置是 9。它是 words 中以 ["foo","bar"] 顺序排列的连接。
输出顺序无关紧要。返回 [9,0] 也是可以的。
示例 2:

输入:s = "wordgoodgoodgoodbestword", words = ["word","good","best","word"]
输出:[]
解释:因为 words.length == 4 并且 words[i].length == 4,所以串联子串的长度必须为 16。
s 中没有子串长度为 16 并且等于 words 的任何顺序排列的连接。
所以我们返回一个空数组。
示例 3:

输入:s = "barfoofoobarthefoobarman", words = ["bar","foo","the"]
输出:[6,9,12]
解释:因为 words.length == 3 并且 words[i].length == 3,所以串联子串的长度必须为 9。
子串 "foobarthe" 开始位置是 6。它是 words 中以 ["foo","bar","the"] 顺序排列的连接。
子串 "barthefoo" 开始位置是 9。它是 words 中以 ["bar","the","foo"] 顺序排列的连接。
子串 "thefoobar" 开始位置是 12。它是 words 中以 ["the","foo","bar"] 顺序排列的连接。


提示:

1 <= s.length <= 10^4
1 <= words.length <= 5000
1 <= words[i].length <= 30
words[i] 和 s 由小写英文字母组成*/
public class SubstringWithConcatenationOfAllWords30 {

public static List<Integer> findSubstring(String s, String[] words) {
/* List<Integer> result = new ArrayList<>();
if (s.isEmpty() || words.length == 0) return result;

int wordLen = words[0].length();
int wordCount = words.length;
int totalLen = wordLen * wordCount;
// 哈希表存储拆分的words每个单词和出现次数,已知word可以重复
HashMap<String, Integer> wordCountMap = new HashMap<>();

// 初始化哈希表
for (String word : words) {
wordCountMap.put(word, wordCountMap.getOrDefault(word, 0) + 1);
}

// 遍历字符串
for (int i = 0; i <= s.length() - totalLen; i++) {
// 存储当前子串中出现的单词和次数
HashMap<String, Integer> wordSeen = new HashMap<>();
// j指针右移,命中一个单词则记录一个,没命中或者出现次数超出则不符合
int j = 0;
while (j < wordCount) {
// 拆出单词
String word = s.substring(i + j * wordLen, i + (j + 1) * wordLen);
if (wordCountMap.containsKey(word)) {
// 记录出现次数
wordSeen.put(word, wordSeen.getOrDefault(word, 0) + 1);
if (wordSeen.get(word) > wordCountMap.getOrDefault(word, 0)) {
break;
}
} else {
break;
}
j++;
}
// 循环结束刚好符合,则记录起始下标
if (j == wordCount) {
result.add(i);
}
}
// O(n * m) O(m)
return result;*/

List<Integer> result = new ArrayList<>();
if (s.isEmpty() || words.length == 0) return result;

int wordLen = words[0].length();
int wordCount = words.length;
// 哈希表存储拆分的words每个单词和出现次数,已知word可以重复
HashMap<String, Integer> wordCountMap = new HashMap<>();
for (String word : words) {
wordCountMap.put(word, wordCountMap.getOrDefault(word, 0) + 1);
}

for (int i = 0; i < wordLen; i++) {
int left = i, right = i, count = 0;
// 存储当前子串中出现的单词和次数
HashMap<String, Integer> wordSeen = new HashMap<>();
// 通过滑动窗口
while (right + wordLen <= s.length()) {
String word = s.substring(right, right + wordLen);
right += wordLen;

if (wordCountMap.containsKey(word)) {
wordSeen.put(word, wordSeen.getOrDefault(word, 0) + 1);
count++;

// 当前单词出现次数已超过,则移动左指针,直到满足条件
while (wordSeen.getOrDefault(word, 0) > wordCountMap.getOrDefault(word, 0)) {
String leftWord = s.substring(left, left + wordLen);
count--;
wordSeen.put(leftWord, wordSeen.get(leftWord) - 1);
left += wordLen;
}

if (count == wordCount) {
result.add(left);
}
} else {
wordSeen.clear();
count = 0;
left = right;
}
}
}
// O(n) O(m)
return result;
}

public static void main(String[] args) {
String s1 = "barfoothefoobarman";
String[] words1 = {"foo","bar"};

String s2 = "wordgoodgoodgoodbestword";
String[] words2 = {"word","good","best","word"};

String s3 = "barfoofoobarthefoobarman";
String[] words3 = {"bar","foo","the"};

System.out.println("Substring indices in s1: " + findSubstring(s1, words1)); // Output: [0, 9]
System.out.println("Substring indices in s2: " + findSubstring(s2, words2)); // Output: []
System.out.println("Substring indices in s3: " + findSubstring(s3, words3)); // Output: [6, 9, 12]
}
}

76. 最小覆盖子串 <hard>

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
95
96
97
98
99
100
101
102
103
104
105
106
/*给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。

注意:

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


示例 1:

输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
示例 2:

输入:s = "a", t = "a"
输出:"a"
解释:整个字符串 s 是最小覆盖子串。
示例 3:

输入: s = "a", t = "aa"
输出: ""
解释: t 中两个字符 'a' 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。


提示:

m == s.length
n == t.length
1 <= m, n <= 105
s 和 t 由英文字母组成*/
public class MinimumWindowSubstring76 {
public static String minWindow(String s, String t) {
if (s.length() == 0 || t.length() == 0 || s.length() < t.length()) return "";
// 拆分字符串t
Map<Character, Integer> countMap = new HashMap<>();
////可以优化Map为集合来提高运行速度
// int[] tCount = new int[128];
for (char ch : t.toCharArray()) {
countMap.put(ch, countMap.getOrDefault(ch, 0) + 1);
}

int left = 0, right = 0;
int minLen = Integer.MAX_VALUE;
int minStart = 0;
int required = countMap.size();
int formed = 0;
Map<Character, Integer> windowMap = new HashMap<>();
// int[] windowCount = new int[128];
// 使用右边界指针遍历字符串s
while (right < s.length()) {
char ch = s.charAt(right);
// windowCount[ch]++;
windowMap.put(ch, windowMap.getOrDefault(ch, 0) + 1);

// if (tCount[ch] > 0 && windowCount[ch] <= tCount[ch]) {
if (countMap.containsKey(ch) && windowMap.get(ch).intValue() == countMap.get(ch).intValue()) {
// 当前字符已全部命中
formed++;
}

// 当字符全部命中后,开始移动左指针尝试删除
while (left <= right && formed == required) {
ch = s.charAt(left);

// 先更新当前最短字符串长度
if (minLen > right - left + 1) {
minLen = right - left + 1;
minStart = left;
}

//windowCount[ch]--;
//if (tCount[ch] > 0 && windowCount[ch] < tCount[ch]) {
// 移除左边界元素
windowMap.put(ch, windowMap.get(ch) - 1);
if (countMap.containsKey(ch) && windowMap.get(ch) < countMap.get(ch)) {
// 若删错元素,则formed-1,继续右移右边界
formed--;
}

// 删除成功,继续移动左边界
left++;
}
right++;
}

// O(n) O(m + n)
return minLen == Integer.MAX_VALUE ? "" : s.substring(minStart, minStart + minLen);
}

public static void main(String[] args) {
String s1 = "ADOBECODEBANC";
String t1 = "ABC";
System.out.println(minWindow(s1, t1)); // Output: "BANC"

String s2 = "a";
String t2 = "a";
System.out.println(minWindow(s2, t2)); // Output: "a"

String s3 = "a";
String t3 = "aa";
System.out.println(minWindow(s3, t3)); // Output: ""
}
}

3.4 矩阵 Matrix

多维数组

36. 有效的数独 <medium>

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
/*请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 ,验证已经填入的数字是否有效即可。

数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)


注意:

一个有效的数独(部分已被填充)不一定是可解的。
只需要根据以上规则,验证已经填入的数字是否有效即可。
空白格用 '.' 表示。


示例 1:


输入:board =
[["5","3",".",".","7",".",".",".","."]
,["6",".",".","1","9","5",".",".","."]
,[".","9","8",".",".",".",".","6","."]
,["8",".",".",".","6",".",".",".","3"]
,["4",".",".","8",".","3",".",".","1"]
,["7",".",".",".","2",".",".",".","6"]
,[".","6",".",".",".",".","2","8","."]
,[".",".",".","4","1","9",".",".","5"]
,[".",".",".",".","8",".",".","7","9"]]
输出:true
示例 2:

输入:board =
[["8","3",".",".","7",".",".",".","."]
,["6",".",".","1","9","5",".",".","."]
,[".","9","8",".",".",".",".","6","."]
,["8",".",".",".","6",".",".",".","3"]
,["4",".",".","8",".","3",".",".","1"]
,["7",".",".",".","2",".",".",".","6"]
,[".","6",".",".",".",".","2","8","."]
,[".",".",".","4","1","9",".",".","5"]
,[".",".",".",".","8",".",".","7","9"]]
输出:false
解释:除了第一行的第一个数字从 5 改为 8 以外,空格内其他数字均与 示例1 相同。 但由于位于左上角的 3x3 宫内有两个 8 存在, 因此这个数独是无效的。


提示:

board.length == 9
board[i].length == 9
board[i][j] 是一位数字(1-9)或者 '.'*/
public class ValidSudoku36 {


public static boolean isValidSudoku(char[][] board) {

if (board == null || board.length != 9 || board[0].length != 9) {
return false;
}

for (int i = 0; i < 9; i++) {
// 分别用数组记录数字是否出现过
boolean[] rowSeen = new boolean[9];
boolean[] colSeen = new boolean[9];

for (int j = 0; j < 9; j++) {
if (board[i][j] != '.') {
// 行检测
int num = board[i][j] - '1';
if (rowSeen[num]) {
// 有重复元素
return false;
}
rowSeen[num] = true;
}

if (board[j][i] != '.') {
// 列检测
int num = board[j][i] - '1';
if (colSeen[num]) {
// 有重复元素
return false;
}
colSeen[num] = true;
}
}
}

// 检查 3x3 宫,遍历9个子网格并使用一个数组 seen 来记录数字的出现情况
for (int block = 0; block < 9; block++) {
boolean[] seen = new boolean[9];
// 行索引和列索引要错位,从而能
for (int i = block / 3 * 3; i < block / 3 * 3 + 3; i++) {
// 行循环,除法,000,111,222,最后乘以3
for (int j = block % 3 * 3; j < block % 3 * 3 + 3; j++) {
// 列循环,求余,012,012,012,最后乘以3
if (board[i][j] != '.') {
int num = board[i][j] - '1';
if (seen[num]) {
return false;
}
seen[num] = true;
}
}
}
}

return true;
}

public static void main(String[] args) {
char[][] board1 = {
{'5','3','.','.','7','.','.','.','.'},
{'6','.','.','1','9','5','.','.','.'},
{'.','9','8','.','.','.','.','6','.'},
{'8','.','.','.','6','.','.','.','3'},
{'4','.','.','8','.','3','.','.','1'},
{'7','.','.','.','2','.','.','.','6'},
{'.','6','.','.','.','.','2','8','.'},
{'.','.','.','4','1','9','.','.','5'},
{'.','.','.','.','8','.','.','7','9'}
};
System.out.println(isValidSudoku(board1)); // Output: true

char[][] board2 = {
{'8','3','.','.','7','.','.','.','.'},
{'6','.','.','1','9','5','.','.','.'},
{'.','9','8','.','.','.','.','6','.'},
{'8','.','.','.','6','.','.','.','3'},
{'4','.','.','8','.','3','.','.','1'},
{'7','.','.','.','2','.','.','.','6'},
{'.','6','.','.','.','.','2','8','.'},
{'.','.','.','4','1','9','.','.','5'},
{'.','.','.','.','8','.','.','7','9'}
};
System.out.println(isValidSudoku(board2)); // Output: false
}
}

54. 螺旋矩阵 <medium>

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
/*给你一个 m 行 n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。

示例 1:

输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]

示例 2:

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

提示:

m == matrix.length
n == matrix[i].length
1 <= m, n <= 10
-100 <= matrix[i][j] <= 100*/
public class SpiralMatrix54 {

public static List<Integer> spiralOrder(int[][] matrix) {
List<Integer> result = new ArrayList<>();
if (matrix == null || matrix.length == 0) return result;

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

// 定义边界指针
int left = 0, right = cols - 1;
int top = 0, bottom = rows - 1;

while (left <= right && top <= bottom) {
// 右移
for (int i = left; i <= right; i++) {
result.add(matrix[top][i]);
}

// 下移
for (int i = top + 1; i <= bottom; i++) {
result.add(matrix[i][right]);
}

// 左移,除非上下指针相碰
if (top < bottom) {
for (int i = right - 1; i >= left; i--) {
result.add(matrix[bottom][i]);
}
}

// 上移,除非左右指针相碰
if (left < right) {
for (int i = bottom - 1; i > top; i--) {
result.add(matrix[i][left]);
}
}

// 移完一圈后,更新指针边界,左上+1,右下-1
left++;
right--;
top++;
bottom--;
}
// O(m*n)
return result;
}

public static void main(String[] args) {
int[][] matrix1 = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
int[][] matrix2 = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};

System.out.println(spiralOrder(matrix1)); // Output: [1, 2, 3, 6, 9, 8, 7, 4, 5]
System.out.println(spiralOrder(matrix2)); // Output: [1, 2, 3, 4, 8, 12, 11, 10, 9, 5, 6, 7]
}

}

48. 旋转图像 <medium>

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
/*给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。

你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。

示例 1:

输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[[7,4,1],[8,5,2],[9,6,3]]
示例 2:


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


提示:

n == matrix.length == matrix[i].length
1 <= n <= 20
-1000 <= matrix[i][j] <= 1000*/
public class RotateImage48 {

public static void rotate(int[][] matrix) {
int n = matrix.length;
for (int i = 0; i < n / 2; i++) {
// i表示层数
for (int j = i; j < n - i - 1; j++) {
// j通过四次交换操作旋转该层的四个边上的元素
int temp = matrix[i][j];
// 从上层的左上角开始,将四个边上的元素依次移到下一个位置
// 如4X4 00-30 30-33 33-03 03-00
// 01-20 20-32 32-13 13 01
matrix[i][j] = matrix[n - j - 1][i];
matrix[n - j - 1][i] = matrix[n - i - 1][n - j - 1];
matrix[n - i - 1][n - j - 1] = matrix[j][n - i - 1];
matrix[j][n - i - 1] = temp;
}
}
// O(n^2)
}

public static void main(String[] args) {
int[][] matrix1 = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
int[][] matrix2 = {{5, 1, 9, 11}, {2, 4, 8, 10}, {13, 3, 6, 7}, {15, 14, 12, 16}};

rotate(matrix1);
rotate(matrix2);

printMatrix(matrix1); // Output: [[7, 4, 1], [8, 5, 2], [9, 6, 3]]
printMatrix(matrix2); // Output: [[15, 13, 2, 5], [14, 3, 4, 1], [12, 6, 8, 9], [16, 7, 10, 11]]
}

private static void printMatrix(int[][] matrix) {
for (int[] row : matrix) {
for (int num : row) {
System.out.print(num + " ");
}
System.out.println();
}
}
}

73. 矩阵置零 <medium>

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
/*给定一个 m x n 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。

示例 1:

输入:matrix = [[1,1,1],[1,0,1],[1,1,1]]
输出:[[1,0,1],[0,0,0],[1,0,1]]

示例 2:

输入:matrix = [[0,1,2,0],[3,4,5,2],[1,3,1,5]]
输出:[[0,0,0,0],[0,4,5,0],[0,3,1,0]]

提示:

m == matrix.length
n == matrix[0].length
1 <= m, n <= 200
-2^31 <= matrix[i][j] <= 2^31 - 1*/
public class SetMatrixZeroes73 {
public static void setZeroes(int[][] matrix) {
// 首行和首列先不处理,作为标记行
int m = matrix.length;
int n = matrix[0].length;
boolean firstRowHasZero = false;
boolean firstColHasZero = false;

// 检查首行是否需要为0
for (int i = 0; i < n; i++) {
if (matrix[0][i] == 0) {
firstRowHasZero = true;
break;
}
}

// 检查首列是否需要为0
for (int i = 0; i < m; i++) {
if (matrix[i][0] == 0) {
firstColHasZero = true;
break;
}
}

// 标记零的边界点
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
// 从(1, 1)开始寻找0的坐标,找到后将上左两侧标记为0
if (matrix[i][j] == 0) {
matrix[i][0] = 0;
matrix[0][j] = 0;
}
}
}

// 如果两侧有任意点为0则将对应点也置0
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (matrix[i][0] == 0 || matrix[0][j] == 0) {
matrix[i][j] = 0;
}
}
}

// 最后处理首行和首列
if (firstRowHasZero) {
for (int i = 0; i < n; i++) {
matrix[0][i] = 0;
}
}

if (firstColHasZero) {
for (int i = 0; i < m; i++) {
matrix[i][0] = 0;
}
}
}

public static void main(String[] args) {
int[][] matrix1 = {{1, 1, 1}, {1, 0, 1}, {1, 1, 1}};
setZeroes(matrix1);
System.out.println(Arrays.deepToString(matrix1));

int[][] matrix2 = {{0, 1, 2, 0}, {3, 4, 5, 2}, {1, 3, 1, 5}};
setZeroes(matrix2);
System.out.println(Arrays.deepToString(matrix2));

}
}

289. 生命游戏 <medium>

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
/*根据 百度百科 , 生命游戏 ,简称为 生命 ,是英国数学家约翰·何顿·康威在 1970 年发明的细胞自动机。

给定一个包含 m × n 个格子的面板,每一个格子都可以看成是一个细胞。每个细胞都具有一个初始状态: 1 即为 活细胞 (live),或 0 即为 死细胞 (dead)。每个细胞与其八个相邻位置(水平,垂直,对角线)的细胞都遵循以下四条生存定律:

如果活细胞周围八个位置的活细胞数少于两个,则该位置活细胞死亡;
如果活细胞周围八个位置有两个或三个活细胞,则该位置活细胞仍然存活;
如果活细胞周围八个位置有超过三个活细胞,则该位置活细胞死亡;
如果死细胞周围正好有三个活细胞,则该位置死细胞复活;
下一个状态是通过将上述规则同时应用于当前状态下的每个细胞所形成的,其中细胞的出生和死亡是同时发生的。给你 m x n 网格面板 board 的当前状态,返回下一个状态。



示例 1:


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


输入:board = [[1,1],[1,0]]
输出:[[1,1],[1,1]]


提示:

m == board.length
n == board[i].length
1 <= m, n <= 25
board[i][j] 为 0 或 1


进阶:

你可以使用原地算法解决本题吗?请注意,面板上所有格子需要同时被更新:你不能先更新某些格子,然后使用它们的更新后的值再更新其他格子。
本题中,我们使用二维数组来表示面板。原则上,面板是无限的,但当活细胞侵占了面板边界时会造成问题。你将如何解决这些问题?*/
public class GameOfLife289 {
public void gameOfLife(int[][] board) {
// directions数组来代表细胞周围的八个方向,使用-1和2分别表示中间状态:活转死,死转活。
int m = board.length;
int n = board[0].length;
int[][] directions = {{-1, -1}, {-1, 0}, {-1, 1}, {0, -1}, {0, 1}, {1, -1}, {1, 0}, {1, 1}};

// 遍历每个细胞,计算其周围活细胞的数量
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
// 统计周围活细胞
int liveNeighbors = 0;

// 查看其8个方向的活细胞数量
for (int[] dir : directions) {
int newRow = i + dir[0];
int newCol = j + dir[1];

// 新的行列在矩阵内,并且为活细胞,则统计+1
if (newRow >= 0 && newRow < m && newCol >= 0 && newCol < n
&& (board[newRow][newCol] == 1 || board[newRow][newCol] == -1)) {
liveNeighbors++;
}
}

// 2:当前为死亡,且周围活细胞为3,要更新为活
if (board[i][j] == 0 && liveNeighbors == 3) {
board[i][j] = 2;
}

// -1:当前为活,且周围活细胞小于2或大于3,要更新为死
if (board[i][j] == 1 && (liveNeighbors < 2 || liveNeighbors > 3)) {
board[i][j] = -1;
}
}
}

// 循环将状态更新
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (board[i][j] == 2) {
board[i][j] = 1;
}
if (board[i][j] == -1) {
board[i][j] = 0;
}
}
}
// O(m * n)
}

public static void main(String[] args) {
int[][] board1 = {{0,1,0},{0,0,1},{1,1,1},{0,0,0}};
int[][] board2 = {{1,1},{1,0}};

GameOfLife289 gameOfLife = new GameOfLife289();
gameOfLife.gameOfLife(board1);
gameOfLife.gameOfLife(board2);

System.out.println("Result 1:");
for (int[] row : board1) {
for (int cell : row) {
System.out.print(cell + " ");
}
System.out.println();
}

System.out.println("Result 2:");
for (int[] row : board2) {
for (int cell : row) {
System.out.print(cell + " ");
}
System.out.println();
}
}
}

3.5 哈希表 Hash Table

  1. 快速查询和搜索
  2. 统计元素出现次数:重复元素、词频统计
  3. 映射关联:构建词典、映射关系等

383. 赎金信 <easy>

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
/*给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。

如果可以,返回 true ;否则返回 false 。

magazine 中的每个字符只能在 ransomNote 中使用一次。



示例 1:

输入:ransomNote = "a", magazine = "b"
输出:false
示例 2:

输入:ransomNote = "aa", magazine = "ab"
输出:false
示例 3:

输入:ransomNote = "aa", magazine = "aab"
输出:true


提示:

1 <= ransomNote.length, magazine.length <= 105
ransomNote 和 magazine 由小写英文字母组成*/
public class RansomNote383 {
public boolean canConstruct(String ransomNote, String magazine) {
/* Map<Character, Integer> charCountMap = new HashMap<>();
int i = 0;
while (i < ransomNote.length()) {
Character ch = ransomNote.charAt(i);
charCountMap.put(ch, charCountMap.getOrDefault(ch, 0) + 1);
i++;
}
int j = 0;
while (j < magazine.length()) {
Character ch = magazine.charAt(j);
if (charCountMap.containsKey(ch)) {
int count = charCountMap.get(ch);
if (count <= 1) {
charCountMap.remove(ch);
} else {
charCountMap.put(ch, count-1);
}
}
j++;
}
// O(n + m)
return charCountMap.size() == 0;*/

// 数组代替哈希表可以提高速度
int[] charCount = new int[26];

for (char ch : magazine.toCharArray()) {
charCount[ch - 'a']++;
}

for (char ch : ransomNote.toCharArray()) {
if (charCount[ch - 'a'] == 0) {
return false;
}
charCount[ch - 'a']--;
}

// O(n + m)
return true;
}

public static void main(String[] args) {
RansomNote383 note = new RansomNote383();

System.out.println(note.canConstruct("a", "b"));
System.out.println(note.canConstruct("aa", "ab"));
System.out.println(note.canConstruct("aa", "aab"));
}
}

205. 同构字符串 <easy>

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
/*给定两个字符串 s 和 t ,判断它们是否是同构的。

如果 s 中的字符可以按某种映射关系替换得到 t ,那么这两个字符串是同构的。

每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同字符不能映射到同一个字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。

示例 1:

输入:s = "egg", t = "add"
输出:true
示例 2:

输入:s = "foo", t = "bar"
输出:false
示例 3:

输入:s = "paper", t = "title"
输出:true


提示:

1 <= s.length <= 5 * 104
t.length == s.length
s 和 t 由任意有效的 ASCII 字符组成*/
public class IsomorphicStrings205 {
public boolean isIsomorphic(String s, String t) {
//if (s.length() != t.length()) {
// return false;
//}
//Map<Character, Character> sMap = new HashMap<>();
//Map<Character, Character> tMap = new HashMap<>();
//int i = 0;
//while (i < s.length()) {
// Character sc = s.charAt(i);
// Character tc = t.charAt(i);
// if ((sMap.containsKey(sc) || tMap.containsKey(tc)) && (sMap.get(sc) != tc || tMap.get(tc) != sc)) {
// return false;
// } else {
// sMap.put(sc, tc);
// tMap.put(tc, sc);
// }
// i++;
//}
//// O(n)
//return true;


if (s.length() != t.length()) {
return false;
}

int[] sToT = new int[128];
int[] tToS = new int[128];

for (int i = 0; i < s.length(); i++) {
char charS = s.charAt(i);
char charT = t.charAt(i);

if (sToT[charS] != tToS[charT]) {
return false;
}

sToT[charS] = i + 1;
tToS[charT] = i + 1;
}

return true;
}

public static void main(String[] args) {
IsomorphicStrings205 test = new IsomorphicStrings205();

System.out.println(test.isIsomorphic("egg", "add"));
System.out.println(test.isIsomorphic("foo", "bar"));
System.out.println(test.isIsomorphic("paper", "title"));
System.out.println(test.isIsomorphic("badc", "baba"));
}
}

290. 单词规律 <easy>

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
/*给定一种规律 pattern 和一个字符串 s ,判断 s 是否遵循相同的规律。

这里的 遵循 指完全匹配,例如, pattern 里的每个字母和字符串 s 中的每个非空单词之间存在着双向连接的对应规律。



示例1:

输入: pattern = "abba", s = "dog cat cat dog"
输出: true
示例 2:

输入:pattern = "abba", s = "dog cat cat fish"
输出: false
示例 3:

输入: pattern = "aaaa", s = "dog cat cat dog"
输出: false


提示:

1 <= pattern.length <= 300
pattern 只包含小写英文字母
1 <= s.length <= 3000
s 只包含小写英文字母和 ' '
s 不包含 任何前导或尾随对空格
s 中每个单词都被 单个空格 分隔*/
public class WordPattern290 {
public boolean wordPattern(String pattern, String s) {
//Map<Character, String> pMap = new HashMap<>();
//Map<String, Character> sMap = new HashMap<>();
//String[] strings = s.split(" ");
//
//if (pattern.length() != strings.length) {
// return false;
//}
//int i = 0;
//while (i < pattern.length()) {
// Character pc = pattern.charAt(i);
// String sc = strings[i];
// if ((sMap.containsKey(sc) || pMap.containsKey(pc)) && (sMap.get(sc) != pc || !pMap.get(pc).equals(sc))) {
// return false;
// } else {
// sMap.put(sc, pc);
// pMap.put(pc, sc);
// }
// i++;
//}
//// O(n)
//return true;
Map<Character, String> pMap = new HashMap<>();
String[] strings = s.split(" ");

if (pattern.length() != strings.length) {
return false;
}

for (int i = 0; i < pattern.length(); i++) {
char pc = pattern.charAt(i);
String sc = strings[i];

if (pMap.containsKey(pc)) {
if (!pMap.get(pc).equals(sc)) {
return false;
}
} else {
if (pMap.containsValue(sc)) {
return false;
}
pMap.put(pc, sc);
}
}
// O(n)
return true;
}

public static void main(String[] args) {
WordPattern290 test = new WordPattern290();

System.out.println(test.wordPattern("abba", "dog cat cat dog"));
System.out.println(test.wordPattern("abba", "dog cat cat fish"));
System.out.println(test.wordPattern("aaaa", "dog cat cat dog"));
}
}

49. 字母异位词分组 <medium>

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
/*给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。

注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。

示例 1:

输入: s = "anagram", t = "nagaram"
输出: true
示例 2:

输入: s = "rat", t = "car"
输出: false


提示:

1 <= s.length, t.length <= 5 * 104
s 和 t 仅包含小写字母


进阶: 如果输入字符串包含 unicode 字符怎么办?你能否调整你的解法来应对这种情况?*/
public class ValidAnagram242 {
public boolean isAnagram(String s, String t) {
//if (s.length() != t.length()) {
// return false;
//}
//
//int[] sList = new int[26];
//int[] tList = new int[26];
//
//for (int i = 0; i < s.length(); i++) {
// char charS = s.charAt(i);
// char charT = t.charAt(i);
// sList[charS- 'a']++;
// tList[charT- 'a']++;
//}
//for (int i = 0; i < sList.length; i++) {
// if (sList[i] != tList[i]) {
// return false;
// }
//}
//
//// O(n)
//return true;
if (s.length() != t.length()) {
return false;
}

int[] count = new int[26];

for (int i = 0; i < s.length(); i++) {
count[s.charAt(i) - 'a']++;
count[t.charAt(i) - 'a']--;
}

for (int value : count) {
if (value != 0) {
return false;
}
}

// O(n)
return true;
}

public static void main(String[] args) {
ValidAnagram242 test = new ValidAnagram242();
System.out.println(test.isAnagram("anagram", "nagaram"));
System.out.println(test.isAnagram("abba", "car"));
System.out.println(test.isAnagram("nl", "cx"));
}
}

49. 字母异位词分组 <medium>

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
/*给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。

字母异位词 是由重新排列源单词的所有字母得到的一个新单词。



示例 1:

输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
示例 2:

输入: strs = [""]
输出: [[""]]
示例 3:

输入: strs = ["a"]
输出: [["a"]]


提示:

1 <= strs.length <= 104
0 <= strs[i].length <= 100
strs[i] 仅包含小写字母*/
public class GroupAnagrams49 {
public List<List<String>> groupAnagrams(String[] strs) {
if (strs == null || strs.length == 0) {
return new ArrayList<>();
}

Map<String, List<String>> map = new HashMap<>();
for (String word : strs) {
// 将字母排序,时间复杂度:O(m*logm) m为字符串最大长度
char[] chars = word.toCharArray();
Arrays.sort(chars);
String sorted = String.valueOf(chars);

// 映射表记录,相同sorted的word
if (!map.containsKey(sorted)) {
map.put(sorted, new ArrayList<>());
}
map.get(sorted).add(word);
}

// O(n * m * logm)
return new ArrayList<>(map.values());
}

public static void main(String[] args) {
GroupAnagrams49 test = new GroupAnagrams49(); // Assuming the class name is Solution

// Example test cases
String[] strs1 = {"eat", "tea", "tan", "ate", "nat", "bat"};
List<List<String>> result1 = test.groupAnagrams(strs1);
System.out.println("Anagram Groups for strs1: " + result1);

String[] strs2 = {""};
List<List<String>> result2 = test.groupAnagrams(strs2);
System.out.println("Anagram Groups for strs2: " + result2);

String[] strs3 = {"a"};
List<List<String>> result3 = test.groupAnagrams(strs3);
System.out.println("Anagram Groups for strs3: " + result3);
}
}

1. 两数之和 <easy>

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
/*给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target  的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

你可以按任意顺序返回答案。



示例 1:

输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:

输入:nums = [3,2,4], target = 6
输出:[1,2]
示例 3:

输入:nums = [3,3], target = 6
输出:[0,1]


提示:

2 <= nums.length <= 10^4
-10^9 <= nums[i] <= 10^9
-10^9 <= target <= 10^9
只会存在一个有效答案


进阶:你可以想出一个时间复杂度小于 O(n^2) 的算法吗?*/
public class TwoSum1 {
public int[] twoSum(int[] nums, int target) {
//int[] result = new int[2];
//int i = 0, j = 1;
//while (i < nums.length){
// if (nums[i] + nums[j] == target) {
// result[0] = i;
// result[1] = j;
// return result;
// } else if (j == nums.length - 1) {
// i++;
// j = i + 1;
// } else {
// j++;
// }
//}
//// O(n^2)
//return result;

Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
// 已遍历的元素存入map
int complement = target - nums[i];
if (map.containsKey(complement)) {
return new int[] { map.get(complement), i };
}
map.put(nums[i], i);
}
// O(n)
return new int[2];
}

public static void main(String[] args) {
TwoSum1 test = new TwoSum1();
int[] nums1 = {2,7,11,15};
int target = 9;
int[] result1 = test.twoSum(nums1, target);
System.out.println("输出: " + Arrays.toString(result1)); // [0,1]

int[] nums2 = {3,2,4};
target = 6;
int[] result2 = test.twoSum(nums2, target);
System.out.println("输出: " + Arrays.toString(result2)); // [1,2]

int[] nums3 = {3,3};
target = 6;
int[] result3 = test.twoSum(nums3, target);
System.out.println("输出: " + Arrays.toString(result3)); // [0,1]

int[] nums4 = {-1,-2,-3,-4,-5};
target = -8;
int[] result4 = test.twoSum(nums4, target);
System.out.println("输出: " + Arrays.toString(result4)); // [2,4]
}
}

202. 快乐数 <easy>

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
/*编写一个算法来判断一个数 n 是不是快乐数。

「快乐数」 定义为:

对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
如果这个过程 结果为 1,那么这个数就是快乐数。
如果 n 是 快乐数 就返回 true ;不是,则返回 false 。

示例 1:

输入:n = 19
输出:true

解释:
1^2 + 9^2 = 82
8^2 + 2^2 = 68
6^2 + 8^2 = 100
1^2 + 0^2 + 0^2 = 1

示例 2:

输入:n = 2
输出:false

提示:

1 <= n <= 2^31 - 1*/
public class HappyNumber202 {
public boolean isHappy(int n) {
// 快慢指针
int slow = n;
int fast = getNext(n);

// 当结果为1或者无限循环时退出
while (fast != 1 && slow != fast) {
slow = getNext(slow);
fast = getNext(getNext(fast));
}

// O(logn)
return fast == 1;
}

private int getNext(int n) {
int totalSum = 0;
while (n > 0) {
// 求n的各位置数字的平方和
int digit = n % 10;
n /= 10;
totalSum += digit * digit;
}
return totalSum;
}
public static void main(String[] args) {
HappyNumber202 test = new HappyNumber202();
System.out.println(test.isHappy(19)); // true
System.out.println(test.isHappy(2)); // false
}
}

219. 存在重复元素 II <easy>

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
/*给你一个整数数组 nums 和一个整数 k ,判断数组中是否存在两个 不同的索引 i 和 j ,满足 nums[i] == nums[j] 且 abs(i - j) <= k 。如果存在,返回 true ;否则,返回 false 。

示例 1:

输入:nums = [1,2,3,1], k = 3
输出:true

示例 2:

输入:nums = [1,0,1,1], k = 1
输出:true

示例 3:

输入:nums = [1,2,3,1,2,3], k = 2
输出:false

提示:

1 <= nums.length <= 105
-109 <= nums[i] <= 109
0 <= k <= 105*/
public class ContainsDuplicateII219 {
public boolean containsNearbyDuplicate(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0;i < nums.length;i++) {
if (map.containsKey(nums[i]) && Math.abs(map.get(nums[i]) - i) <= k) {
return true;
}
map.put(nums[i], i);
}
return false;
}
}

128. 最长连续序列 <medium>

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
/*给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。

请你设计并实现时间复杂度为 O(n) 的算法解决此问题。

示例 1:

输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。

示例 2:

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

提示:

0 <= nums.length <= 105
-109 <= nums[i] <= 109*/
public class LongestConsecutiveSequence128 {
public int longestConsecutive(int[] nums) {
// 将数组放入哈希集合
Set<Integer> set = new HashSet<>();
for (int num : nums) {
set.add(num);
}

int maxSequence = 0;
// 遍历数组
for (int num : nums) {
// 如果当前元素-1后不在哈希集合中,则它可能是一个起点
if (!set.contains(num - 1)) {
int currentNum = num;
int currentSequence = 1;

// 遍历获取连续序列
while (set.contains(currentNum + 1)) {
currentNum++;
currentSequence++;
}

maxSequence = Math.max(maxSequence, currentSequence);
}
}

// O(n)
return maxSequence;
}
}

3.6 区间 Interval

228. 汇总区间 <easy>

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
95
96
97
/*给定一个  无重复元素 的 有序 整数数组 nums 。

返回 恰好覆盖数组中所有数字 的 最小有序 区间范围列表 。也就是说,nums 的每个元素都恰好被某个区间范围所覆盖,并且不存在属于某个范围但不属于 nums 的数字 x 。

列表中的每个区间范围 [a,b] 应该按如下格式输出:

"a->b" ,如果 a != b
"a" ,如果 a == b


示例 1:

输入:nums = [0,1,2,4,5,7]
输出:["0->2","4->5","7"]
解释:区间范围是:
[0,2] --> "0->2"
[4,5] --> "4->5"
[7,7] --> "7"
示例 2:

输入:nums = [0,2,3,4,6,8,9]
输出:["0","2->4","6","8->9"]
解释:区间范围是:
[0,0] --> "0"
[2,4] --> "2->4"
[6,6] --> "6"
[8,9] --> "8->9"


提示:

0 <= nums.length <= 20
-231 <= nums[i] <= 231 - 1
nums 中的所有值都 互不相同
nums 按升序排列*/
public class SummaryRanges228 {
public List<String> summaryRanges(int[] nums) {
//List<String> results = new ArrayList<>();
//if (nums.length == 0) {
// return results;
//}
//int j, start = 0;
//boolean flag = false;
//for (int i = 0;i < nums.length;i++) {
// j = i + 1;
// if (j < nums.length && nums[j] > nums[i] + 1) {
// if (flag) {
// results.add(start + "->" + nums[i]);
// flag = false;
// } else {
// results.add(nums[i] + "");
// }
// } else if (j < nums.length && nums[j] == nums[i] + 1) {
// if (!flag) {
// start = nums[i];
// flag = true;
// }
// } else {
// if (flag) {
// results.add(start + "->" + nums[i]);
// flag = false;
// } else {
// results.add(nums[i] + "");
// }
// }
//}
//return results;
List<String> result = new ArrayList<>();
if (nums == null || nums.length == 0) {
return result;
}

for (int i = 0; i < nums.length; i++) {
int start = nums[i];
// 有连续数字时,循环跳过
while (i + 1 < nums.length && nums[i + 1] == nums[i] + 1) {
i++;
}
int end = nums[i];
if (start == end) {
result.add(Integer.toString(start));
} else {
result.add(start + "->" + end);
}
}

return result;
}

public static void main(String[] args) {
SummaryRanges228 test = new SummaryRanges228();
//int[] nums1 = {0,1,2,4,5,7};
//System.out.println(test.summaryRanges(nums1)); // ["0->2","4->5","7"]
int[] nums2 = {0,2,3,4,6,8,9};
System.out.println(test.summaryRanges(nums2)); // ["0","2->4","6","8->9"]
}
}

56. 合并区间 <medium>

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
/*以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。

示例 1:

输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
示例 2:

输入:intervals = [[1,4],[4,5]]
输出:[[1,5]]
解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。


提示:

1 <= intervals.length <= 104
intervals[i].length == 2
0 <= starti <= endi <= 104*/
public class MergeIntervals56 {
public int[][] merge(int[][] intervals) {
//Arrays.sort(intervals, Comparator.comparingInt(a -> a[0]));
//List<int[]> results = new ArrayList<>();
//for (int[] interval : intervals) {
// if (results.isEmpty() || results.get(results.size() - 1)[1] < interval[0]) {
// // 若数组为空,或当前区间不覆盖数组已有区间,直接加入数组
// results.add(interval);
// } else {
// // 否则当前区间覆盖数组最后一个区间,通过更新结束边界来更新区间
// results.get(results.size() - 1)[1] = Math.max(results.get(results.size() - 1)[1], interval[1]);
// }
//}
//// O(n log n)
//return results.toArray(new int[results.size()][]);

List<int[]> result = new ArrayList<>();
if (intervals.length == 0) {
return result.toArray(new int[0][]);
}
// 先保证有序
Arrays.sort(intervals, Comparator.comparingInt(a -> a[0]));
int[] currentInterval = intervals[0];
result.add(currentInterval);

for (int[] interval : intervals) {
int currentEnd = currentInterval[1];
int nextStart = interval[0];
int nextEnd = interval[1];

if (currentEnd >= nextStart) {
currentInterval[1] = Math.max(currentEnd, nextEnd);
} else {
currentInterval = interval;
result.add(currentInterval);
}
}

return result.toArray(new int[result.size()][]);
}
public static void main(String[] args) {
MergeIntervals56 test = new MergeIntervals56();
int[][] intervals1 = {{1,3},{2,6},{8,10},{15,18}};
printArray(test.merge(intervals1)); // [[1,6],[8,10],[15,18]]
int[][] intervals2 = {{1,4},{4,5}};
printArray(test.merge(intervals2)); // [[1,5]]
int[][] intervals3 = {{1,4},{0,4}};
printArray(test.merge(intervals3)); // [[0,4]]
int[][] intervals4 = {{1,4},{2,3}};
printArray(test.merge(intervals4)); // [[1,4]]
}
public static void printArray(int[][] matrix) {
for (int[] row : matrix) {
System.out.print("[");
for (int num : row) {
System.out.print(num + " ");
}
System.out.print("]");
}
System.out.println();
}
}

57. 插入区间 <medium>

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
95
/*给你一个 无重叠的 ,按照区间起始端点排序的区间列表。

在列表中插入一个新的区间,你需要确保列表中的区间仍然有序且不重叠(如果有必要的话,可以合并区间)。

示例 1:

输入:intervals = [[1,3],[6,9]], newInterval = [2,5]
输出:[[1,5],[6,9]]
示例 2:

输入:intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], newInterval = [4,8]
输出:[[1,2],[3,10],[12,16]]
解释:这是因为新的区间 [4,8] 与 [3,5],[6,7],[8,10] 重叠。
示例 3:

输入:intervals = [], newInterval = [5,7]
输出:[[5,7]]
示例 4:

输入:intervals = [[1,5]], newInterval = [2,3]
输出:[[1,5]]
示例 5:

输入:intervals = [[1,5]], newInterval = [2,7]
输出:[[1,7]]


提示:

0 <= intervals.length <= 104
intervals[i].length == 2
0 <= intervals[i][0] <= intervals[i][1] <= 105
intervals 根据 intervals[i][0] 按 升序 排列
newInterval.length == 2
0 <= newInterval[0] <= newInterval[1] <= 105*/
public class InsertInterval57 {
public int[][] insert(int[][] intervals, int[] newInterval) {
List<int[]> result = new ArrayList<>();

int i = 0;
int n = intervals.length;

// 先将原区间集合在新区间之前的插入数组
while (i < n && intervals[i][1] < newInterval[0]) {
result.add(intervals[i]);
i++;
}

// 若新区间在当前区间开始之后
while (i < n && intervals[i][0] <= newInterval[1]) {
// 将新区间置换,取更小的左边和最大的右边
newInterval[0] = Math.min(newInterval[0], intervals[i][0]);
newInterval[1] = Math.max(newInterval[1], intervals[i][1]);
i++;
}
result.add(newInterval);

// 将剩余区间加入数组
while (i < n) {
result.add(intervals[i]);
i++;
}

// O(n)
return result.toArray(new int[result.size()][]);
}

public static void main(String[] args) {
InsertInterval57 inserter = new InsertInterval57();

int[][] intervals1 = {{1, 3}, {6, 9}};
int[] newInterval1 = {2, 5};
print(inserter.insert(intervals1, newInterval1));//[[1,5],[6,9]]

int[][] intervals2 = {{1,2},{3,5},{6,7},{8,10},{12,16}};
int[] newInterval2 = {4,8};
print(inserter.insert(intervals2, newInterval2));//[[1,2],[3,10],[12,16]]

int[][] intervals3 = {{1,5}};
int[] newInterval3 = {2,3};
print(inserter.insert(intervals3, newInterval3));//[[1,5]]

int[][] intervals4 = {{1,5}};
int[] newInterval4 = {2,7};
print(inserter.insert(intervals4, newInterval4));//[[1,7]]
}

public static void print(int[][] mergedIntervals) {
for (int[] interval : mergedIntervals) {
System.out.print(Arrays.toString(interval));
}
System.out.println();
}

}

452. 用最少数量的箭引爆气球 <medium>

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
/*有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] = [xstart, xend] 表示水平直径在 xstart 和 xend之间的气球。你不知道气球的确切 y 坐标。

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

给你一个数组 points ,返回引爆所有气球所必须射出的 最小 弓箭数 。


示例 1:

输入:points = [[10,16],[2,8],[1,6],[7,12]]
输出:2
解释:气球可以用2支箭来爆破:
-在x = 6处射出箭,击破气球[2,8]和[1,6]。
-在x = 11处发射箭,击破气球[10,16]和[7,12]。
示例 2:

输入:points = [[1,2],[3,4],[5,6],[7,8]]
输出:4
解释:每个气球需要射出一支箭,总共需要4支箭。
示例 3:

输入:points = [[1,2],[2,3],[3,4],[4,5]]
输出:2
解释:气球可以用2支箭来爆破:
- 在x = 2处发射箭,击破气球[1,2]和[2,3]。
- 在x = 4处射出箭,击破气球[3,4]和[4,5]。


提示:

1 <= points.length <= 105
points[i].length == 2
-231 <= xstart < xend <= 231 - 1*/
public class MinimumNumberOfArrowsToBurstBalloons452 {
public int findMinArrowShots(int[][] points) {
// 排序 + 贪心
if (points == null || points.length == 0) {
return 0;
}

// 通过右边界排序
Arrays.sort(points, Comparator.comparingInt(a -> a[1]));

int arrows = 1;
int end = points[0][1];

// 遍历气球节点
for (int i = 1; i < points.length; i++) {
// 若当前节点与end不重叠,箭数增加,更新新的end
if (points[i][0] > end) {
arrows++;
end = points[i][1];
}
}

// O(n log n)
return arrows;
}
public static void main(String[] args) {
MinimumNumberOfArrowsToBurstBalloons452 minArrows = new MinimumNumberOfArrowsToBurstBalloons452();
int[][] points1 = {{10, 16}, {2, 8}, {1, 6}, {7, 12}};
int[][] points2 = {{1, 2}, {3, 4}, {5, 6}, {7, 8}};
int[][] points3 = {{1, 2}, {2, 3}, {3, 4}, {4, 5}};

System.out.println(minArrows.findMinArrowShots(points1)); // Output: 2
System.out.println(minArrows.findMinArrowShots(points2)); // Output: 4
System.out.println(minArrows.findMinArrowShots(points3)); // Output: 2
}
}

3.7 栈 Stack

20. 有效的括号 <easy>

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
/*给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
每个右括号都有一个对应的相同类型的左括号。


示例 1:

输入:s = "()"
输出:true
示例 2:

输入:s = "()[]{}"
输出:true
示例 3:

输入:s = "(]"
输出:false


提示:

1 <= s.length <= 104
s 仅由括号 '()[]{}' 组成*/
public class ValidParentheses20 {
public boolean isValid(String s) {
if (s.length() < 2 || s.length() % 2 == 1) {
return false;
}
Stack<Character> stack = new Stack<>();
int i = 0;
// 遍历遇到左括号入栈,遇到右括号出栈,判断是否匹配
while (i < s.length()) {
char ch = s.charAt(i);
if (left(ch)) {
// 左括号
stack.push(s.charAt(i));
} else if (!stack.empty()) {
// 右括号,并且栈中有左括号
char left = stack.pop();
if (!match(left, ch)) {
return false;
}
} else {
// 右括号,栈中没有左括号
return false;
}
i++;
}

// O(n)
// 栈中括号是否全部匹配
return stack.empty();
}

public boolean match(char left, char right) {
if (left == '(') {
return right == ')';
} else if (left == '{') {
return right == '}';
} else if (left == '[') {
return right == ']';
} else {
return false;
}
}

public boolean left(char left) {
return left == '(' || left == '{' || left == '[';
}

public static void main(String[] args) {
ValidParentheses20 test = new ValidParentheses20();

System.out.println(test.isValid("()")); // true
System.out.println(test.isValid("()[]{}")); // true
System.out.println(test.isValid("(]")); // false
}
}

71. 简化路径 <medium>

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
/*给你一个字符串 path ,表示指向某一文件或目录的 Unix 风格 绝对路径 (以 '/' 开头),请你将其转化为更加简洁的规范路径。

在 Unix 风格的文件系统中,一个点(.)表示当前目录本身;此外,两个点 (..) 表示将目录切换到上一级(指向父目录);两者都可以是复杂相对路径的组成部分。任意多个连续的斜杠(即,'//')都被视为单个斜杠 '/' 。 对于此问题,任何其他格式的点(例如,'...')均被视为文件/目录名称。

请注意,返回的 规范路径 必须遵循下述格式:

始终以斜杠 '/' 开头。
两个目录名之间必须只有一个斜杠 '/' 。
最后一个目录名(如果存在)不能 以 '/' 结尾。
此外,路径仅包含从根目录到目标文件或目录的路径上的目录(即,不含 '.' 或 '..')。
返回简化后得到的 规范路径 。



示例 1:

输入:path = "/home/"
输出:"/home"
解释:注意,最后一个目录名后面没有斜杠。
示例 2:

输入:path = "/../"
输出:"/"
解释:从根目录向上一级是不可行的,因为根目录是你可以到达的最高级。
示例 3:

输入:path = "/home//foo/"
输出:"/home/foo"
解释:在规范路径中,多个连续斜杠需要用一个斜杠替换。
示例 4:

输入:path = "/a/./b/../../c/"
输出:"/c"


提示:

1 <= path.length <= 3000
path 由英文字母,数字,'.','/' 或 '_' 组成。
path 是一个有效的 Unix 风格绝对路径。*/
public class SimplifyPath71 {
public static String simplifyPath(String path) {
Deque<String> stack = new ArrayDeque<>();
Set<String> skip = new HashSet<>(Arrays.asList(".", ""));

// 根据/分割字符串
for (String dir : path.split("/")) {
if (dir.equals("..")) {
// ..表示返回上一级,弹出栈顶元素
if (!stack.isEmpty()) {
stack.pop();
}
} else if (!skip.contains(dir)) {
// 有效目录,则存放入栈
stack.push(dir);
}
// 其余直接跳过
}

// 栈中剩余的元素即为简化后的路径
StringBuilder result = new StringBuilder();
while (!stack.isEmpty()) {
result.insert(0, "/" + stack.pop());
}
// O(n)
return result.length() == 0 ? "/" : result.toString();
}

public static void main(String[] args) {
String[] testCases = {"/home/", "/../", "/home//foo/", "/a/./b/../../c/", "/a/./b/../../...c/"};
for (String path : testCases) {
System.out.println("Input: " + path);
System.out.println("Output: " + simplifyPath(path));
}
}
}

155. 最小栈 <medium>

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
/*设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。

实现 MinStack 类:

MinStack() 初始化堆栈对象。
void push(int val) 将元素val推入堆栈。
void pop() 删除堆栈顶部的元素。
int top() 获取堆栈顶部的元素。
int getMin() 获取堆栈中的最小元素。


示例 1:

输入:
["MinStack","push","push","push","getMin","pop","top","getMin"]
[[],[-2],[0],[-3],[],[],[],[]]

输出:
[null,null,null,null,-3,null,0,-2]

解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.getMin(); --> 返回 -2.


提示:

-231 <= val <= 231 - 1
pop、top 和 getMin 操作总是在 非空栈 上调用
push, pop, top, and getMin最多被调用 3 * 104 次*/
public class MinStack155 {

// 存储元素
Stack<Integer> stack;
// 存储当前栈中的最小元素
Stack<Integer> minStack;

public MinStack155() {
stack = new Stack<>();
minStack = new Stack<>();
}

public void push(int val) {
stack.push(val);
// 新值为当前最小元素,则入栈
if (minStack.isEmpty() || val <= minStack.peek()) {
minStack.push(val);
}
}

public void pop() {
// 如果栈顶是最小元素,则两个栈都要出栈
if (stack.peek().equals(minStack.peek())) {
minStack.pop();
}
stack.pop();
}

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

public int getMin() {
return minStack.peek();
}

public static void main(String[] args) {
MinStack155 minStack = new MinStack155();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
System.out.println(minStack.getMin()); // Output: -3
minStack.pop();
System.out.println(minStack.top()); // Output: 0
System.out.println(minStack.getMin()); // Output: -2
}
}

/**
* Your MinStack object will be instantiated and called as such:
* MinStack obj = new MinStack();
* obj.push(val);
* obj.pop();
* int param_3 = obj.top();
* int param_4 = obj.getMin();
*/

150. 逆波兰表达式求值 <medium>

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
95
96
97
98
99
/*给你一个字符串数组 tokens ,表示一个根据 逆波兰表示法 表示的算术表达式。

请你计算该表达式。返回一个表示表达式值的整数。

注意:

有效的算符为 '+'、'-'、'*' 和 '/' 。
每个操作数(运算对象)都可以是一个整数或者另一个表达式。
两个整数之间的除法总是 向零截断 。
表达式中不含除零运算。
输入是一个根据逆波兰表示法表示的算术表达式。
答案及所有中间计算结果可以用 32 位 整数表示。


示例 1:

输入:tokens = ["2","1","+","3","*"]
输出:9
解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9
示例 2:

输入:tokens = ["4","13","5","/","+"]
输出:6
解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6
示例 3:

输入:tokens = ["10","6","9","3","+","-11","*","/","*","17","+","5","+"]
输出:22
解释:该算式转化为常见的中缀算术表达式为:
((10 * (6 / ((9 + 3) * -11))) + 17) + 5
= ((10 * (6 / (12 * -11))) + 17) + 5
= ((10 * (6 / -132)) + 17) + 5
= ((10 * 0) + 17) + 5
= (0 + 17) + 5
= 17 + 5
= 22


提示:

1 <= tokens.length <= 104
tokens[i] 是一个算符("+"、"-"、"*" 或 "/"),或是在范围 [-200, 200] 内的一个整数


逆波兰表达式:

逆波兰表达式是一种后缀表达式,所谓后缀就是指算符写在后面。

平常使用的算式则是一种中缀表达式,如 ( 1 + 2 ) * ( 3 + 4 ) 。
该算式的逆波兰表达式写法为 ( ( 1 2 + ) ( 3 4 + ) * ) 。
逆波兰表达式主要有以下两个优点:

去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。
适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中*/
public class EvaluateReversePolishNotation150 {
public int evalRPN(String[] tokens) {
Deque<String> stack = new ArrayDeque<>();
int result = 0;
for (String token : tokens) {
if (isDigit(token)) {
// 数字
stack.push(token);
} else {
// 运算符号
int b = Integer.parseInt(stack.pop());
int a = Integer.parseInt(stack.pop());
if ("+".equals(token)) {
result = a + b;
} else if ("-".equals(token)) {
result = a - b;
} else if ("*".equals(token)) {
result = a * b;
} else if ("/".equals(token)) {
result = a / b;
}
stack.push(result + "");
}
}
if (stack.isEmpty()) {
return 0;
} else {
return Integer.parseInt(stack.pop());
}
}

public boolean isDigit(String token) {
return !("+".equals(token) || "-".equals(token) || "*".equals(token) || "/".equals(token));
}

public static void main(String[] args) {
EvaluateReversePolishNotation150 test = new EvaluateReversePolishNotation150();
String[] tokens1 = {"2","1","+","3","*"};
System.out.println(test.evalRPN(tokens1)); // 9
String[] tokens2 = {"4","13","5","/","+"};
System.out.println(test.evalRPN(tokens2)); // 6
String[] tokens3 = {"10","6","9","3","+","-11","*","/","*","17","+","5","+"};
System.out.println(test.evalRPN(tokens3)); // 22
}
}

224. 基本计算器 <hard>

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
/*给你一个字符串表达式 s ,请你实现一个基本计算器来计算并返回它的值。

注意:不允许使用任何将字符串作为数学表达式计算的内置函数,比如 eval() 。



示例 1:

输入:s = "1 + 1"
输出:2
示例 2:

输入:s = " 2-1 + 2 "
输出:3
示例 3:

输入:s = "(1+(4+5+2)-3)+(6+8)"
输出:23


提示:

1 <= s.length <= 3 * 105
s 由数字、'+'、'-'、'('、')'、和 ' ' 组成
s 表示一个有效的表达式
'+' 不能用作一元运算(例如, "+1" 和 "+(2 + 3)" 无效)
'-' 可以用作一元运算(即 "-1" 和 "-(2 + 3)" 是有效的)
输入中不存在两个连续的操作符
每个数字和运行的计算将适合于一个有符号的 32位 整数*/
public class BasicCalculator224 {
public static int calculate(String s) {
//维护两个栈,一个用于存储数字,一个用于存储运算符号
Deque<Integer> numStack = new ArrayDeque<>();
Deque<Integer> signStack = new ArrayDeque<>();
// 当前数字的值
int num = 0;
// 当前数字的符号
int sign = 1;
// 当前的计算结果
int result = 0;

for (char ch : s.toCharArray()) {
if (Character.isDigit(ch)) {
// 数字,若之前已有字符为数字,则需要进位
num = num * 10 + (ch - '0');
} else if (ch == '+' || ch == '-') {
// +或-,更新结果 result,然后重置 num 和 sign
result += sign * num;
num = 0;
// 减号作为下一数字的负符号
sign = (ch == '+') ? 1 : -1;
} else if (ch == '(') {
// 将当前结果 result 和当前符号 sign 入栈,重置 result 和 sign
numStack.push(result);
signStack.push(sign);
result = 0;
sign = 1;
} else if (ch == ')') {
// 右括号,则弹出栈顶元素存放的之前的结果进行计算,并更新 result 和 sign
// 当前括号结果
result += sign * num;
result *= signStack.pop();
// 加上之前的结果
result += numStack.pop();
num = 0;
}
}

result += sign * num;
// O(n)
return result;
}

public static void main(String[] args) {
String expr1 = "1 + 1";
String expr2 = " 2-1 + 2 ";
String expr3 = "(1+(4+5+2)-3)+(6+8)";

System.out.println("Expr 1 result: " + calculate(expr1)); // Output: 2
System.out.println("Expr 2 result: " + calculate(expr2)); // Output: 3
System.out.println("Expr 3 result: " + calculate(expr3)); // Output: 23
}
}

3.8 链表 Linked List

141. 环形链表 <easy> 快慢指针

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
95
96
97
98
99
100
101
102
103
104
105
/*给你一个链表的头节点 head ,判断链表中是否有环。

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

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



示例 1:



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



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



输入:head = [1], pos = -1
输出:false
解释:链表中没有环。


提示:

链表中节点的数目范围是 [0, 104]
-105 <= Node.val <= 105
pos 为 -1 或者链表中的一个 有效索引 。


进阶:你能用 O(1)(即,常量)内存解决此问题吗?*/

/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class LinkedListCycle141 {
public static boolean hasCycle(ListNode head) {
// 使用快慢指针解决。通过两个指针在链表上移动来判断是否有环。
if (head == null || head.next == null) {
return false;
}

// 定义两个指针,一个慢指针 slow,一次移动一个节点;一个快指针 fast,一次移动两个节点。如果链表中有环,那么快指针终究会追上慢指针
ListNode slow = head;
ListNode fast = head.next;

while (fast != null && fast.next != null) {
// 每次移动后,判断 slow 和 fast 是否指向相同的节点
if (slow == fast) {
// 如果是,则链表中有环
return true;
}
// 慢指针 slow 移动一步
slow = slow.next;
// 快指针 fast 移动两步
fast = fast.next.next;
}

// O(N) O(1)
return false;
}

public static void main(String[] args) {
ListNode head1 = new ListNode(3);
head1.next = new ListNode(2);
head1.next.next = new ListNode(0);
head1.next.next.next = new ListNode(-4);
head1.next.next.next.next = head1.next; // Creating a cycle

ListNode head2 = new ListNode(1);
head2.next = new ListNode(2);
head2.next.next = head2; // Creating a cycle

ListNode head3 = new ListNode(1);

System.out.println("Has cycle in head1: " + hasCycle(head1)); // Output: true
System.out.println("Has cycle in head2: " + hasCycle(head2)); // Output: true
System.out.println("Has cycle in head3: " + hasCycle(head3)); // Output: false
}

static class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
next = null;
}
}
}

2. 两数相加 <medium> 遍历同时逐位相加

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
/*给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。



示例 1:


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

输入:l1 = [0], l2 = [0]
输出:[0]
示例 3:

输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
输出:[8,9,9,9,0,0,0,1]


提示:

每个链表中的节点数在范围 [1, 100] 内
0 <= Node.val <= 9
题目数据保证列表表示的数字不含前导零*/

import java.math.BigDecimal;

/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
public class AddTwoNumbers2 {

public static ListNode addTwoNumbers(ListNode l1, ListNode l2) {
// 普通解法:通过将链表转换为整数进行相加,然后再将结果转换回链表
// 因为每个链表中的节点数在范围 [1, 100] 内,所以int和long的长度都不足
//BigDecimal a = BigDecimal.ZERO;
//BigDecimal x = BigDecimal.ONE;
//while (l1 != null) {
// a = a.add(x.multiply(BigDecimal.valueOf(l1.val)));
// x = x.multiply(BigDecimal.TEN);
// l1 = l1.next;
//}
//
//BigDecimal b = BigDecimal.ZERO;
//x = BigDecimal.ONE;
//while (l2 != null) {
// b = b.add(x.multiply(BigDecimal.valueOf(l2.val)));
// x = x.multiply(BigDecimal.TEN);
// l2 = l2.next;
//}
//BigDecimal c = a.add(b);
//String string = c + "";
//ListNode result = new ListNode(Character.getNumericValue(string.charAt(string.length() - 1)));
//ListNode first = result;
//for (int i = string.length() - 2;i >= 0;i--) {
// ListNode temp = new ListNode(Character.getNumericValue(string.charAt(i)));
// result.next = temp;
// result = temp;
//}
//
//return first;

// 优化:在遍历链表的同时,逐位相加并构建新链表作为结果。
ListNode dummyHead = new ListNode();
ListNode current = dummyHead;
// 处理每个节点相加后的进位。当两个节点相加的和大于等于 10 时,需要将进位值传递到下一个节点的计算中。
int carry = 0;

// 同时遍历两个链表
while (l1 != null || l2 != null) {
int x = (l1 != null) ? l1.val : 0;
int y = (l2 != null) ? l2.val : 0;

// 计算当前位的和,并更新进位
int sum = carry + x + y;
carry = sum / 10;

// 将加后的节点构建新的链表节点
current.next = new ListNode(sum % 10);
current = current.next;

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

if (carry > 0) {
current.next = new ListNode(carry);
}

return dummyHead.next;
}

public static void printLinkedList(ListNode head) {
while (head != null) {
System.out.print(head.val);
head = head.next;
if (head != null) {
System.out.print(" -> ");
}
}
System.out.println();
}

public static void main(String[] args) {
AddTwoNumbers2.ListNode l1 = new AddTwoNumbers2.ListNode(2);
l1.next = new AddTwoNumbers2.ListNode(4);
l1.next.next = new AddTwoNumbers2.ListNode(3);
AddTwoNumbers2.ListNode l2 = new AddTwoNumbers2.ListNode(5);
l2.next = new AddTwoNumbers2.ListNode(6);
l2.next.next = new AddTwoNumbers2.ListNode(4);

AddTwoNumbers2.ListNode l3 = new AddTwoNumbers2.ListNode(0);
AddTwoNumbers2.ListNode l4 = new AddTwoNumbers2.ListNode(0);

AddTwoNumbers2.ListNode l5 = new AddTwoNumbers2.ListNode(9);
l5.next = new AddTwoNumbers2.ListNode(9);
l5.next.next = new AddTwoNumbers2.ListNode(9);
l5.next.next.next = new AddTwoNumbers2.ListNode(9);
l5.next.next.next.next = new AddTwoNumbers2.ListNode(9);
l5.next.next.next.next.next = new AddTwoNumbers2.ListNode(9);
l5.next.next.next.next.next.next = new AddTwoNumbers2.ListNode(9);
AddTwoNumbers2.ListNode l6 = new AddTwoNumbers2.ListNode(9);
l6.next = new AddTwoNumbers2.ListNode(9);
l6.next.next = new AddTwoNumbers2.ListNode(9);
l6.next.next.next = new AddTwoNumbers2.ListNode(9);

// 数字长度10,int不够用
AddTwoNumbers2.ListNode l7 = new AddTwoNumbers2.ListNode(9);
AddTwoNumbers2.ListNode l8 = new AddTwoNumbers2.ListNode(1);
l8.next = new AddTwoNumbers2.ListNode(9);
l8.next.next = new AddTwoNumbers2.ListNode(9);
l8.next.next.next = new AddTwoNumbers2.ListNode(9);
l8.next.next.next.next = new AddTwoNumbers2.ListNode(9);
l8.next.next.next.next.next = new AddTwoNumbers2.ListNode(9);
l8.next.next.next.next.next.next = new AddTwoNumbers2.ListNode(9);
l8.next.next.next.next.next.next.next = new AddTwoNumbers2.ListNode(9);
l8.next.next.next.next.next.next.next.next = new AddTwoNumbers2.ListNode(9);
l8.next.next.next.next.next.next.next.next.next = new AddTwoNumbers2.ListNode(9);

// 数字长度超过19,long不够用
AddTwoNumbers2.ListNode l9 = new AddTwoNumbers2.ListNode(1);
l9.next = new AddTwoNumbers2.ListNode(0);
l9.next.next = new AddTwoNumbers2.ListNode(0);
l9.next.next.next = new AddTwoNumbers2.ListNode(0);
l9.next.next.next.next= new AddTwoNumbers2.ListNode(0);
l9.next.next.next.next.next = new AddTwoNumbers2.ListNode(0);
l9.next.next.next.next.next.next = new AddTwoNumbers2.ListNode(0);
l9.next.next.next.next.next.next.next = new AddTwoNumbers2.ListNode(0);
l9.next.next.next.next.next.next.next.next = new AddTwoNumbers2.ListNode(0);
l9.next.next.next.next.next.next.next.next.next = new AddTwoNumbers2.ListNode(0);
l9.next.next.next.next.next.next.next.next.next.next = new AddTwoNumbers2.ListNode(0);
l9.next.next.next.next.next.next.next.next.next.next.next = new AddTwoNumbers2.ListNode(0);
l9.next.next.next.next.next.next.next.next.next.next.next.next = new AddTwoNumbers2.ListNode(0);
l9.next.next.next.next.next.next.next.next.next.next.next.next.next = new AddTwoNumbers2.ListNode(0);
l9.next.next.next.next.next.next.next.next.next.next.next.next.next.next = new AddTwoNumbers2.ListNode(0);
l9.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next = new AddTwoNumbers2.ListNode(0);
l9.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next = new AddTwoNumbers2.ListNode(0);
l9.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next = new AddTwoNumbers2.ListNode(0);
l9.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next = new AddTwoNumbers2.ListNode(0);
l9.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next = new AddTwoNumbers2.ListNode(0);
l9.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next = new AddTwoNumbers2.ListNode(0);
l9.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next = new AddTwoNumbers2.ListNode(0);
l9.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next = new AddTwoNumbers2.ListNode(0);
l9.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next = new AddTwoNumbers2.ListNode(0);
l9.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next = new AddTwoNumbers2.ListNode(0);
l9.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next = new AddTwoNumbers2.ListNode(0);
l9.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next = new AddTwoNumbers2.ListNode(0);
l9.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next = new AddTwoNumbers2.ListNode(0);
l9.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next = new AddTwoNumbers2.ListNode(0);
l9.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next = new AddTwoNumbers2.ListNode(0);
l9.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next = new AddTwoNumbers2.ListNode(1);

AddTwoNumbers2.ListNode l10 = new AddTwoNumbers2.ListNode(5);
l10.next = new AddTwoNumbers2.ListNode(6);
l10.next.next = new AddTwoNumbers2.ListNode(4);

printLinkedList(addTwoNumbers(l1, l2));// Output: [7,0,8]
printLinkedList(addTwoNumbers(l3, l4)); // Output: [0]
printLinkedList(addTwoNumbers(l5, l6)); // Output: [8,9,9,9,0,0,0,1]
printLinkedList(addTwoNumbers(l7, l8)); // Output: [0,0,0,0,0,0,0,0,0,0,1]
printLinkedList(addTwoNumbers(l9, l10)); // [6,6,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1]
}

public static class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
}

21. 合并两个有序链表 <easy>

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
95
96
97
98
99
/*将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

示例 1:


输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
示例 2:

输入:l1 = [], l2 = []
输出:[]
示例 3:

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


提示:

两个链表的节点数目范围是 [0, 50]
-100 <= Node.val <= 100
l1 和 l2 均按 非递减顺序 排列*/

/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
public class MergeTwoSortedLists21 {
public static ListNode mergeTwoLists(ListNode list1, ListNode list2) {
// 先构建一个假节点作为起点
ListNode result = new ListNode(-1);
// current指向当前节点
ListNode current = result;

while (list1 != null && list2 != null) {
// 两个链表都不为空时
if (list1.val <= list2.val) {
// 指向小的节点
current.next = list1;
list1 = list1.next;
} else {
current.next = list2;
list2 = list2.next;
}
current = current.next;
}

// 若还有链表不为空,则指向该链表
current.next = (list1 != null) ? list1 : list2;

// O(m + n)
return result.next;
}

// Helper method to print the linked list
public void printList(ListNode head) {
while (head != null) {
System.out.print(head.val + " ");
head = head.next;
}
System.out.println();
}

public static void main(String[] args) {
MergeTwoSortedLists21 merger = new MergeTwoSortedLists21();

// Example 1
ListNode l1 = new ListNode(1, new ListNode(2, new ListNode(4)));
ListNode l2 = new ListNode(1, new ListNode(3, new ListNode(4)));
ListNode result1 = merger.mergeTwoLists(l1, l2);
merger.printList(result1);

// Example 2
ListNode l3 = null;
ListNode l4 = null;
ListNode result2 = merger.mergeTwoLists(l3, l4);
merger.printList(result2);

// Example 3
ListNode l5 = null;
ListNode l6 = new ListNode(0);
ListNode result3 = merger.mergeTwoLists(l5, l6);
merger.printList(result3);
}

public static class ListNode {
int val;
MergeTwoSortedLists21.ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, MergeTwoSortedLists21.ListNode next) { this.val = val; this.next = next; }
}
}

138. 随机链表的复制 <medium> Map提前生成复制节点

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136

/*给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。

构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。

例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。

返回复制链表的头节点。

用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:

val:一个表示 Node.val 的整数。
random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为 null 。
你的代码 只 接受原链表的头节点 head 作为传入参数。

示例 1:

输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
示例 2:

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

输入:head = [[3,null],[3,0],[3,null]]
输出:[[3,null],[3,0],[3,null]]

提示:

0 <= n <= 1000
-104 <= Node.val <= 104
Node.random 为 null 或指向链表中的节点。*/
public class CopyListWithRandomPointer138 {


public Node copyRandomList(Node head) {
if (head == null) return null;

// 通过一个map来记录原链表节点和复制链表的映射关系
Map<Node, Node> map = new HashMap<>();

// 遍历链表,将复制节点存入map
Node current = head;
while (current != null) {
map.put(current, new Node(current.val));
current = current.next;
}

// 再次遍历链表,
current = head;
while (current != null) {
// 连接复制节点的next和random,都从map中找到对应原节点的复制节点
map.get(current).next = map.get(current.next);
map.get(current).random = map.get(current.random);
current = current.next;
}

return map.get(head);
}

public void printRandomList(Node head) {
Node current = head;
while (current != null) {
System.out.print("[" + current.val + ", ");
if (current.random != null) {
System.out.print(current.random.val + "]");
} else {
System.out.print("null]");
}
if (current.next != null) {
System.out.print(" -> ");
}
current = current.next;
}
System.out.println();
}

public static void main(String[] args) {
CopyListWithRandomPointer138 copier = new CopyListWithRandomPointer138();

// Example 1
CopyListWithRandomPointer138.Node node1 = copier.new Node(7);
CopyListWithRandomPointer138.Node node2 = copier.new Node(13);
CopyListWithRandomPointer138.Node node3 = copier.new Node(11);
CopyListWithRandomPointer138.Node node4 = copier.new Node(10);
CopyListWithRandomPointer138.Node node5 = copier.new Node(1);

node1.next = node2;
node2.next = node3;
node3.next = node4;
node4.next = node5;

node1.random = null;
node2.random = node1;
node3.random = node5;
node4.random = node3;
node5.random = node1;

CopyListWithRandomPointer138.Node result1 = copier.copyRandomList(node1);
copier.printRandomList(result1);

// Example 2
CopyListWithRandomPointer138.Node node6 = copier.new Node(1);
CopyListWithRandomPointer138.Node node7 = copier.new Node(2);
node6.next = node7;
node6.random = node7;
node7.random = node7;

CopyListWithRandomPointer138.Node result2 = copier.copyRandomList(node6);
copier.printRandomList(result2);

// Example 3
CopyListWithRandomPointer138.Node node8 = copier.new Node(3);
CopyListWithRandomPointer138.Node node9 = copier.new Node(3);
CopyListWithRandomPointer138.Node node10 = copier.new Node(3);
node8.next = node9;
node9.next = node10;
node9.random = node8;

CopyListWithRandomPointer138.Node result3 = copier.copyRandomList(node8);
copier.printRandomList(result3);
}

public class Node {
int val;
Node next;
Node random;

public Node(int val) {
this.val = val;
this.next = null;
this.random = null;
}
}
}

92. 反转链表 II <medium>

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
/*给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。

示例 1:

输入:head = [1,2,3,4,5], left = 2, right = 4
输出:[1,4,3,2,5]
示例 2:

输入:head = [5], left = 1, right = 1
输出:[5]

提示:

链表中节点数目为 n
1 <= n <= 500
-500 <= Node.val <= 500
1 <= left <= right <= n*/

import java.util.ArrayDeque;
import java.util.Deque;

/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
public class ReverseLinkedListII92 {

public ListNode reverseBetween(ListNode head, int left, int right) {
if (left == right) {
return head;
}

Deque<ListNode> stack = new ArrayDeque<>();
ListNode current = head;
// left前一个节点
ListNode last = head;
// right后一个节点
ListNode end = head;
int i = 1;
while (i <= right) {
if (i < left) {
// i指向left前最后一个节点
last = current;
} else if (i >= left && i < right) {
// 将i在区域内的节点入栈
stack.push(current);
} else if (i == right) {
// 到达右节点
stack.push(current);
end = current.next;
}
current = current.next;
i++;
}

if (left == 1) {
// 从head开始反转时,要更新head
head = stack.pop();
last = head;
}

while (!stack.isEmpty()) {
last.next = stack.pop();
last = last.next;
}

if (end != head) {
last.next = end;
}

return head;
}


public void printList(ReverseLinkedListII92.ListNode head) {
while (head != null) {
System.out.print(head.val + " ");
head = head.next;
}
System.out.println();
}

public static void main(String[] args) {
ReverseLinkedListII92 test = new ReverseLinkedListII92();

ListNode l1 = new ListNode(1, new ListNode(2, new ListNode(3, new ListNode(4, new ListNode(5)))));
test.printList(test.reverseBetween(l1, 2, 4)); // [1,4,3,2,5]

ListNode l2 = new ListNode(5);
test.printList(test.reverseBetween(l2, 1, 1)); // [5]

ListNode l3 = new ListNode(3, new ListNode(5));
test.printList(test.reverseBetween(l3, 1, 2)); // [5,3]
}

public static class ListNode {
int val;
ReverseLinkedListII92.ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ReverseLinkedListII92.ListNode next) { this.val = val; this.next = next; }

@Override
public String toString() {
return val + "";
}
}
}

25. K 个一组翻转链表 <hard>

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
/*给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。

k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。



示例 1:


输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]
示例 2:



输入:head = [1,2,3,4,5], k = 3
输出:[3,2,1,4,5]


提示:
链表中的节点数目为 n
1 <= k <= n <= 5000
0 <= Node.val <= 1000


进阶:你可以设计一个只用 O(1) 额外内存空间的算法解决此问题吗?*/
public class ReverseNodesInKGroup25 {

public ListNode reverseKGroup(ListNode head, int k) {
// 假节点作为首节点
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode prev = dummy;

while (head != null) {
ListNode tail = prev;
// 检测是否有k个可以反转的节点,tail会指向k后的节点
for (int i = 0; i < k; i++) {
tail = tail.next;
if (tail == null) {
// 不足k
return dummy.next;
}
}

ListNode nextGroup = tail.next;
// 反转当前组head到tail之间的k个节点
ListNode[] reversed = reverse(head, tail);
head = reversed[0];
tail = reversed[1];

// 将反转组和链表剩余的节点连接
prev.next = head;
tail.next = nextGroup;

// 指向后面的节点
prev = tail;
head = nextGroup;
}

// O(n) O(1)
return dummy.next;
}

private ListNode[] reverse(ListNode head, ListNode tail) {
// 反转head到tail之间的节点,并返回两个节点
// A B C D E head = A tail = D
ListNode prev = tail.next; // E
ListNode curr = head; // A
while (prev != tail) {
ListNode nextTemp = curr.next; // B
// A 指向 E
curr.next = prev;
// A作为新的尾节点
prev = curr;
// B作为新的头节点
curr = nextTemp;
}
return new ListNode[] { tail, head };
}

public static void main(String[] args) {
ReverseNodesInKGroup25 solution = new ReverseNodesInKGroup25();

// Test Case 1
ListNode head1 = new ListNode(1, new ListNode(2, new ListNode(3, new ListNode(4, new ListNode(5)))));
int k1 = 2;
ListNode result1 = solution.reverseKGroup(head1, k1);
printList(result1); // Output: 2 1 4 3 5

// Test Case 2
ListNode head2 = new ListNode(1, new ListNode(2, new ListNode(3, new ListNode(4, new ListNode(5)))));
int k2 = 3;
ListNode result2 = solution.reverseKGroup(head2, k2);
printList(result2); // Output: 3 2 1 4 5
}

private static void printList(ListNode head) {
while (head != null) {
System.out.print(head.val + " ");
head = head.next;
}
System.out.println();
}

public static class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }

@Override
public String toString() {
return val + "";
}
}
}

19. 删除链表的倒数第 N 个结点 <medium>

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
/*给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

示例 1:

输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
示例 2:

输入:head = [1], n = 1
输出:[]
示例 3:

输入:head = [1,2], n = 1
输出:[1]

提示:

链表中结点的数目为 sz
1 <= sz <= 30
0 <= Node.val <= 100
1 <= n <= sz*/
public class RemoveNthNodeFromEndOfList19 {
public ListNode removeNthFromEnd(ListNode head, int n) {
//// 假节点作为首节点
//ListNode dummy = new ListNode(0);
//dummy.next = head;
//ListNode curr = head;
//
//// 将节点都入栈
//Deque<ListNode> stack = new ArrayDeque<>();
//while (curr != null) {
// stack.push(curr);
// curr = curr.next;
//}
//
//int i = 1;
//ListNode prev = curr;
//while (i <= n && !stack.isEmpty()) {
// curr = stack.pop();
// if (i == n) {
// // 到达删除节点
// if (stack.isEmpty()) {
// // head为要删除节点
// return curr.next;
// } else {
// // 中间节点为删除节点
// ListNode next = stack.pop();
// next.next = prev;
// }
// }
// prev = curr;
// i++;
//}
//
//// O(n)
//return dummy.next;

// 使用快慢指针遍历链表
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode slow = dummy;
ListNode fast = dummy;

// 快指针先移动n个节点
for (int i = 0; i <= n; i++) {
if (fast == null) {
return head;
}
fast = fast.next;
}

// 一起移动两个指针,直到快指针到达终点
while (fast != null) {
slow = slow.next;
fast = fast.next;
}

// 慢指针指向的下个节点即为要删除的节点
slow.next = slow.next.next;

// O(n)
return dummy.next;
}

public static void main(String[] args) {
RemoveNthNodeFromEndOfList19 solution = new RemoveNthNodeFromEndOfList19();

ListNode head1 = new ListNode(1, new ListNode(2, new ListNode(3, new ListNode(4, new ListNode(5)))));
int n1 = 2;
ListNode result1 = solution.removeNthFromEnd(head1, n1);
printList(result1); // Output: [1,2,3,5]

ListNode head2 = new ListNode(1);
int n2 = 1;
ListNode result2 = solution.removeNthFromEnd(head2, n2);
printList(result2); // Output: []

ListNode head3 = new ListNode(1, new ListNode(2));
int n3 = 1;
ListNode result3 = solution.removeNthFromEnd(head3, n3);
printList(result3); // Output: [1]

ListNode head4 = new ListNode(1, new ListNode(2));
int n4 = 2;
ListNode result4 = solution.removeNthFromEnd(head4, n4);
printList(result4); // Output: [2]

}

private static void printList(ListNode head) {
while (head != null) {
System.out.print(head.val + " ");
head = head.next;
}
System.out.println();
}

public static class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }

@Override
public String toString() {
return val + "";
}
}
}

82. 删除排序链表中的重复元素 II <medium>

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
/*给定一个已排序的链表的头 head , 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表 。

示例 1:

输入:head = [1,2,3,3,4,4,5]
输出:[1,2,5]
示例 2:

输入:head = [1,1,1,2,3]
输出:[2,3]

提示:

链表中节点数目在范围 [0, 300] 内
-100 <= Node.val <= 100
题目数据保证链表已经按升序 排列*/
public class RemoveDuplicatesFromSortedListII82 {
public ListNode deleteDuplicates(ListNode head) {
if (head == null || head.next == null) {
return head;
}
// 使用快慢指针遍历链表
ListNode dummy = new ListNode(0);
dummy.next = head;
// 指向重复节点前的节点
ListNode prev = dummy;
// 指向重复节点的首节点
ListNode slow = head;
// 指向重复节点的尾节点
ListNode fast = head.next;

while (fast != null) {
int count = 0;
while (fast != null && fast.val == slow.val) {
fast = fast.next;
count++;
}

if (count > 0) {
// 有重复节点,删除重复节点,prev不变
prev.next = fast;
slow = fast;
if (fast != null) {
fast = fast.next;
}
} else {
// 无重复节点
prev = prev.next;
fast = fast.next;
slow = slow.next;
}
}
// O(n)
return dummy.next;
}

public static void main(String[] args) {
RemoveDuplicatesFromSortedListII82 solution = new RemoveDuplicatesFromSortedListII82();

ListNode head1 = new ListNode(1, new ListNode(2, new ListNode(3, new ListNode(3, new ListNode(4, new ListNode(4, new ListNode(5)))))));
ListNode result1 = solution.deleteDuplicates(head1);
printList(result1); // Output: [1,2,5]

ListNode head2 = new ListNode(1, new ListNode(1, new ListNode(1, new ListNode(2, new ListNode(3)))));
ListNode result2 = solution.deleteDuplicates(head2);
printList(result2); // Output: [2,3]

ListNode head3 = new ListNode(1, new ListNode(1));
ListNode result3 = solution.deleteDuplicates(head3);
printList(result3); // Output: []
}

private static void printList(ListNode head) {
while (head != null) {
System.out.print(head.val + " ");
head = head.next;
}
System.out.println();
}

public static class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }

@Override
public String toString() {
return val + "";
}
}
}

61. 旋转链表 <medium>

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
/*给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。

示例 1:

输入:head = [1,2,3,4,5], k = 2
输出:[4,5,1,2,3]
示例 2:

输入:head = [0,1,2], k = 4
输出:[2,0,1]

提示:

链表中节点的数目在范围 [0, 500] 内
-100 <= Node.val <= 100
0 <= k <= 2 * 109*/
public class RotateList61 {
public ListNode rotateRight(ListNode head, int k) {
if (head == null || k == 0 || head.next == null) {
return head;
}

int length = 1;
ListNode tail = head;

// 计算链表长度,并找到尾节点
while (tail.next != null) {
tail = tail.next;
length++;
}

// 更新k
k %= length;
if (k == 0) {
return head;
}

// 到新头节点的距离
int stepsToNewHead = length - k;
ListNode newTail = head;
// 循环获取新的头节点和尾节点
for (int i = 1; i < stepsToNewHead; i++) {
newTail = newTail.next;
}

// 找到后更新头节点,新尾节点,旧尾节点
ListNode newHead = newTail.next;
newTail.next = null;
tail.next = head;

// O(n)
return newHead;
}

public static void main(String[] args) {
RotateList61 solution = new RotateList61();

// Test cases
ListNode head1 = new ListNode(1, new ListNode(2, new ListNode(3, new ListNode(4, new ListNode(5)))));
int k1 = 2;
ListNode rotated1 = solution.rotateRight(head1, k1);
printList(rotated1); // Output: [4,5,1,2,3]

ListNode head2 = new ListNode(0, new ListNode(1, new ListNode(2)));
int k2 = 4;
ListNode rotated2 = solution.rotateRight(head2, k2);
printList(rotated2); // Output: [2,0,1]
}


private static void printList(ListNode head) {
while (head != null) {
System.out.print(head.val + " ");
head = head.next;
}
System.out.println();
}

public static class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }

@Override
public String toString() {
return val + "";
}
}
}

86. 分隔链表 <medium>

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
/*给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。

你应当 保留 两个分区中每个节点的初始相对位置。

示例 1:

输入:head = [1,4,3,2,5,2], x = 3
输出:[1,2,2,4,3,5]
示例 2:

输入:head = [2,1], x = 2
输出:[1,2]

提示:

链表中节点的数目在范围 [0, 200] 内
-100 <= Node.val <= 100
-200 <= x <= 200*/
public class PartitionList86 {
public ListNode partition(ListNode head, int x) {
if (head == null || head.next == null) {
return head;
}

// 存储小于x的节点
ListNode beforeHead = new ListNode(0);
// 存储大于x的节点
ListNode afterHead = new ListNode(0);
ListNode before = beforeHead;
ListNode after = afterHead;

while (head != null) {
if (head.val < x) {
// 小于x放入before链表
before.next = head;
before = before.next;
} else {
// 大于x放入after链表
after.next = head;
after = after.next;
}
head = head.next;
}

// 将after链表连接到before链表
after.next = null;
before.next = afterHead.next;

// O(n)
return beforeHead.next;
}

public static void main(String[] args) {
PartitionList86 solution = new PartitionList86();

// Test cases
ListNode head1 = new ListNode(1, new ListNode(4, new ListNode(3, new ListNode(2, new ListNode(5, new ListNode(2))))));
int x1 = 3;
ListNode partitioned1 = solution.partition(head1, x1);
printList(partitioned1); // Output: [1,2,2,4,3,5]

ListNode head2 = new ListNode(2, new ListNode(1));
int x2 = 2;
ListNode partitioned2 = solution.partition(head2, x2);
printList(partitioned2); // Output: [1,2]
}


private static void printList(ListNode head) {
while (head != null) {
System.out.print(head.val + " ");
head = head.next;
}
System.out.println();
}

public static class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }

@Override
public String toString() {
return val + "";
}
}
}

146. LRU 缓存 <medium>

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
/*请你设计并实现一个满足  LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache 类:
LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。
函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。

示例:

输入
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]

解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1); // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2); // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1); // 返回 -1 (未找到)
lRUCache.get(3); // 返回 3
lRUCache.get(4); // 返回 4


提示:

1 <= capacity <= 3000
0 <= key <= 10000
0 <= value <= 105
最多调用 2 * 105 次 get 和 put*/
public class LruCache146 {
class Node {
int key;
int value;
Node prev;
Node next;

public Node(int key, int value) {
this.key = key;
this.value = value;
}
}

private final int capacity;
// 存储键值对
private final Map<Integer, Node> map;
// 使用双向链表维护节点的顺序,头节点 head 代表最近最少使用的节点,尾节点 tail 是最近使用的节点
private final Node head;
private final Node tail;

public LruCache146(int capacity) {
this.capacity = capacity;
this.map = new HashMap<>();
this.head = new Node(-1, -1);
this.tail = new Node(-1, -1);
head.next = tail;
tail.prev = head;
}

public int get(int key) {
// 如果 key 不存在于哈希表中,返回 -1。
if (!map.containsKey(key)) {
return -1;
}

// 如果 key 存在于哈希表中,获取节点,将其移动到链表头部(表示最近使用过)并返回值。
Node node = map.get(key);
removeNode(node);
addToFront(node);

return node.value;
}

public void put(int key, int value) {
if (map.containsKey(key)) {
//如果 key 存在于哈希表中,则更新节点值,将其移动到链表头部。
Node node = map.get(key);
node.value = value;
removeNode(node);
addToFront(node);
} else {
//如果 key 不存在于哈希表中:
if (map.size() == capacity) {
//如果当前缓存已满,则删除链表尾部节点(表示最久未使用),从哈希表中删除对应项。
Node lastNode = tail.prev;
removeNode(lastNode);
map.remove(lastNode.key);
}

//创建新节点并添加到链表头部,并在哈希表中添加对应项。
Node newNode = new Node(key, value);
addToFront(newNode);
map.put(key, newNode);
}
}

private void removeNode(Node node) {
// 删除节点
Node prevNode = node.prev;
Node nextNode = node.next;
prevNode.next = nextNode;
nextNode.prev = prevNode;
}

private void addToFront(Node node) {
// 将节点加到头部
Node nextNode = head.next;
head.next = node;
node.prev = head;
node.next = nextNode;
nextNode.prev = node;
}
}

/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache obj = new LRUCache(capacity);
* int param_1 = obj.get(key);
* obj.put(key,value);
*/

3.9 二叉树 Binary Tree

  1. 递归算法
  2. 几种遍历二叉树的方式
    • 前序、中序、后序:非递归使用栈
    • 层级遍历:非递归使用链表

104. 二叉树的最大深度 <easy>

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
/*给定一个二叉树 root ,返回其最大深度。

二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。

示例 1:

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

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

提示:

树中节点的数量在 [0, 104] 区间内。
-100 <= Node.val <= 100*/
public class MaximumDepthOfBinaryTree104 {
public int maxDepth(TreeNode root) {
// 1.递归实现
//if (root == null) {
// return 0; // 如果根节点为空,深度为0
//} else {
// // 计算左子树和右子树的深度
// int leftDepth = maxDepth(root.left);
// int rightDepth = maxDepth(root.right);
//
// // 返回较大的深度再加上根节点的高度(即 1)
// return Math.max(leftDepth, rightDepth) + 1;
//}
// 2.非递归实现
if (root == null) {
return 0; // 如果根节点为空,深度为0
}

int depth = 0;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);

while (!queue.isEmpty()) {
int size = queue.size(); // 当前层的节点数
depth++; // 每遍历一层深度加一

// 遍历当前层的所有节点,并将下一层节点加入队列
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
}

return depth;
}

public static void main(String[] args) {
MaximumDepthOfBinaryTree104 solution = new MaximumDepthOfBinaryTree104();

// 创建示例二叉树
MaximumDepthOfBinaryTree104.TreeNode root = solution.new TreeNode(3);
root.left = solution.new TreeNode(9);
root.right = solution.new TreeNode(20);
root.right.left = solution.new TreeNode(15);
root.right.right = solution.new TreeNode(7);

// 计算最大深度并打印结果
int depth = solution.maxDepth(root);
System.out.println("Maximum Depth of Binary Tree: " + depth);
}

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;
}
}
}

100. 相同的树 <easy>

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
/*给你两棵二叉树的根节点 p 和 q ,编写一个函数来检验这两棵树是否相同。

如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。

示例 1:

输入:p = [1,2,3], q = [1,2,3]
输出:true
示例 2:

输入:p = [1,2], q = [1,null,2]
输出:false
示例 3:

输入:p = [1,2,1], q = [1,1,2]
输出:false

提示:

两棵树上的节点数目都在范围 [0, 100] 内
-104 <= Node.val <= 104*/
public class SameTree100 {
public boolean isSameTree(TreeNode p, TreeNode q) {
if (p == null || q == null) {
return p == q;
}
Deque<TreeNode> stack1 = new ArrayDeque<>();
Deque<TreeNode> stack2 = new ArrayDeque<>();
stack1.push(p);
stack2.push(q);

// 前序遍历二叉树p,对比同结构q
while (!stack1.isEmpty()) {
TreeNode node1 = stack1.pop();
TreeNode node2 = stack2.pop();
if (node1.val != node2.val) {
return false;
}

if (node1.right != null) {
if (node2.right == null) {
return false;
} else {
stack1.push(node1.right);
stack2.push(node2.right);
}
} else if (node2.right != null) {
stack2.push(node2.right);
}

if (node1.left != null) {
if (node2.left == null) {
return false;
} else {
stack1.push(node1.left);
stack2.push(node2.left);
}
} else if (node2.left != null) {
stack2.push(node2.left);
}
}

// 遍历完后如果q还有节点,则不一致
return stack2.isEmpty();
}

public static void main(String[] args) {
SameTree100 solution = new SameTree100();

SameTree100.TreeNode p1 = solution.new TreeNode(1);
p1.left = solution.new TreeNode(2);
p1.right = solution.new TreeNode(3);
SameTree100.TreeNode q1 = solution.new TreeNode(1);
q1.left = solution.new TreeNode(2);
q1.right = solution.new TreeNode(3);

System.out.println(solution.isSameTree(p1, q1)); // true

SameTree100.TreeNode p2 = solution.new TreeNode(1);
p2.left = solution.new TreeNode(2);
SameTree100.TreeNode q2 = solution.new TreeNode(1);
q2.right = solution.new TreeNode(2);

System.out.println(solution.isSameTree(p2, q2)); // false

SameTree100.TreeNode p3 = solution.new TreeNode(1);
p3.left = solution.new TreeNode(2);
p3.right = solution.new TreeNode(1);
SameTree100.TreeNode q3 = solution.new TreeNode(1);
p3.left = solution.new TreeNode(1);
q3.right = solution.new TreeNode(2);

System.out.println(solution.isSameTree(p3, q3)); // false

SameTree100.TreeNode p4 = solution.new TreeNode(1);
SameTree100.TreeNode q4 = solution.new TreeNode(1);
q4.right = solution.new TreeNode(2);

System.out.println(solution.isSameTree(p4, q4)); // false
}

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;
}
}
}

226. 翻转二叉树 <easy>

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
/*给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。

示例 1:

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

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

输入:root = []
输出:[]

提示:

树中节点数目范围在 [0, 100] 内
-100 <= Node.val <= 100*/
public class InvertBinaryTree226 {
public TreeNode invertTree(TreeNode root) {
// 遍历二叉树,将左右子节点互换
if (root == null) {
return root;
}

Deque<TreeNode> stack = new ArrayDeque<>();
stack.push(root);

while (!stack.isEmpty()) {
TreeNode node = stack.pop();
TreeNode temp = node.left;
node.left = node.right;
node.right = temp;
if (node.right != null) {
stack.push(node.right);
}
if (node.left != null) {
stack.push(node.left);
}
}
return root;
}

public static void main(String[] args) {
InvertBinaryTree226 solution = new InvertBinaryTree226();

TreeNode p1 = solution.new TreeNode(4);
p1.left = solution.new TreeNode(2);
p1.right = solution.new TreeNode(7);
p1.left.left = solution.new TreeNode(1);
p1.left.right = solution.new TreeNode(3);
p1.right.left = solution.new TreeNode(6);
p1.right.right = solution.new TreeNode(9);


printTree(solution.invertTree(p1)); // [4,7,2,9,6,3,1]
System.out.println();
TreeNode p2 = solution.new TreeNode(2);
p2.left = solution.new TreeNode(1);
p2.right = solution.new TreeNode(3);

printTree(solution.invertTree(p2)); // [2,3,1]
System.out.println();

TreeNode p3 = null;

printTree(solution.invertTree(p3)); // []
System.out.println();
}

public static void printTree(TreeNode root) {
if (root == null) {
return;
}
System.out.print(root.val + " ");
printTree(root.left);
printTree(root.right);
}

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;
}
}
}

101. 对称二叉树 <easy>

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
/*给你一个二叉树的根节点 root , 检查它是否轴对称。

示例 1:

输入:root = [1,2,2,3,4,4,3]
输出:true
示例 2:

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

提示:

树中节点数目在范围 [1, 1000] 内
-100 <= Node.val <= 100

进阶:你可以运用递归和迭代两种方法解决这个问题吗?*/
public class SymmetricTree101 {
public boolean isSymmetric(TreeNode root) {
// 递归实现
if (root == null) {
return true;
}
// O(n),空间复杂度是 O(logN) 到 O(N)
return isMirror(root.left, root.right);
}

private boolean isMirror(TreeNode left, TreeNode right) {
if (left == null && right == null) {
return true;
}
if (left == null || right == null) {
return false;
}
return (left.val == right.val) &&
isMirror(left.left, right.right) &&
isMirror(left.right, right.left);
}


public static void main(String[] args) {
SymmetricTree101 solution = new SymmetricTree101();

TreeNode p1 = solution.new TreeNode(1);
p1.left = solution.new TreeNode(2);
p1.right = solution.new TreeNode(2);
p1.left.left = solution.new TreeNode(3);
p1.left.right = solution.new TreeNode(4);
p1.right.left = solution.new TreeNode(4);
p1.right.right = solution.new TreeNode(3);
System.out.println(solution.isSymmetric(p1)); // true

TreeNode p2 = solution.new TreeNode(1);
p2.left = solution.new TreeNode(2);
p2.right = solution.new TreeNode(2);
p2.left.right = solution.new TreeNode(3);
p2.right.right = solution.new TreeNode(3);
System.out.println(solution.isSymmetric(p2)); // false
}

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;
}
}
}

105. 从前序与中序遍历序列构造二叉树 <medium>

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
95
96
97
98
99
100
/*给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。

示例 1:

输入: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
输出: [3,9,20,null,null,15,7]
示例 2:

输入: preorder = [-1], inorder = [-1]
输出: [-1]

提示:

1 <= preorder.length <= 3000
inorder.length == preorder.length
-3000 <= preorder[i], inorder[i] <= 3000
preorder 和 inorder 均 无重复 元素
inorder 均出现在 preorder
preorder 保证 为二叉树的前序遍历序列
inorder 保证 为二叉树的中序遍历序列*/
public class ConstructBinaryTreeFromPreorderAndInorderTraversal105 {

public TreeNode buildTree(int[] preorder, int[] inorder) {
if (preorder.length == 0 || inorder.length == 0) {
return null;
}
// 递归实现
return build(preorder, 0, preorder.length - 1, inorder, 0, inorder.length - 1);
}

private TreeNode build(int[] preorder, int preStart, int preEnd,
int[] inorder, int inStart, int inEnd) {
if (preStart > preEnd || inStart > inEnd) {
return null;
}

// 获取当前根节点
int rootValue = preorder[preStart];
TreeNode root = new TreeNode(rootValue);

int rootIndex = 0;
// 遍历中序数组,获取当前根节点下标
for (int i = inStart; i <= inEnd; i++) {
if (inorder[i] == rootValue) {
rootIndex = i;
break;
}
}

// 左子树的大小 = 根节点下标 - 起始下标
int leftTreeSize = rootIndex - inStart;

// 左子树 前序数组前leftTreeSize截取到,中序数组更新结束下标为根节点前
root.left = build(preorder, preStart + 1, preStart + leftTreeSize,
inorder, inStart, rootIndex - 1);
// 右子树 前序数组leftTreeSize后,中序数组更新开始下标为根节点后
root.right = build(preorder, preStart + leftTreeSize + 1, preEnd,
inorder, rootIndex + 1, inEnd);

return root;
}

public static void main(String[] args) {
ConstructBinaryTreeFromPreorderAndInorderTraversal105 solution = new ConstructBinaryTreeFromPreorderAndInorderTraversal105();

int[] preorder1 = {3, 9, 20, 15, 7};
int[] inorder1 = {9, 3, 15, 20, 7};
TreeNode result1 = solution.buildTree(preorder1, inorder1);
solution.printTree(result1); // Output: [3,9,20,null,null,15,7]

System.out.println();
int[] preorder2 = {-1};
int[] inorder2 = {-1};
TreeNode result2 = solution.buildTree(preorder2, inorder2);
solution.printTree(result2); // Output: [-1]
}

private void printTree(TreeNode root) {
if (root == null) {
System.out.print("null ");
return;
}
System.out.print(root.val + " ");
printTree(root.left);
printTree(root.right);
}

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;
}
}
}

106. 从中序与后序遍历序列构造二叉树 <medium>

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
95
96
/*给定两个整数数组 inorder 和 postorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树 。

示例 1:

输入:inorder = [9,3,15,20,7], postorder = [9,15,7,20,3]
输出:[3,9,20,null,null,15,7]
示例 2:

输入:inorder = [-1], postorder = [-1]
输出:[-1]

提示:

1 <= inorder.length <= 3000
postorder.length == inorder.length
-3000 <= inorder[i], postorder[i] <= 3000
inorder 和 postorder 都由 不同 的值组成
postorder 中每一个值都在 inorder 中
inorder 保证是树的中序遍历
postorder 保证是树的后序遍历*/
public class ConstructBinaryTreeFromInorderAndPostorderTraversal106 {

public TreeNode buildTree(int[] inorder, int[] postorder) {
if (inorder.length != postorder.length || inorder.length == 0) {
return null;
}

// 遍历中序数组,存储根节点与下标的对应关系
Map<Integer, Integer> indexMap = new HashMap<>();
for (int i = 0; i < inorder.length; i++) {
indexMap.put(inorder[i], i);
}

// 递归实现
return build(inorder, postorder, 0, inorder.length - 1, 0, postorder.length - 1, indexMap);
}

private TreeNode build(int[] inorder, int[] postorder, int inStart, int inEnd, int postStart, int postEnd, Map<Integer, Integer> indexMap) {
if (inStart > inEnd || postStart > postEnd) {
return null;
}

// 后序遍历的最后一个元素是树的根节点,通过这个根节点可以将中序遍历数组分成左右子树的部分
int rootValue = postorder[postEnd];
TreeNode root = new TreeNode(rootValue);

// 左子树的大小 = 根节点下标 - 起始下标
int rootIndex = indexMap.get(rootValue);
int leftSubtreeSize = rootIndex - inStart;

// 左子树 后序数组前leftTreeSize截取到,中序数组更新结束下标为根节点前
root.left = build(inorder, postorder, inStart, rootIndex - 1, postStart, postStart + leftSubtreeSize - 1, indexMap);
// 右子树 后序数组leftTreeSize后,中序数组更新开始下标为根节点后
root.right = build(inorder, postorder, rootIndex + 1, inEnd, postStart + leftSubtreeSize, postEnd - 1, indexMap);

return root;
}

public static void main(String[] args) {
ConstructBinaryTreeFromInorderAndPostorderTraversal106 solution = new ConstructBinaryTreeFromInorderAndPostorderTraversal106();

int[] preorder1 = {9,3,15,20,7};
int[] inorder1 = {9,15,7,20,3};
TreeNode result1 = solution.buildTree(preorder1, inorder1);
solution.printTree(result1); // Output: [3,9,20,null,null,15,7]

System.out.println();
int[] preorder2 = {-1};
int[] inorder2 = {-1};
TreeNode result2 = solution.buildTree(preorder2, inorder2);
solution.printTree(result2); // Output: [-1]
}

private void printTree(TreeNode root) {
if (root == null) {
System.out.print("null ");
return;
}
System.out.print(root.val + " ");
printTree(root.left);
printTree(root.right);
}

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;
}
}
}

117. 填充每个节点的下一个右侧节点指针 II <medium>

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
/*给定一个二叉树:

struct Node {
int val;
Node *left;
Node *right;
Node *next;
}
填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL 。

初始状态下,所有 next 指针都被设置为 NULL 。

示例 1:

输入:root = [1,2,3,4,5,null,7]
输出:[1,#,2,3,#,4,5,7,#]
解释:给定二叉树如图 A 所示,你的函数应该填充它的每个 next 指针,以指向其下一个右侧节点,如图 B 所示。序列化输出按层序遍历顺序(由 next 指针连接),'#' 表示每层的末尾。
示例 2:

输入:root = []
输出:[]

提示:

树中的节点数在范围 [0, 6000] 内
-100 <= Node.val <= 100
进阶:

你只能使用常量级额外空间。
使用递归解题也符合要求,本题中递归程序的隐式栈空间不计入额外空间复杂度。*/
public class PopulatingNextRightPointersInEachNodeII117 {

public Node connect(Node root) {
// 1.非递归,空间O(n)
//if (root == null) {
// return null;
//}
//
//// 存放同层节点
//Queue<Node> queue = new LinkedList<>();
//queue.offer(root);
//
//while (!queue.isEmpty()) {
// // 本层节点数
// int levelSize = queue.size();
// for (int i = 0; i < levelSize; i++) {
// Node node = queue.poll();
//
// // 有右边节点,则更新next节点
// if (i < levelSize - 1) {
// node.next = queue.peek();
// }
//
// // 先左后右将下层节点分别放入链表
// if (node.left != null) {
// queue.offer(node.left);
// }
// if (node.right != null) {
// queue.offer(node.right);
// }
// }
//}
//
//// O(n) O(n)
//return root;
// 2.非递归
// 指针指向同层开始节点
Node levelStart = root;

while (levelStart != null) {
Node current = levelStart;
// 虚拟节点,作为下一层节点的前一个节点,串联起同层的所有节点
Node dummy = new Node(0);
Node prev = dummy;

while (current != null) {
// 沿着current的子节点,横向连接
if (current.left != null) {
// 有左节点,则prev更新为left
prev.next = current.left;
prev = prev.next;
}

if (current.right != null) {
// 有右节点,连接后更新为右节点
prev.next = current.right;
prev = prev.next;
}

// current也更新为同层后一个节点
current = current.next;
}

// 当前层遍历结束后,将levelStart指针指向下一层最左侧节点
levelStart = dummy.next;
}

// O(n) O(1)
return root;
}

// Testing the solution
public static void main(String[] args) {
PopulatingNextRightPointersInEachNodeII117 solution = new PopulatingNextRightPointersInEachNodeII117();

// Example 1
Node root1 = solution.new Node(1);
root1.left = solution.new Node(2);
root1.right = solution.new Node(3);
root1.left.left = solution.new Node(4);
root1.left.right = solution.new Node(5);
root1.right.right = solution.new Node(7);

Node result1 = solution.connect(root1);
System.out.println("Example 1:");
solution.printNextPointers(result1);

// Example 2
Node root2 = null;
Node result2 = solution.connect(root2);
System.out.println("\nExample 2:");
solution.printNextPointers(result2);
}

private void printNextPointers(Node root) {
while (root != null) {
Node current = root;
while (current != null) {
System.out.print(current.val + " ");
current = current.next;
}
System.out.print("# "); // Denotes the end of a level
root = root.left; // Move to the next level
}
}


public class Node {
public int val;
public Node left;
public Node right;
public Node next;

public Node() {}

public Node(int _val) {
val = _val;
}

public Node(int _val, Node _left, Node _right, Node _next) {
val = _val;
left = _left;
right = _right;
next = _next;
}
}
}

114. 二叉树展开为链表 <medium>

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
/*给你二叉树的根结点 root ,请你将它展开为一个单链表:

展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null 。
展开后的单链表应该与二叉树 先序遍历 顺序相同。

示例 1:

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

输入:root = []
输出:[]
示例 3:

输入:root = [0]
输出:[0]

提示:

树中结点数在范围 [0, 2000] 内
-100 <= Node.val <= 100*/
public class FlattenBinaryTreeToLinkedList114 {
public void flatten(TreeNode root) {
if (root == null) {
return;
}

flatten(root.left);
flatten(root.right);

// 保存右子树
TreeNode right = root.right;
// 右指针指向左子树
root.right = root.left;
// 左子树设为空
root.left = null;

// 移动到已展开的右子树的最右边节点
TreeNode current = root;
while (current.right != null) {
current = current.right;
}

// 将保存的右子树连接到已展开的左子树的最右边节点的右边
current.right = right;
}

// Test the implementation
public static void main(String[] args) {
FlattenBinaryTreeToLinkedList114 solution = new FlattenBinaryTreeToLinkedList114();

// Example 1
TreeNode root1 = solution.new TreeNode(1);
root1.left = solution.new TreeNode(2, solution.new TreeNode(3), solution.new TreeNode(4));
root1.right = solution.new TreeNode(5, null, solution.new TreeNode(6));
solution.flatten(root1);
printLinkedList(root1); // Output: 1 -> 2 -> 3 -> 4 -> 5 -> 6

// Example 2
TreeNode root2 = null;
solution.flatten(root2); // Output: null

// Example 3
TreeNode root3 = solution.new TreeNode(0);
solution.flatten(root3);
printLinkedList(root3); // Output: 0
}

// Helper function to print the linked list
private static void printLinkedList(TreeNode root) {
while (root != null) {
System.out.print(root.val + " -> ");
root = root.right;
}
System.out.println("null");
}

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;
}
}
}

112. 路径总和 <easy>

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

叶子节点 是指没有子节点的节点。

示例 1:

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

输入:root = [1,2,3], targetSum = 5
输出:false
解释:树中存在两条根节点到叶子节点的路径:
(1 --> 2): 和为 3
(1 --> 3): 和为 4
不存在 sum = 5 的根节点到叶子节点的路径。
示例 3:

输入:root = [], targetSum = 0
输出:false
解释:由于树是空的,所以不存在根节点到叶子节点的路径。

提示:

树中节点的数目在范围 [0, 5000] 内
-1000 <= Node.val <= 1000
-1000 <= targetSum <= 1000*/
public class PathSum112 {
public boolean hasPathSum(TreeNode root, int targetSum) {
if (root == null) {
return false;
}

if (root.left == null && root.right == null && root.val == targetSum) {
return true;
}

// 迭代判断二叉树是否满足条件
boolean left = hasPathSum(root.left, targetSum - root.val);
boolean right = hasPathSum(root.right, targetSum - root.val);

// 任一满足即可
return left || right;
}

public static void main(String[] args) {
PathSum112 solution = new PathSum112();

// Test cases
TreeNode root1 = new TreeNode(5);
root1.left = new TreeNode(4, new TreeNode(11, new TreeNode(7), new TreeNode(2)), null);
root1.right = new TreeNode(8, new TreeNode(13), new TreeNode(4, null, new TreeNode(1)));
int targetSum1 = 22;
System.out.println(solution.hasPathSum(root1, targetSum1)); // Output: true

TreeNode root2 = new TreeNode(1, new TreeNode(2), new TreeNode(3));
int targetSum2 = 5;
System.out.println(solution.hasPathSum(root2, targetSum2)); // Output: false

TreeNode root3 = null;
int targetSum3 = 0;
System.out.println(solution.hasPathSum(root3, targetSum3)); // Output: false
}

public static 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;
}
}
}

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

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
/*给你一个二叉树的根节点 root ,树中每个节点都存放有一个 0 到 9 之间的数字。
每条从根节点到叶节点的路径都代表一个数字:

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

叶节点 是指没有子节点的节点。

示例 1:

输入:root = [1,2,3]
输出:25
解释:
从根到叶子节点路径 1->2 代表数字 12
从根到叶子节点路径 1->3 代表数字 13
因此,数字总和 = 12 + 13 = 25
示例 2:

输入:root = [4,9,0,5,1]
输出:1026
解释:
从根到叶子节点路径 4->9->5 代表数字 495
从根到叶子节点路径 4->9->1 代表数字 491
从根到叶子节点路径 4->0 代表数字 40
因此,数字总和 = 495 + 491 + 40 = 1026

提示:

树中节点的数目在范围 [1, 1000] 内
0 <= Node.val <= 9
树的深度不超过 10*/
public class SumRootToLeafNumbers129 {

public int sumNumbers(TreeNode root) {
return dfs(root, 0);
}

private int dfs(TreeNode node, int currSum) {
if (node == null) {
return 0;
}

// 拼接当前节点数字
currSum = currSum * 10 + node.val;

// 叶子节点就返回结果数字
if (node.left == null && node.right == null) {
return currSum;
}

// 非叶子节点则递归子树
int leftSum = dfs(node.left, currSum);
int rightSum = dfs(node.right, currSum);

return leftSum + rightSum;
}

public static void main(String[] args) {
SumRootToLeafNumbers129 solution = new SumRootToLeafNumbers129();

// Test cases
TreeNode root1 = new TreeNode(1, new TreeNode(2), new TreeNode(3));
System.out.println(solution.sumNumbers(root1)); // Output: 25

TreeNode root2 = new TreeNode(4, new TreeNode(9, new TreeNode(5), new TreeNode(1)), new TreeNode(0));
System.out.println(solution.sumNumbers(root2)); // Output: 1026
}

public static 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;
}
}
}

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

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
/*二叉树中的 路径 被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。

路径和 是路径中各节点值的总和。

给你一个二叉树的根节点 root ,返回其 最大路径和 。

示例 1:

输入:root = [1,2,3]
输出:6
解释:最优路径是 2 -> 1 -> 3 ,路径和为 2 + 1 + 3 = 6
示例 2:

输入:root = [-10,9,20,null,null,15,7]
输出:42
解释:最优路径是 15 -> 20 -> 7 ,路径和为 15 + 20 + 7 = 42

提示:

树中节点数目范围是 [1, 3 * 104]
-1000 <= Node.val <= 1000*/
public class BinaryTreeMaximumPathSum124 {
int maxSum = Integer.MIN_VALUE;

public int maxPathSum(TreeNode root) {
// 递归获取最大路径和
maxPathSumHelper(root);
return maxSum;
}

private int maxPathSumHelper(TreeNode node) {
if (node == null) {
return 0;
}

// 分别获取左子树和右子树的最大和
int leftSum = Math.max(maxPathSumHelper(node.left), 0);
int rightSum = Math.max(maxPathSumHelper(node.right), 0);

// 更新当前节点的最大和
maxSum = Math.max(maxSum, node.val + leftSum + rightSum);

// 返回从当前节点开始的最大路径和
return node.val + Math.max(leftSum, rightSum);
}

public static void main(String[] args) {
BinaryTreeMaximumPathSum124 solution = new BinaryTreeMaximumPathSum124();

// Test cases
TreeNode root1 = new TreeNode(1, new TreeNode(2), new TreeNode(3));
System.out.println(solution.maxPathSum(root1)); // Output: 6

TreeNode root2 = new TreeNode(-10, new TreeNode(9), new TreeNode(20, new TreeNode(15), new TreeNode(7)));
System.out.println(solution.maxPathSum(root2)); // Output: 42
}

public static 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;
}
}
}

173. 二叉搜索树迭代器 <medium>

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
95
96
97
98
99
100
101
102
103
/*实现一个二叉搜索树迭代器类BSTIterator ,表示一个按中序遍历二叉搜索树(BST)的迭代器:
BSTIterator(TreeNode root) 初始化 BSTIterator 类的一个对象。BST 的根节点 root 会作为构造函数的一部分给出。指针应初始化为一个不存在于 BST 中的数字,且该数字小于 BST 中的任何元素。
boolean hasNext() 如果向指针右侧遍历存在数字,则返回 true ;否则返回 false 。
int next()将指针向右移动,然后返回指针处的数字。
注意,指针初始化为一个不存在于 BST 中的数字,所以对 next() 的首次调用将返回 BST 中的最小元素。

你可以假设 next() 调用总是有效的,也就是说,当调用 next() 时,BST 的中序遍历中至少存在一个下一个数字。

示例:

输入
["BSTIterator", "next", "next", "hasNext", "next", "hasNext", "next", "hasNext", "next", "hasNext"]
[[[7, 3, 15, null, null, 9, 20]], [], [], [], [], [], [], [], [], []]
输出
[null, 3, 7, true, 9, true, 15, true, 20, false]

解释
BSTIterator bSTIterator = new BSTIterator([7, 3, 15, null, null, 9, 20]);
bSTIterator.next(); // 返回 3
bSTIterator.next(); // 返回 7
bSTIterator.hasNext(); // 返回 True
bSTIterator.next(); // 返回 9
bSTIterator.hasNext(); // 返回 True
bSTIterator.next(); // 返回 15
bSTIterator.hasNext(); // 返回 True
bSTIterator.next(); // 返回 20
bSTIterator.hasNext(); // 返回 False


提示:

树中节点的数目在范围 [1, 105] 内
0 <= Node.val <= 106
最多调用 105 次 hasNext 和 next 操作


进阶:

你可以设计一个满足下述条件的解决方案吗?next() 和 hasNext() 操作均摊时间复杂度为 O(1) ,并使用 O(h) 内存。其中 h 是树的高度。*/
public class BinarySearchTreeIterator173 {
private Stack<TreeNode> stack;

public BinarySearchTreeIterator173(TreeNode root) {
// BST 的根节点 root 会作为构造函数的一部分给出。指针应初始化为一个不存在于 BST 中的数字,且该数字小于 BST 中的任何元素。
stack = new Stack<>();
leftmostInorder(root);
}

private void leftmostInorder(TreeNode node) {
// 遍历节点将左子树入栈,直到叶子节点
while (node != null) {
stack.push(node);
node = node.left;
}
}

public int next() {
// 将指针向右移动,然后返回指针处的数字。
TreeNode topNode = stack.pop();
if (topNode.right != null) {
leftmostInorder(topNode.right);
}
return topNode.val;
}

public boolean hasNext() {
// 如果向指针右侧遍历存在数字,则返回 true ;否则返回 false
return !stack.isEmpty();
}

public static void main(String[] args) {
TreeNode root = new TreeNode(7);
root.left = new TreeNode(3);
root.right = new TreeNode(15);
root.right.left = new TreeNode(9);
root.right.right = new TreeNode(20);

BinarySearchTreeIterator173 iterator = new BinarySearchTreeIterator173(root);

System.out.println(iterator.next()); // Output: 3
System.out.println(iterator.next()); // Output: 7
System.out.println(iterator.hasNext()); // Output: true
System.out.println(iterator.next()); // Output: 9
System.out.println(iterator.hasNext()); // Output: true
System.out.println(iterator.next()); // Output: 15
System.out.println(iterator.hasNext()); // Output: true
System.out.println(iterator.next()); // Output: 20
System.out.println(iterator.hasNext()); // Output: false
}

public static 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;
}
}
}

222. 完全二叉树的节点个数 <easy>

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
95
96
97
/*给你一棵 完全二叉树 的根节点 root ,求出该树的节点个数。

完全二叉树 的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2h 个节点。

示例 1:

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

输入:root = []
输出:0
示例 3:

输入:root = [1]
输出:1

提示:

树中节点的数目范围是[0, 5 * 104]
0 <= Node.val <= 5 * 104
题目数据保证输入的树是 完全二叉树

进阶:遍历树来统计节点是一种时间复杂度为 O(n) 的简单解决方案。你可以设计一个更快的算法吗?*/
public class CountCompleteTreeNodes222 {
public int countNodes(TreeNode root) {
if (root == null) {
return 0;
}

// 计算左右子树的高度
int leftHeight = getLeftHeight(root);
int rightHeight = getRightHeight(root);

// 左右子树高度相同,二叉树完全对仗,直接返回节点数为2的leftHeight次方-1
if (leftHeight == rightHeight) {
return (1 << leftHeight) - 1;
}

// 若非完全对仗,则迭代分别获取左右子节点的节点数
return 1 + countNodes(root.left) + countNodes(root.right);
}

private int getLeftHeight(TreeNode node) {
int height = 0;
while (node != null) {
height++;
node = node.left;
}
return height;
}

private int getRightHeight(TreeNode node) {
int height = 0;
while (node != null) {
height++;
node = node.right;
}
return height;
}

public static void main(String[] args) {
CountCompleteTreeNodes222 solution = new CountCompleteTreeNodes222();

// Test cases
// Example 1
TreeNode root1 = new TreeNode(1);
root1.left = new TreeNode(2);
root1.right = new TreeNode(3);
root1.left.left = new TreeNode(4);
root1.left.right = new TreeNode(5);
root1.right.left = new TreeNode(6);

System.out.println(solution.countNodes(root1)); // Output: 6

// Example 2
TreeNode root2 = null;
System.out.println(solution.countNodes(root2)); // Output: 0

// Example 3
TreeNode root3 = new TreeNode(1);
System.out.println(solution.countNodes(root3)); // Output: 1
}

public static 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;
}
}
}

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

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
/*给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

示例 1:


输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出:3
解释:节点 5 和节点 1 的最近公共祖先是节点 3 。
示例 2:


输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出:5
解释:节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可以为节点本身。
示例 3:

输入:root = [1,2], p = 1, q = 2
输出:1

提示:

树中节点数目在范围 [2, 105] 内。
-109 <= Node.val <= 109
所有 Node.val 互不相同 。
p != q
p 和 q 均存在于给定的二叉树中。*/
public class LowestCommonAncestorOfABinaryTree236 {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// DFS递归解法 O(n) O(h)
// 如果当前节点为空,或者等于其中一个目标节点,直接返回当前节点
if (root == null || root == p || root == q) {
return root;
}

// 在左右子树中寻找目标节点
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);

// 如果左右子树分别包含目标节点,返回当前节点
if (left != null && right != null) {
return root;
}

// 如果只有左子树或右子树包含目标节点,则返回找到的节点
return left != null ? left : right;
}

public static void main(String[] args) {
LowestCommonAncestorOfABinaryTree236 solution = new LowestCommonAncestorOfABinaryTree236();

// 构建测试用例
TreeNode root = new TreeNode(3);
root.left = new TreeNode(5);
root.right = new TreeNode(1);
root.left.left = new TreeNode(6);
root.left.right = new TreeNode(2);
root.right.left = new TreeNode(0);
root.right.right = new TreeNode(8);
root.left.right.left = new TreeNode(7);
root.left.right.right = new TreeNode(4);

TreeNode p = root.left;
TreeNode q = root.right;

// 找到最近公共祖先
TreeNode ancestor = solution.lowestCommonAncestor(root, p, q);
System.out.println("Lowest Common Ancestor: " + ancestor.val);
}

public static 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;
}
}
}

3.10 二叉树层次遍历 Binary Tree Level Order Traversal

199. 二叉树的右视图 <medium>

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
/*给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。

示例 1:

输入: [1,2,3,null,5,null,4]
输出: [1,3,4]
示例 2:

输入: [1,null,3]
输出: [1,3]
示例 3:

输入: []
输出: []

提示:

二叉树的节点个数的范围是 [0,100]
-100 <= Node.val <= 100*/
public class BinaryTreeRightSideView199 {
public List<Integer> rightSideView(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) return result;

// 通过链表进行层级遍历
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);

while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
if (i == size - 1) {
// 添加最右侧节点的值
result.add(node.val);
}

// 将下层子节点加入链表
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
}

return result;
}

public static void main(String[] args) {
BinaryTreeRightSideView199 solution = new BinaryTreeRightSideView199();

TreeNode root1 = new TreeNode(1);
root1.left = new TreeNode(2, null, new TreeNode(5));
root1.right = new TreeNode(3, null, new TreeNode(4));
// 获取右视图节点值列表
List<Integer> rightView = solution.rightSideView(root1);
System.out.println("Right Side View: " + rightView); // [1,3,4]

TreeNode root2 = new TreeNode(1);
root2.right = new TreeNode(3);
System.out.println("Right Side View: " + solution.rightSideView(root2)); // [1,3]
}

public static 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;
}
}
}

637. 二叉树的层平均值 <easy>

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
/*给定一个非空二叉树的根节点 root , 以数组的形式返回每一层节点的平均值。与实际答案相差 10-5 以内的答案可以被接受。

示例 1:

输入:root = [3,9,20,null,null,15,7]
输出:[3.00000,14.50000,11.00000]
解释:第 0 层的平均值为 3,第 1 层的平均值为 14.5,第 2 层的平均值为 11 。
因此返回 [3, 14.5, 11] 。
示例 2:

输入:root = [3,9,20,15,7]
输出:[3.00000,14.50000,11.00000]

提示:

树中节点数量在 [1, 104] 范围内
-231 <= Node.val <= 231 - 1*/
public class AverageOfLevelsInBinaryTree637 {
public List<Double> averageOfLevels(TreeNode root) {
List<Double> result = new ArrayList<>();
if (root == null) {
return result;
}

Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);

while (!queue.isEmpty()) {
int size = queue.size();
double average = 0.0;
double sum = 0.0;
for (int i = 0;i < size;i++) {
TreeNode node = queue.poll();
sum += node.val;

if (i == size - 1) {
average = sum / size;
}
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
result.add(average);
}

return result;
}

public static void main(String[] args) {
AverageOfLevelsInBinaryTree637 solution = new AverageOfLevelsInBinaryTree637();

TreeNode root1 = new TreeNode(3);
root1.left = new TreeNode(9);
root1.right = new TreeNode(20, new TreeNode(15), new TreeNode(7));
List<Double> rightView = solution.averageOfLevels(root1);
System.out.println("averageOfLevels: " + rightView); // [3.00000,14.50000,11.00000]

TreeNode root2 = new TreeNode(3);
root2.left = new TreeNode(9, new TreeNode(15), new TreeNode(7));
root2.right = new TreeNode(20);
System.out.println("averageOfLevels: " + solution.averageOfLevels(root2)); // [3.00000,14.50000,11.00000]
}

public static 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;
}
}
}

3.11 二叉搜索树 Binary Search Tree

3.12 图 Graph

3.13 图的广度优先搜索 Breadth-First Search (BFS) in Graphs

3.14 字典树 Trie (Prefix Tree)

3.15 回溯 Backtracking

3.16 分治 Divide and Conquer

3.17 Kadane算法 Kadane’s Algorithm

3.19 堆 Heap

3.20 位运算 Bit Manipulation

3.21 数学 Mathematics

3.22 一维动态规划 One-dimensional Dynamic Programming

3.23 多维动态规划 Multi-dimensional Dynamic Programming

问:使用O(N)复杂度完成GBDT分裂?

问:找出无序数组中相隔距离最长的逆序对?

问:给出一个rand5(),实现一个分布均匀的rand7()生成器?

问:有一个点,向北走5公里、东走10km、向南走5公里,回到原点。这样的点有几个,证明?

问:有一个n克的物体、一个天平和若干砝码,这个天平没有刻度,试问最少用多少个什么样的砝码可以将这个物体的质量称出。砝码的质量和个数自行指定。

问:实现堆排序

问:给了长度为N的有重复元素的数组,要求输出第10大的数。需要在2小时内完成

问:手写一个对象池

问:排序算法?

问:一致性 Hash 算法,一致性 Hash 算法的应用?

问:TopK 问题?

问:试着实现一下LRU算法?

四.手撕

问:一个人从起点出发,初始生命值为n,每次可以向前跳k步并消耗k点生命值,路上的每一个位置都有恢复生命值的好蘑菇,和扣除生命值的坏蘑菇,人的生命值不能为0。问该人到达终点时的最大剩余生命值是多少?如果不能到达终点,返回-1。

一开始以为这道题是道动态规划的题目,但是老想不出递推关系式。问了下是不是贪心的思路,面试官说是贪心,然后给了个例子以及一波暗示,我才终于发现其实每次都跳到加血的蘑菇位置就行了。然后赶紧写了波代码,就这样过去了

问:leetcode原题,买卖股票。给一组股票价格,只进行一次交易,最大利润是多少?如果可以进行多次交易,利润又是多少呢?

问:有大量已知的url网址,以及一个新的url网址,如何高效的判断新网址是否和已知网址重复?

答:用字典树来做,省略100字。

问:url很长,字典树很占空间怎么办?

答:对url进行分段哈希,然后再放到树节点中,而不是直接把每一个字符作为一个树节点。

问:你刚才提到了哈希,那说一下哈希的实现?

答:说了下STL里面unordered_map的大概实现原理,哈希函数,二次探测,开链,省略100字。

哈希在什么情况下需要扩容?怎么扩容?

答:说了下饱和度过大时需要扩容,每次容量翻一倍。

为什么扩容时是翻一倍呢?而不是两倍,三倍?

答:(这我就真不知道了😖,一顿乱猜)于是说了下STL内存分配器的特点以及里面内存池的结构可能更适合这样分配,才算糊弄过去😅。

服务器中有大量数据需要迁移,如何在不停止对外服务的情况下安全的迁移数据?

答:一开始没明白意思,说这个直接复制就行噻😃,

然后面试官提醒要是复制的过程中数据被改动了怎么办?

答:需要对数据加锁。

加锁了不就没法对外服务了吗?

答:可以把数据分成许多段,复制某一段的时候就把这一段数据加锁。

面试官觉得我越扯越远,就没继续问了。😌

谈一下你的职业规划?答略

问:找出数组中只出现一次的那个数,其他都出现两次;(异或)

接上面,如果数组中有两个数只出现一次呢?

问:BST中任意两个节点差的最小值

问:判断二叉树是否中心对称

问:大数相乘

问:给我一张纸,画了一个九方格,都填了数字,给一个MN矩阵,从1开始逆时针打印这MN个数,要求时间复杂度尽可能低,可以先说下思路

问:数组A,2*n个元素,n个奇数、n个偶数,设计一个算法,使得数组奇数下标位置放置的都是奇数,偶数下标位置放置的都是偶数。先说下你的思路

问:统计一个字符串的字符出现字数并打印?除了这个还有别的办法吗?

问:全排列?

问:假如你正在做淘宝的后台,如何快速查询出1000-2000价格的商品?

问:有一个场景,一个背包里,多个线程往里面移走东西如何保证线程安全?加锁加在什么地方,让你来写代码你怎么写?

问:判断数组里是否有和为指定值的两个数?

问:对笔试题进行提问?有没找到更好的解法吗?

(题目大概是一个数组里除了一个数字出现一次,其它数字都出现了两次,给一个最优解法?我说可以用map,key为数字,value为该数字出现次数?面试官给了一个异或的方法,提示两次异或是本身)

-————————————–

参考:

🔗后端面试的所有知识点