文章目录
- [[LeetCode top 1~50](https://leetcode.cn/studyplan/top-100-liked/)](#LeetCode top 1~50)
- 「哈希」
-
- [[1. 两数之和](https://leetcode.cn/problems/two-sum/)](#1. 两数之和)
- [[49. 字母异位词分组](https://leetcode.cn/problems/group-anagrams/)](#49. 字母异位词分组)
- [[128. 最长连续序列](https://leetcode.cn/problems/longest-consecutive-sequence/)](#128. 最长连续序列)
- 「双指针」
-
- [[283. 移动零](https://leetcode.cn/problems/move-zeroes/)](#283. 移动零)
- [[11. 盛最多水的容器](https://leetcode.cn/problems/container-with-most-water/)](#11. 盛最多水的容器)
- [[15. 三数之和](https://leetcode.cn/problems/3sum/)](#15. 三数之和)
- [[42. 接雨水](https://leetcode.cn/problems/trapping-rain-water/)](#42. 接雨水)
- 「滑动窗口」
-
- [[3. 无重复字符的最长子串](https://leetcode.cn/problems/longest-substring-without-repeating-characters/)](#3. 无重复字符的最长子串)
- [[438. 找到字符串中所有字母异位词](https://leetcode.cn/problems/find-all-anagrams-in-a-string/)](#438. 找到字符串中所有字母异位词)
- 「子串」
-
- [[560. 和为 K 的子数组](https://leetcode.cn/problems/subarray-sum-equals-k/)](#560. 和为 K 的子数组)
- [[239. 滑动窗口最大值](https://leetcode.cn/problems/sliding-window-maximum/)](#239. 滑动窗口最大值)
- [[76. 最小覆盖子串](https://leetcode.cn/problems/minimum-window-substring/)](#76. 最小覆盖子串)
- 「普通数组」
-
- [[53. 最大子数组和](https://leetcode.cn/problems/maximum-subarray/)](#53. 最大子数组和)
- [[56. 合并区间](https://leetcode.cn/problems/merge-intervals/)](#56. 合并区间)
- [[189. 轮转数组](https://leetcode.cn/problems/rotate-array/)](#189. 轮转数组)
- [[238. 除自身以外数组的乘积](https://leetcode.cn/problems/product-of-array-except-self/)](#238. 除自身以外数组的乘积)
- [[41. 缺失的第一个正数](https://leetcode.cn/problems/first-missing-positive/)](#41. 缺失的第一个正数)
- 「矩阵」
-
- [[73. 矩阵置零](https://leetcode.cn/problems/set-matrix-zeroes/)](#73. 矩阵置零)
- [[54. 螺旋矩阵](https://leetcode.cn/problems/spiral-matrix/)](#54. 螺旋矩阵)
- [[48. 旋转图像](https://leetcode.cn/problems/rotate-image/)](#48. 旋转图像)
- [[240. 搜索二维矩阵 II](https://leetcode.cn/problems/search-a-2d-matrix-ii/)](#240. 搜索二维矩阵 II)
- 「链表」
-
- [[160. 相交链表](https://leetcode.cn/problems/intersection-of-two-linked-lists/)](#160. 相交链表)
- [[206. 反转链表](https://leetcode.cn/problems/reverse-linked-list/)](#206. 反转链表)
- [[234. 回文链表](https://leetcode.cn/problems/palindrome-linked-list/)](#234. 回文链表)
- [[141. 环形链表](https://leetcode.cn/problems/linked-list-cycle/)](#141. 环形链表)
- [[142. 环形链表 II](https://leetcode.cn/problems/linked-list-cycle-ii/)](#142. 环形链表 II)
- [[21. 合并两个有序链表](https://leetcode.cn/problems/merge-two-sorted-lists/)](#21. 合并两个有序链表)
- [[2. 两数相加](https://leetcode.cn/problems/add-two-numbers/)](#2. 两数相加)
- [[19. 删除链表的倒数第 N 个结点](https://leetcode.cn/problems/remove-nth-node-from-end-of-list/)](#19. 删除链表的倒数第 N 个结点)
- [[24. 两两交换链表中的节点](https://leetcode.cn/problems/swap-nodes-in-pairs/)](#24. 两两交换链表中的节点)
- [[25. K 个一组翻转链表](https://leetcode.cn/problems/reverse-nodes-in-k-group/)](#25. K 个一组翻转链表)
- [[138. 随机链表的复制](https://leetcode.cn/problems/copy-list-with-random-pointer/)](#138. 随机链表的复制)
- [[148. 排序链表](https://leetcode.cn/problems/sort-list/)](#148. 排序链表)
- [[23. 合并 K 个升序链表](https://leetcode.cn/problems/merge-k-sorted-lists/)](#23. 合并 K 个升序链表)
- [[146. LRU 缓存](https://leetcode.cn/problems/lru-cache/)](#146. LRU 缓存)
- 「二叉树」
-
- [[94. 二叉树的中序遍历](https://leetcode.cn/problems/binary-tree-inorder-traversal/)](#94. 二叉树的中序遍历)
- [[104. 二叉树的最大深度](https://leetcode.cn/problems/maximum-depth-of-binary-tree/)](#104. 二叉树的最大深度)
- [[226. 翻转二叉树](https://leetcode.cn/problems/invert-binary-tree/)](#226. 翻转二叉树)
- [[101. 对称二叉树](https://leetcode.cn/problems/symmetric-tree/)](#101. 对称二叉树)
- [[543. 二叉树的直径](https://leetcode.cn/problems/diameter-of-binary-tree/)](#543. 二叉树的直径)
- [[102. 二叉树的层序遍历](https://leetcode.cn/problems/binary-tree-level-order-traversal/)](#102. 二叉树的层序遍历)
- [[108. 将有序数组转换为二叉搜索树](https://leetcode.cn/problems/convert-sorted-array-to-binary-search-tree/)](#108. 将有序数组转换为二叉搜索树)
- [[98. 验证二叉搜索树](https://leetcode.cn/problems/validate-binary-search-tree/)](#98. 验证二叉搜索树)
- [[230. 二叉搜索树中第 K 小的元素](https://leetcode.cn/problems/kth-smallest-element-in-a-bst/)](#230. 二叉搜索树中第 K 小的元素)
- [[199. 二叉树的右视图](https://leetcode.cn/problems/binary-tree-right-side-view/)](#199. 二叉树的右视图)
- [[114. 二叉树展开为链表](https://leetcode.cn/problems/flatten-binary-tree-to-linked-list/)](#114. 二叉树展开为链表)
- [[105. 从前序与中序遍历序列构造二叉树](https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal/)](#105. 从前序与中序遍历序列构造二叉树)
- [[437. 路径总和 III](https://leetcode.cn/problems/path-sum-iii/)](#437. 路径总和 III)
- [[236. 二叉树的最近公共祖先](https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/)](#236. 二叉树的最近公共祖先)
- [[124. 二叉树中的最大路径和](https://leetcode.cn/problems/binary-tree-maximum-path-sum/)](#124. 二叉树中的最大路径和)
LeetCode top 1~50

「哈希」
1. 两数之和
题目描述
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。
你可以按任意顺序返回答案。
核心思路
使用 HashMap 存储 val 到 Index 的映射。
题解代码
java
class Solution {
public int[] twoSum(int[] nums, int target) {
// 1.由于返回的是索引,所以需要用一个 Map 存储 val 到 Index 的映射
HashMap<Integer, Integer> valToIndex = new HashMap<>();
// 2.遍历迭代
for(int i = 0; i < nums.length; i++){
int need = target - nums[i];
if(valToIndex.containsKey(need)){
return new int[]{valToIndex.get(need), i};
}
valToIndex.put(nums[i], i);
}
return null;
}
}
时空分析
- 时间复杂度:(O(n))
- 空间复杂度:(O(n))
- 原因分析 :
- 时间复杂度 :代码包含一个
for循环,该循环遍历数组nums一次。在每次循环中,执行的操作如计算差值、在HashMap中查找和插入元素,平均情况下,这些操作的时间复杂度均为 (O(1))。所以总的时间复杂度为 (O(n)\times O(1)= O(n)) 。- 空间复杂度 :代码中使用了一个
HashMap来存储数组中的元素及其索引。在最坏情况下,数组中的所有元素都需要存储到HashMap中,因此空间复杂度与数组的长度 n 成正比,即 (O(n)) 。
49. 字母异位词分组
题目描述
给你一个字符串数组,请你将字母异位词组合在一起。可以按任意顺序返回结果列表。
核心思路
数据编码和 HashMap -> 找到一种编码方法,使得字母异位词的编码都相同。找到这种编码方式之后,就可以用一个哈希表存储编码相同的所有异位词,得到最终的答案。
编码方法:利用每个字符出现的次数进行编码。 "10001...000"
题解代码
java
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
// 编码到分组的映射
HashMap<String, List<String>> codeToGroup = new HashMap<>();
for (String s : strs) {
// 对字符串进行编码
String code = encode(s);
// 把编码相同的字符串放在一起
codeToGroup.putIfAbsent(code, new LinkedList<>());
codeToGroup.get(code).add(s);
}
// 获取结果
List<List<String>> res = new LinkedList<>();
for (List<String> group : codeToGroup.values()) {
res.add(group);
}
return res;
}
// 利用每个字符的出现次数进行编码
String encode(String s) {
char[] count = new char[26];
for (char c : s.toCharArray()) {
int delta = c - 'a';
count[delta]++;
}
return new String(count);
}
}
时空分析
- 时间复杂度:(O(n \cdot k))
- 空间复杂度:(O(n \cdot k))
- 原因分析 :
- 时间复杂度 :
- 外层有一个
for循环遍历strs数组,循环次数为 n,n 是strs数组的长度。- 对于
strs中的每一个字符串,都会调用encode方法。在encode方法中,又有一个for循环遍历字符串 s,假设字符串平均长度为 k,这个循环时间复杂度为 (O(k))。- 此外,在
groupAnagrams方法的for循环中,每次循环还有平均时间复杂度为 (O(1)) 的putIfAbsent和add操作。所以整体时间复杂度为 (O(n)\times O(k + 1)= O(n \cdot k)) 。- 空间复杂度 :
- 代码中使用了一个
HashMap来存储编码后的字符串及其对应的字符串列表。在最坏情况下,所有不同的编码字符串都不同,且每个编码字符串对应一个长度为 k 的字符串列表。每个编码字符串长度为 26(常数),假设平均每个编码字符串对应一个长度为 k 的字符串列表,所以HashMap占用空间为 (O(n \cdot k))。- 此外,
res列表存储所有分组后的结果,在最坏情况下,它也占用 (O(n \cdot k)) 的空间。因此,总的空间复杂度为 (O(n \cdot k)) 。
128. 最长连续序列
题目描述
给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
请你设计并实现时间复杂度为 O(n) 的算法解决此问题。
核心思路
HashSet -> 想找连续序列,首先要找到这个连续序列的开头元素,然后递增,看看之后有多少个元素还在 nums 中,即可得到最长连续序列的长度了。
题解代码
java
class Solution {
public int longestConsecutive(int[] nums) {
// 转化成哈希集合,方便快速查找是否存在某个元素
HashSet<Integer> set = new HashSet<Integer>();
for (int num : nums) {
set.add(num);
}
int res = 0;
for (int num : set) {
if (set.contains(num - 1)) {
// num 不是连续子序列的第一个,跳过
continue;
}
// num 是连续子序列的第一个,开始向上计算连续子序列的长度
int curNum = num;
int curLen = 1;
while (set.contains(curNum + 1)) {
curNum += 1;
curLen += 1;
}
// 更新最长连续序列的长度
res = Math.max(res, curLen);
}
return res;
}
}
时空分析
- 时间复杂度:(O(n))
- 空间复杂度:(O(n))
- 原因分析 :
- 时间复杂度 :
- 首先,有一个
for循环遍历数组nums并将元素添加到HashSet中,这个过程的时间复杂度为 (O(n)),因为向HashSet中添加元素平均时间复杂度是 (O(1)),总共添加 n 个元素。- 然后,又有一个
for循环遍历HashSet中的元素。在这个循环中,对于每个元素,首先检查其前驱元素是否存在,如果不存在,则从该元素开始查找连续序列的长度。虽然这里有一个while循环,但对于每个连续序列,只会从序列的起始元素进入while循环一次。例如,对于序列[1, 2, 3],只有1会进入while循环来计算这个序列的长度。因此,这个while循环的总时间复杂度也是 (O(n))。所以整体时间复杂度为 (O(n + n)= O(n)) 。- 空间复杂度 :
- 代码中使用了一个
HashSet来存储数组中的元素。在最坏情况下,数组中的所有元素都不相同,HashSet需要存储 n 个元素,所以空间复杂度为 (O(n))。
「双指针」
283. 移动零
题目描述
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
核心思路
快慢指针 -> 先移除所有 0,然后把最后的元素都置为 0,就相当于移动 0 的效果。
题解代码
java
class Solution {
public void moveZeroes(int[] nums) {
// 去除 nums 中的所有 0
// 返回去除 0 之后的数组长度
int p = removeElement(nums, 0);
// 将 p 之后的所有元素赋值为 0
for (; p < nums.length; p++) {
nums[p] = 0;
}
}
// 双指针技巧,复用 [27. 移除元素] 的解法。
int removeElement(int[] nums, int val) {
int fast = 0, slow = 0;
while (fast < nums.length) {
if (nums[fast] != val) {
nums[slow] = nums[fast];
slow++;
}
fast++;
}
return slow;
}
}
时空分析
时间复杂度:(O(n))
空间复杂度:(O(1))
原因分析:
时间复杂度:代码的核心操作集中在两个方法的循环中:
removeElement方法中,fast指针从 0 遍历到数组末尾(共 n 次,n 为数组长度),循环内操作均为常数时间 (O(1)),因此该方法时间复杂度为 (O(n));
moveZeroes方法中,除调用removeElement外,还包含一个从 (i = r) 到 (i = n-1) 的循环,该循环执行次数为 (n - r)(r 为removeElement的返回值),最多为 n 次,时间复杂度为 (O(n))。总操作次数为 (O(n) + O(n) = O(n)),忽略常数系数后,整体时间复杂度为 (O(n))。
空间复杂度 :两个方法均仅使用固定数量的临时变量(如
fast、slow、r、i),未申请额外数组或动态内存,空间占用不随输入规模 n 变化,因此空间复杂度为常数级 (O(1))。
11. 盛最多水的容器
题目描述
给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。
找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
说明: 你不能倾斜容器。
核心思路
左右指针 -> 用 left 和 right 两个指针从两端向中心收缩,一边收缩一边计算 [left, right] 之间的矩形面积,取最大的面积值即是答案。
题解代码
java
class Solution {
public int maxArea(int[] height) {
int left = 0, right = height.length - 1;
int res = 0;
while (left < right) {
// [left, right] 之间的矩形面积
int cur_area = Math.min(height[left], height[right]) * (right - left);
res = Math.max(res, cur_area);
// 双指针技巧,移动较低的一边
if (height[left] < height[right]) {
left++;
} else {
right--;
}
}
return res;
}
}
时空分析
时间复杂度:(O(n))
空间复杂度:(O(1))
原因分析:
时间复杂度 :代码使用双指针法,核心是一个
while循环:
初始时左指针
left = 0,右指针right = n - 1(n 为数组长度);每次循环中,指针只会向中间移动一次(
left++或right--);循环终止条件为
left >= right,最多执行(n-1)次(指针从两端移动到相邻位置);循环内部操作(计算面积、更新结果、移动指针)均为常数时间 (O(1))。
总操作次数为 (O(n)),因此时间复杂度为 (O(n))。
空间复杂度 :仅使用固定数量的临时变量(
left、right、res、area),未申请额外的数组或动态内存空间,空间占用不随输入规模 n 变化,因此空间复杂度为常数级 (O(1))。
15. 三数之和
题目描述
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。
注意: 答案中不可以包含重复的三元组。
核心思路
nSumTarget 模版。
排序 + 双指针 -> 先给数组从小到大排序,然后双指针 lo 和 hi 分别在数组开头和结尾,这样就可以控制 nums[lo] 和 nums[hi] 这两数之和的大小。如果你想让它俩的和大一些,就让 lo++,如果你想让它俩的和小一些,就让 hi--。
题解代码
java
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
// n 为 3,从 nums[0] 开始计算和为 0 的三元组
return nSumTarget(nums, 3, 0, 0);
}
// 注意:调用这个函数之前一定要先给 nums 排序
// n 填写想求的是几数之和,start 从哪个索引开始计算(一般填 0),target 填想凑出的目标和
List<List<Integer>> nSumTarget(int[] nums, int n, int start, long target) {
int sz = nums.length;
List<List<Integer>> res = new ArrayList<>();
// 至少是 2Sum,且数组大小不应该小于 n
if (n < 2 || sz < n) return res;
// 2Sum 是 base case
if (n == 2) {
// 双指针那一套操作
int lo = start, hi = sz - 1;
while (lo < hi) {
int sum = nums[lo] + nums[hi];
int left = nums[lo], right = nums[hi];
if (sum < target) {
while (lo < hi && nums[lo] == left) lo++;
} else if (sum > target) {
while (lo < hi && nums[hi] == right) hi--;
} else {
res.add(new ArrayList<>(Arrays.asList(left, right)));
while (lo < hi && nums[lo] == left) lo++;
while (lo < hi && nums[hi] == right) hi--;
}
}
} else {
// n > 2 时,递归计算 (n-1)Sum 的结果
for (int i = start; i < sz; i++) {
List<List<Integer>> sub = nSumTarget(nums, n - 1, i + 1, target - nums[i]);
for (List<Integer> arr : sub) {
// (n-1)Sum 加上 nums[i] 就是 nSum
arr.add(nums[i]);
res.add(arr);
}
while (i < sz - 1 && nums[i] == nums[i + 1]) i++;
}
}
return res;
}
}
时空分析
时间复杂度:(O(n^2))
空间复杂度:(O(\log n))
原因分析:
时间复杂度:
- 首先对数组进行排序,排序操作的时间复杂度为 (O(n \log n))(其中 n 为数组长度)。
- 核心逻辑通过递归实现 nSum 计算,对于本题的
threeSum(即 (n = 3)):
- 外层有一个遍历数组的循环,时间复杂度为 (O(n));
- 每次循环内部调用 2Sum 逻辑,2Sum 通过双指针实现,时间复杂度为 (O(n));
- 因此 3Sum 的整体计算复杂度为 (O(n \times n) = O(n^2))。
- 由于 (O(n^2)) 的增长速度快于排序的 (O(n \log n)),最终时间复杂度由 (O(n^2)) 主导。
空间复杂度:
主要来源于两部分:
- 排序过程中(Java 的
Arrays.sort对基本类型使用双轴快排)的递归栈空间,复杂度为 (O(\log n));- 递归调用
nSumTarget的栈深度,对于threeSum仅从 3Sum 递归到 2Sum,深度为常数级((O(1)))。因此整体空间复杂度由排序的递归栈空间主导,为 (O(\log n))。
42. 接雨水
题目描述
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
核心思路
对于任意一个位置 i,能够装的水为:
java
water[i] = min(
# 左边最高的柱子
max(height[0..i]),
# 右边最高的柱子
max(height[i..end])
) - height[i]
然后将这些位置求和即可。
题解代码
java
class Solution {
public int trap(int[] height) {
if (height.length == 0) {
return 0;
}
int n = height.length;
int res = 0;
// 数组充当备忘录
int[] l_max = new int[n];
int[] r_max = new int[n];
// 初始化 base case
l_max[0] = height[0];
r_max[n - 1] = height[n - 1];
// 从左向右计算 l_max
for (int i = 1; i < n; i++)
l_max[i] = Math.max(height[i], l_max[i - 1]);
// 从右向左计算 r_max
for (int i = n - 2; i >= 0; i--)
r_max[i] = Math.max(height[i], r_max[i + 1]);
// 计算答案
for (int i = 1; i < n - 1; i++)
res += Math.min(l_max[i], r_max[i]) - height[i];
return res;
}
}
时空分析
时间复杂度:(O(n))
空间复杂度:(O(n))
原因分析:
时间复杂度:代码包含三个独立的线性循环,均基于数组长度 n 执行:
第一个循环(计算左侧最大值数组 (l_max))从 (i = 1) 到 (i = n-1),共执行 (n-1) 次;
第二个循环(计算右侧最大值数组 (r_max))从 (j = n-2) 到 (j = 0),共执行 (n-1) 次;
第三个循环(累加总接水量)从 (i = 0) 到 (i = n-1),共执行 n 次。
总操作次数为 ((n-1) + (n-1) + n = 3n - 2),根据大 O 表示法的定义,忽略常数项和系数后,时间复杂度为(O(n))。
空间复杂度:额外使用了两个长度为 n 的数组 (l_max) 和 (r_max),用于存储每个位置的左侧最大值和右侧最大值,总额外空间为 2n;其他变量(如 n、i、j、res)仅占用常数空间。忽略系数后,空间复杂度为 (O(n))。
「滑动窗口」
3. 无重复字符的最长子串
题目描述
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
核心思路
用一个 HashMap(CharToCount) 统计窗口中出现的字符的数量。当 window[c] 值大于 1 时,说明窗口中存在重复字符,不符合条件,就该移动 left 缩小窗口了。
题解代码
java
class Solution {
public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> window = new HashMap<>();
int left = 0, right = 0;
// 记录结果
int res = 0;
while (right < s.length()) {
char c = s.charAt(right);
right++;
// 进行窗口内数据的一系列更新
window.put(c, window.getOrDefault(c, 0) + 1);
// 判断左侧窗口是否要收缩
while (window.get(c) > 1) {
char d = s.charAt(left);
left++;
// 进行窗口内数据的一系列更新
window.put(d, window.get(d) - 1);
}
// 在这里更新答案
res = Math.max(res, right - left);
}
return res;
}
}
时空分析
- 时间复杂度:(O(n))
- 空间复杂度 :(O(min(n, m))),其中 n 是字符串
s的长度,m 是字符集的大小- 原因分析 :
- 时间复杂度 :
- 代码使用滑动窗口算法,通过
while循环遍历字符串s。right指针从字符串开头开始,逐步向右移动,最多移动 n 次,其中 n 是字符串s的长度。- 对于每个
right指针移动,虽然存在一个内层while循环用于收缩左侧窗口,但从整体来看,left指针也最多向右移动 n 次。也就是说,两个指针移动的总次数最多为 2n 次。因此,整体时间复杂度为 (O(n))。- 空间复杂度 :
- 代码中使用了一个
HashMap来存储窗口内的字符及其出现的次数。在最坏情况下,窗口内可能包含所有不同的字符。如果字符集大小为 m,那么HashMap最多会存储 m 个不同的字符。当 (m < n) 时,空间复杂度为 (O(m));当 (m \geq n) 时,窗口内最多只能容纳 n 个不同字符,此时空间复杂度为 (O(n))。所以空间复杂度为 (O(min(n, m)))。
438. 找到字符串中所有字母异位词
题目描述
给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
核心思路
先用一个 HashMap 统计 p 中各个字符出现的次数,need。然后在用一个 HashMap 统计 window 中各字符出现的次数。如果 window 满足了 need(用一个 valid 实现),就将索引添加到结果列表中。
什么时候扩大窗口? right 一直往右遍历。
什么时候缩小窗口? right - left >= p.length() 时缩小窗口。
什么时候更新答案? 当缩小窗口时,如果满足 valid 则更新答案。
note:Integer 包装器类型,判断值是否相等要使用 .equals()。
题解代码
java
class Solution {
public List<Integer> findAnagrams(String s, String t) {
Map<Character, Integer> need = new HashMap<>();
Map<Character, Integer> window = new HashMap<>();
for (char c : t.toCharArray()) {
need.put(c, need.getOrDefault(c, 0) + 1);
}
int left = 0, right = 0;
int valid = 0;
// 记录结果
List<Integer> res = new ArrayList<>();
while (right < s.length()) {
char c = s.charAt(right);
right++;
// 进行窗口内数据的一系列更新
if (need.containsKey(c)) {
window.put(c, window.getOrDefault(c, 0) + 1);
if (window.get(c).equals(need.get(c))) {
valid++;
}
}
// 判断左侧窗口是否要收缩
while (right - left >= t.length()) {
// 当窗口符合条件时,把起始索引加入 res
if (valid == need.size()) {
res.add(left);
}
char d = s.charAt(left);
left++;
// 进行窗口内数据的一系列更新
if (need.containsKey(d)) {
if (window.get(d).equals(need.get(d))) {
valid--;
}
window.put(d, window.get(d) - 1);
}
}
}
return res;
}
}
「子串」
560. 和为 K 的子数组
题目描述
给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 。
子数组是数组中元素的连续非空序列。
核心思路
前缀和 和 HashMap -> 需要在维护 preSum 前缀和数组的同时动态维护 count 映射,而不能等到 preSum 计算完成后再处理 count,因为 count[need] 应该维护 preSum[0..i] 中值为 need 的元素个数。
题解代码
java
class Solution {
public int subarraySum(int[] nums, int k) {
int n = nums.length;
// 前缀和数组
int[] preSum = new int[n + 1];
preSum[0] = 0;
// 前缀和到该前缀和出现次数的映射,方便快速查找所需的前缀和
HashMap<Integer, Integer> count = new HashMap<>();
count.put(0, 1);
// 记录和为 k 的子数组个数
int res = 0;
// 计算 nums 的前缀和
for (int i = 1; i <= n; i++) {
preSum[i] = preSum[i - 1] + nums[i - 1];
// 如果之前存在值为 need 的前缀和
// 说明存在以 nums[i-1] 结尾的子数组的和为 k
int need = preSum[i] - k;
if (count.containsKey(need)) {
res += count.get(need);
}
// 将当前前缀和存入哈希表
if (!count.containsKey(preSum[i])) {
count.put(preSum[i], 1);
} else {
count.put(preSum[i], count.get(preSum[i]) + 1);
}
}
return res;
}
}
239. 滑动窗口最大值
题目描述
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
核心思路
单调队列作为滑动窗口。
使用一个队列充当不断滑动的窗口,每次滑动记录其中的最大值:

如何在 O(1) 时间计算最大值,只需要一个特殊的数据结构「单调队列」,push 方法依然在队尾添加元素,但是要把前面比自己小的元素都删掉,直到遇到更大的元素才停止删除。

题解代码
java
class Solution {
// 单调队列的实现
class MonotonicQueue {
LinkedList<Integer> q = new LinkedList<>();
public void push(int n) {
// 将小于 n 的元素全部删除
while (!q.isEmpty() && q.getLast() < n) {
q.pollLast();
}
// 然后将 n 加入尾部
q.addLast(n);
}
public int max() {
return q.getFirst();
}
public void pop(int n) {
if (n == q.getFirst()) {
q.pollFirst();
}
}
}
// 解题函数的实现
public int[] maxSlidingWindow(int[] nums, int k) {
MonotonicQueue window = new MonotonicQueue();
List<Integer> res = new ArrayList<>();
for (int i = 0; i < nums.length; i++) {
if (i < k - 1) {
// 先填满窗口的前 k - 1
window.push(nums[i]);
} else {
// 窗口向前滑动,加入新数字
window.push(nums[i]);
// 记录当前窗口的最大值
res.add(window.max());
// 移出旧数字
window.pop(nums[i - k + 1]);
}
}
// 需要转成 int[] 数组再返回
int[] arr = new int[res.size()];
for (int i = 0; i < res.size(); i++) {
arr[i] = res.get(i);
}
return arr;
}
}
76. 最小覆盖子串
题目描述
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。
注意:
- 对于
t中重复字符,我们寻找的子字符串中该字符数量必须不少于t中该字符数量。 - 如果
s中存在这样的子串,我们保证它是唯一的答案。
核心思路
滑动窗口 -> 标准滑动窗口框架,用两个 HashMap 分别充当 window 和 need,统计每个字符的数量。
什么时候扩大窗口?right < n 时扩大窗口。
什么时候缩小窗口?当 valid >= need.size() 时缩小窗口,也就是窗口中覆盖了 t 的所有字符。
什么时候更新答案?当缩小窗口的时候更新答案。
题解代码
java
class Solution {
public String minWindow(String s, String t) {
Map<Character, Integer> need = new HashMap<>();
Map<Character, Integer> window = new HashMap<>();
for (char c : t.toCharArray()) {
need.put(c, need.getOrDefault(c, 0) + 1);
}
int left = 0, right = 0;
int valid = 0;
// 记录最小覆盖子串的起始索引及长度
int start = 0, len = Integer.MAX_VALUE;
while (right < s.length()) {
// c 是将移入窗口的字符
char c = s.charAt(right);
// 扩大窗口
right++;
// 进行窗口内数据的一系列更新
if (need.containsKey(c)) {
window.put(c, window.getOrDefault(c, 0) + 1);
if (window.get(c).equals(need.get(c)))
valid++;
}
// 判断左侧窗口是否要收缩
while (valid == need.size()) {
// 在这里更新最小覆盖子串
if (right - left < len) {
start = left;
len = right - left;
}
// d 是将移出窗口的字符
char d = s.charAt(left);
// 缩小窗口
left++;
// 进行窗口内数据的一系列更新
if (need.containsKey(d)) {
if (window.get(d).equals(need.get(d)))
valid--;
window.put(d, window.get(d) - 1);
}
}
}
// 返回最小覆盖子串
return len == Integer.MAX_VALUE ? "" : s.substring(start, start + len);
}
}
「普通数组」
53. 最大子数组和
题目描述
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组 是数组中的一个连续部分。
核心思路
动态规划 -> dp 数组的含义:以 nums[i] 为结尾的「最大子数组和」为 dp[i] 。dp[i] 有两种「选择」,要么与前面的相邻子数组连接,形成一个和更大的子数组;要么不与前面的子数组连接,自成一派,自己作为一个子数组。在这两种选择中择优,就可以计算出最大子数组。
滑动窗口 -> right < n 时扩大窗口,windowSum < 0 时缩小窗口,扩大窗口的时候同时更新答案。
题解代码
java
class Solution {
public int maxSubArray(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
// base case
// 第一个元素前面没有子数组
dp[0] = nums[0];
// 状态转移方程
for (int i = 1; i < n; i++) {
dp[i] = Math.max(nums[i], nums[i] + dp[i - 1]);
}
// 得到 nums 的最大子数组
int res = Integer.MIN_VALUE;
for (int i = 0; i < n; i++) {
res = Math.max(res, dp[i]);
}
return res;
}
}
java
class Solution {
public int maxSubArray(int[] nums) {
int n = nums.length;
int windowSum = 0;
int left = 0, right = 0;
int res = Integer.MIN_VALUE;
while(right < n){
windowSum += nums[right];
right++;
res = Math.max(res, windowSum);
while(windowSum < 0){
windowSum -= nums[left];
left++;
}
}
return res;
}
}
56. 合并区间
题目描述
以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
核心思路
技巧 -> 一个区间可以表示为 [start, end],先按区间的 start 排序。对于几个相交区间合并后的结果区间 x,x.start 一定是这些相交区间中 start 最小的,x.end 一定是这些相交区间中 end 最大的。
题解代码
java
class Solution {
public int[][] merge(int[][] intervals) {
LinkedList<int[]> res = new LinkedList<>();
// 按区间的 start 升序排列
Arrays.sort(intervals, (a, b) -> {
return a[0] - b[0];
});
res.add(intervals[0]);
for (int i = 1; i < intervals.length; i++) {
int[] curr = intervals[i];
// res 中最后一个元素的引用
int[] last = res.getLast();
if (curr[0] <= last[1]) {
last[1] = Math.max(last[1], curr[1]);
} else {
// 处理下一个待合并区间
res.add(curr);
}
}
return res.toArray(new int[0][0]);
}
}
189. 轮转数组
题目描述
给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。
核心思路
技巧 -> 先把整个数组反转,再分别反转前 k 个和后 n-k 个即可。
例如 [1,2,3,4,5], k = 3, 先反转整个数组得 [5,4,3,2,1], 再反转前 k 个得 [3,4,5,2,1], 再反转后 n-k 个得 [3,4,5,1,2] 即为答案。
note:k 需要小于 n。(取模)
题解代码
java
class Solution {
public void rotate(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);
}
public void reverse(int[] nums, int start, int end){
while(start < end){
int temp = nums[start];
nums[start] = nums[end];
nums[end] = temp;
start++;
end--;
}
}
}
238. 除自身以外数组的乘积
题目描述
给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。
题目数据 保证 数组 nums 之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。
请 不要使用除法, 且在 O(n) 时间复杂度内完成此题。
核心思路
前缀积 -> 前缀和数组中两个元素之差是子数组元素之和,那么如果构造「前缀积」数组,两个元素相除就是子数组元素之积。
构造一个 prefix 数组记录「前缀积」,再用一个 suffix 记录「后缀积」,根据前缀和后缀积就能计算除了当前元素之外其他元素的积。
题解代码
java
class Solution {
public int[] productExceptSelf(int[] nums) {
int n = nums.length;
// 从左到右的前缀积,prefix[i] 是 nums[0..i] 的元素积
int[] prefix = new int[n];
prefix[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
prefix[i] = prefix[i - 1] * nums[i];
}
// 从右到左的前缀积,suffix[i] 是 nums[i..n-1] 的元素积
int[] suffix = new int[n];
suffix[n - 1] = nums[n - 1];
for (int i = n - 2; i >= 0; i--) {
suffix[i] = suffix[i + 1] * nums[i];
}
// 结果数组
int[] res = new int[n];
res[0] = suffix[1];
res[n - 1] = prefix[n - 2];
for (int i = 1; i < n - 1; i++) {
// 除了 nums[i] 自己的元素积就是 nums[i] 左侧和右侧所有元素之积
res[i] = prefix[i - 1] * suffix[i + 1];
}
return res;
}
}
41. 缺失的第一个正数
题目描述
给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。
请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。
核心思路
对于一个长度为 N 的数组,其中没有出现的最小正整数只能在 [1, N+1] 中。这是因为如果 [1, N] 都出现了,那么答案是 N+1,否则答案是 [1, N] 中没有出现的最小正整数。
数组设计成哈希表 -> 我们对数组进行遍历,对于遍历到的数 x,如果它在 [1, N] 的范围内,那么就将数组中的第 x−1 个位置(注意:数组下标从 0 开始)打上「标记」。在遍历结束之后,如果所有的位置都被打上了标记,那么答案是 N+1,否则答案是最小的没有打上标记的位置加 1。
算法的流程如下:
- 我们将数组中所有小于等于 0 的数修改为 N+1;
- 我们遍历数组中的每一个数 x,它可能已经被打了标记,因此原本对应的数为 ∣x∣,其中 ∣∣ 为绝对值符号。如果 ∣x∣∈ [1, N],那么我们给数组中的第 ∣x∣−1 个位置的数添加一个负号。注意如果它已经有负号,不需要重复添加;
- 在遍历完成之后,如果数组中的每一个数都是负数,那么答案是 N+1,否则答案是第一个正数的位置加 1。

题解代码
java
class Solution {
public int firstMissingPositive(int[] nums) {
int n = nums.length;
for (int i = 0; i < n; i++) {
if (nums[i] <= 0) {
nums[i] = n + 1;
}
}
for (int i = 0; i < n; i++) {
int num = Math.abs(nums[i]);
if (1 <= num && num <= n) {
nums[num - 1] = -Math.abs(nums[num - 1]);
}
}
for (int i = 0; i < n; i++) {
if (nums[i] > 0) {
return i + 1;
}
}
return n + 1;
}
}
「矩阵」
73. 矩阵置零
题目描述
给定一个 m x n 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用原地算法。
核心思路
二维数组遍历 -> 用两个标记数组分别记录每一行和每一列是否有零出现 -> 首先遍历数组一次,如果某个元素为 0,那么就将该元素所在的行和列所对应标记数组的位置置为 true。最后再次遍历该数组,用标记数组更新原数组即可。
题解代码
java
class Solution {
public void setZeroes(int[][] matrix) {
int m = matrix.length, n = matrix[0].length;
boolean[] row = new boolean[m];
boolean[] col = new boolean[n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (matrix[i][j] == 0) {
row[i] = col[j] = true;
}
}
}
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (row[i] || col[j]) {
matrix[i][j] = 0;
}
}
}
}
}
54. 螺旋矩阵
题目描述
给你一个 m 行 n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。
核心思路

二维数组遍历 -> 按照 右、下、左、上 的顺序遍历数组,并使用四个变量圈定未遍历元素的边界。
题解代码
java
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
int m = matrix.length, n = matrix[0].length;
int upper_bound = 0, lower_bound = m - 1;
int left_bound = 0, right_bound = n - 1;
List<Integer> res = new LinkedList<>();
// res.size() == m * n 则遍历完整个数组
while (res.size() < m * n) {
if (upper_bound <= lower_bound) {
// 在顶部从左向右遍历
for (int j = left_bound; j <= right_bound; j++) {
res.add(matrix[upper_bound][j]);
}
// 上边界下移
upper_bound++;
}
if (left_bound <= right_bound) {
// 在右侧从上向下遍历
for (int i = upper_bound; i <= lower_bound; i++) {
res.add(matrix[i][right_bound]);
}
// 右边界左移
right_bound--;
}
if (upper_bound <= lower_bound) {
// 在底部从右向左遍历
for (int j = right_bound; j >= left_bound; j--) {
res.add(matrix[lower_bound][j]);
}
// 下边界上移
lower_bound--;
}
if (left_bound <= right_bound) {
// 在左侧从下向上遍历
for (int i = lower_bound; i >= upper_bound; i--) {
res.add(matrix[i][left_bound]);
}
// 左边界右移
left_bound++;
}
}
return res;
}
}
48. 旋转图像
题目描述
给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。
你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。
核心思路
二维数组遍历 -> 先把二维矩阵沿对角线反转,然后反转矩阵的每一行,结果就是顺时针反转整个矩阵。
题解代码
java
class Solution {
public void rotate(int[][] matrix) {
int n = matrix.length;
// 先沿对角线反转二维矩阵
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
// swap(matrix[i][j], matrix[j][i]);
int temp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = temp;
}
}
// 然后反转二维矩阵的每一行
for (int[] row : matrix) {
reverse(row);
}
}
// 反转一维数组
void reverse(int[] arr) {
int i = 0, j = arr.length - 1;
while (j > i) {
// swap(arr[i], arr[j]);
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
i++;
j--;
}
}
}
240. 搜索二维矩阵 II
题目描述
编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:
- 每行的元素从左到右升序排列。
- 每列的元素从上到下升序排列。
核心思路
二维数组遍历 -> 从右上角开始遍历,规定只能向左或向下移动。如果向左移动,元素在减小,如果向下移动,元素在增大,这样的话就可以根据当前位置的元素和 target 的相对大小来判断应该往哪移动,不断接近从而找到 target 的位置。
题解代码
java
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
int m = matrix.length, n = matrix[0].length;
// 初始化在右上角
int i = 0, j = n - 1;
while (i < m && j >= 0) {
if (matrix[i][j] == target) {
return true;
}
if (matrix[i][j] < target) {
// 需要大一点,往下移动
i++;
} else {
// 需要小一点,往左移动
j--;
}
}
// while 循环中没有找到,则 target 不存在
return false;
}
}
「链表」
160. 相交链表
题目描述
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。
图示两个链表在节点 c1 开始相交:

题目数据 保证 整个链式结构中不存在环。
注意 ,函数返回结果后,链表必须 保持其原始结构。
核心思路
两个指针 p1 和 p2 分别在两条链表上前进,我们可以让 p1 遍历完链表 A 之后开始遍历链表 B,让 p2 遍历完链表 B 之后开始遍历链表 A,这样相当于「逻辑上」两条链表接在了一起。
如果这样进行拼接,就可以让 p1 和 p2 同时进入公共部分,也就是同时到达相交节点 c1。

题解代码
java
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
// p1 指向 A 链表头结点,p2 指向 B 链表头结点
ListNode p1 = headA, p2 = headB;
while (p1 != p2) {
// p1 走一步,如果走到 A 链表末尾,转到 B 链表
if (p1 == null) p1 = headB;
else p1 = p1.next;
// p2 走一步,如果走到 B 链表末尾,转到 A 链表
if (p2 == null) p2 = headA;
else p2 = p2.next;
}
return p1;
}
}
206. 反转链表
题目描述
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
核心思路
迭代 -> 三个结点 cur,pre 与 next。
题解代码
java
class Solution {
public ListNode reverseList(ListNode head) {
ListNode cur = head, pre = null;
while(cur != null){
ListNode next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
}
234. 回文链表
题目描述
给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。
核心思路

- 先通过快慢指针找到链表的中点。(注意奇偶长度)
- 从 slow 开始反转后面的链表,现在就可以开始比较回文串了。
题解代码
java
class Solution {
public boolean isPalindrome(ListNode head) {
ListNode slow, fast;
slow = fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
if (fast != null)
slow = slow.next;
ListNode left = head;
ListNode right = reverse(slow);
while (right != null) {
if (left.val != right.val)
return false;
left = left.next;
right = right.next;
}
return true;
}
ListNode reverse(ListNode head) {
ListNode pre = null, cur = head;
while (cur != null) {
ListNode next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
}
141. 环形链表
题目描述
给你一个链表的头节点 head ,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true 。 否则,返回 false 。
核心思路
快慢指针 -> 每当慢指针 slow 前进一步,快指针 fast 就前进两步。如果 fast 最终遇到空指针,说明链表中没有环;如果 fast 最终和 slow 相遇,那肯定是 fast 超过了 slow 一圈,说明链表中含有环。
题解代码
java
public class Solution {
public boolean hasCycle(ListNode head) {
// 快慢指针初始化指向 head
ListNode slow = head, fast = head;
// 快指针走到末尾时停止
while (fast != null && fast.next != null) {
// 慢指针走一步,快指针走两步
slow = slow.next;
fast = fast.next.next;
// 快慢指针相遇,说明含有环
if (slow == fast) {
return true;
}
}
// 不包含环
return false;
}
}
142. 环形链表 II
题目描述
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始 )。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
核心思路
基于环形链表的解法,直观地来说就是当快慢指针相遇时,让其中任一个指针指向头节点,然后让它俩以相同速度前进,再次相遇时所在的节点位置就是环开始的位置。
题解代码
java
class Solution {
public ListNode detectCycle(ListNode head) {
ListNode fast, slow;
fast = slow = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast == slow) break;
}
// 上面的代码类似 hasCycle 函数
if (fast == null || fast.next == null) {
// fast 遇到空指针说明没有环
return null;
}
// 重新指向头结点
slow = head;
// 快慢指针同步前进,相交点就是环起点
while (slow != fast) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
}
21. 合并两个有序链表
题目描述
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
核心思路
双指针和虚拟头结点

题解代码
java
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
// 虚拟头结点
ListNode dummy = new ListNode(-1), p = dummy;
ListNode p1 = l1, p2 = l2;
while (p1 != null && p2 != null) {
// 比较 p1 和 p2 两个指针
// 将值较小的的节点接到 p 指针
if (p1.val > p2.val) {
p.next = p2;
p2 = p2.next;
} else {
p.next = p1;
p1 = p1.next;
}
// p 指针不断前进
p = p.next;
}
if (p1 != null) {
p.next = p1;
}
if (p2 != null) {
p.next = p2;
}
return dummy.next;
}
}
2. 两数相加
题目描述
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
核心思路

用一个 carry 变量记录进位,然后通过 (valA + valB + carry) % 10 计算当前位的值,通过 (valA + valB + carry) / 10 计算进位。
题解代码
java
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
// 在两条链表上的指针
ListNode p1 = l1, p2 = l2;
// 虚拟头结点(构建新链表时的常用技巧)
ListNode dummy = new ListNode(-1);
// 指针 p 负责构建新链表
ListNode p = dummy;
// 记录进位
int carry = 0;
// 开始执行加法,两条链表走完且没有进位时才能结束循环
while (p1 != null || p2 != null || carry > 0) {
// 先加上上次的进位
int val = carry;
if (p1 != null) {
val += p1.val;
p1 = p1.next;
}
if (p2 != null) {
val += p2.val;
p2 = p2.next;
}
// 处理进位情况
carry = val / 10;
val = val % 10;
// 构建新节点
p.next = new ListNode(val);
p = p.next;
}
// 返回结果链表的头结点(去除虚拟头结点)
return dummy.next;
}
}
19. 删除链表的倒数第 N 个结点
题目描述
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
核心思路
要删除倒数第 n 个节点,就得获得倒数第 n + 1 个节点的引用。
第一步,我们先让一个指针 p1 指向链表的头节点 head,然后走 k 步。
第二步,用一个指针 p2 指向链表头节点 head。
第三步,让 p1 和 p2 同时向前走,p1 走到链表末尾的空指针时走了 n - k 步,p2 也走了 n - k 步,也就是链表的倒数第 k 个节点。
这样,只遍历了一次链表,就获得了倒数第 k 个节点 p2。
题解代码
java
class Solution {
// 主函数
public ListNode removeNthFromEnd(ListNode head, int n) {
// 虚拟头结点
ListNode dummy = new ListNode(-1);
dummy.next = head;
// 删除倒数第 n 个,要先找倒数第 n + 1 个节点
ListNode x = findFromEnd(dummy, n + 1);
// 删掉倒数第 n 个节点
x.next = x.next.next;
return dummy.next;
}
// 返回链表的倒数第 k 个节点
ListNode findFromEnd(ListNode head, int k) {
ListNode p1 = head;
// p1 先走 k 步
for (int i = 0; i < k; i++) {
p1 = p1.next;
}
ListNode p2 = head;
// p1 和 p2 同时走 n - k 步
while (p1 != null) {
p2 = p2.next;
p1 = p1.next;
}
// p2 现在指向第 n - k 个节点
return p2;
}
}
24. 两两交换链表中的节点
题目描述
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
核心思路
递归
题解代码
java
class Solution {
// 定义:输入以 head 开头的单链表,将这个单链表中的每两个元素翻转,
// 返回翻转后的链表头结点
public ListNode swapPairs(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode first = head;
ListNode second = head.next;
ListNode others = head.next.next;
// 先把前两个元素翻转
second.next = first;
// 利用递归定义,将剩下的链表节点两两翻转,接到后面
first.next = swapPairs(others);
// 现在整个链表都成功翻转了,返回新的头结点
return second;
}
}
25. K 个一组翻转链表
题目描述
给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。
k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
核心思路

- 先反转以
head开头的k个元素 。这里可以复用前面实现的reverseN函数。 - 将第
k + 1个元素作为head递归调用reverseKGroup函数。 - 将上述两个过程的结果连接起来。
题解代码
java
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
ListNode p1 = head, p2 = head;
// 找到第 k + 1 个结点
for(int i = 0; i < k; i++){
// 如果不够 K 个,不需要反转
if(p2 == null) return head;
p2 = p2.next;
}
// 反转前 k 个元素
ListNode pre = reverseK(p1, k);
// 递归反转后续链表,并连接起来
head.next = reverseKGroup(p2, k);
return pre;
}
// 反转前 k 个链表
public ListNode reverseK(ListNode head, int k){
ListNode pre = null, cur = head;
while(k != 0){
ListNode next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
k--;
}
return pre;
}
}
138. 随机链表的复制
题目描述
给你一个长度为 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 作为传入参数。
核心思路
一个哈希表 + 两次遍历。
第一次遍历专门克隆节点,借助哈希表把原始节点和克隆节点的映射存储起来;第二次专门组装节点,照着原数据结构的样子,把克隆节点的指针组装起来。
题解代码
java
class Solution {
public Node copyRandomList(Node head) {
HashMap<Node, Node> originToClone = new HashMap<>();
// 第一次遍历,先把所有节点克隆出来
for (Node p = head; p != null; p = p.next) {
if (!originToClone.containsKey(p)) {
originToClone.put(p, new Node(p.val));
}
}
// 第二次遍历,把克隆节点的结构连接好
for (Node p = head; p != null; p = p.next) {
if (p.next != null) {
originToClone.get(p).next = originToClone.get(p.next);
}
if (p.random != null) {
originToClone.get(p).random = originToClone.get(p.random);
}
}
// 返回克隆之后的头结点
return originToClone.get(head);
}
}
148. 排序链表
题目描述
给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。
核心思路

归并排序(分治) ->
- 找到 链表的中间结点 head2 的 前一个节点,并断开 head2 与其前一个节点的连接。这样我们就把原链表均分成了两段更短的链表。
- 分治,递归调用 sortList,分别排序 head(只有前一半)和 head2。
- 排序后,我们得到了两个有序链表,那么 合并两个有序链表,得到排序后的链表,返回链表头节点。
题解代码
java
class Solution {
public ListNode sortList(ListNode head) {
// 如果链表为空或者只有一个节点,无需排序
if (head == null || head.next == null) {
return head;
}
// 找到中间节点 head2,并断开 head2 与其前一个节点的连接
// 比如 head=[4,2,1,3],那么 middleNode 调用结束后 head=[4,2] head2=[1,3]
ListNode head2 = findMiddleNode(head);
// 分治
head = sortList(head);
head2 = sortList(head2);
// 合并
return mergeTwoLists(head, head2);
}
// 快慢指针寻找链表中点,并将中点与前一个结点断开连接
public ListNode findMiddleNode(ListNode head){
ListNode fast = head, slow = head, pre = head;
while(fast != null && fast.next != null){
pre = slow;
slow = slow.next;
fast = fast.next.next;
}
pre.next = null;
return slow;
}
// 合并两个有序列表
public ListNode mergeTwoLists(ListNode head1, ListNode head2){
ListNode dummy = new ListNode(-1), p = dummy;
ListNode p1 = head1, p2 = head2;
while(p1 != null && p2 != null){
if(p1.val < p2.val){
p.next = p1;
p1 = p1.next;
}else{
p.next = p2;
p2 = p2.next;
}
p = p.next;
}
if(p1 == null) p.next = p2;
if(p2 == null) p.next = p1;
return dummy.next;
}
}
23. 合并 K 个升序链表
题目描述
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
核心思路
PriorityQueue -> 合并 k 个有序链表的逻辑类似合并两个有序链表,难点在于,如何快速得到 k 个节点中的最小节点,接到结果链表上?这里我们就要用到优先级队列这种数据结构,把链表节点放入一个最小堆,就可以每次获得 k 个节点中的最小节点。
题解代码
java
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
ListNode dummy = new ListNode(-1), p = dummy;
PriorityQueue<ListNode> q = new PriorityQueue<>((a,b) -> (a.val - b.val));
// 将 K 个链表的头节点加入优先级队列
for(ListNode head : lists){
if(head != null){
q.add(head);
}
}
while(!q.isEmpty()){
// 获取最小节点,接到结果链表中
ListNode cur = q.poll();
p.next = cur;
p = p.next;
if(cur.next != null){
q.add(cur.next);
}
}
return dummy.next;
}
}
146. LRU 缓存
题目描述
请你设计并实现一个满足 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) 的平均时间复杂度运行。
核心思路
要让 put 和 get 方法的时间复杂度为 O(1),可以总结出 cache 这个数据结构必要的条件:
1、显然 cache 中的元素必须有时序,以区分最近使用的和久未使用的数据,当容量满了之后要删除最久未使用的那个元素腾位置。
2、我们要在 cache 中快速找某个 key 是否已存在并得到对应的 val;
3、每次访问 cache 中的某个 key,需要将这个元素变为最近使用的,也就是说 cache 要支持在任意位置快速插入和删除元素。
哈希表查找快,但是数据无固定顺序;链表有顺序之分,插入删除快,但是查找慢,所以结合二者的长处,可以形成一种新的数据结构:哈希链表 LinkedHashMap:

put 和 get 的具体逻辑,可以画出这样一个流程图:

题解代码
java
class LRUCache {
int cap;
LinkedHashMap<Integer, Integer> cache = new LinkedHashMap<>();
public LRUCache(int capacity) {
this.cap = capacity;
}
public int get(int key) {
if (!cache.containsKey(key)) {
return -1;
}
// 将 key 变为最近使用
makeRecently(key);
return cache.get(key);
}
public void put(int key, int val) {
if (cache.containsKey(key)) {
// 修改 key 的值
cache.put(key, val);
// 将 key 变为最近使用
makeRecently(key);
return;
}
if (cache.size() >= this.cap) {
// 链表头部就是最久未使用的 key
int oldestKey = cache.keySet().iterator().next();
cache.remove(oldestKey);
}
// 将新的 key 添加链表尾部
cache.put(key, val);
}
private void makeRecently(int key) {
int val = cache.get(key);
// 删除 key,重新插入到队尾
cache.remove(key);
cache.put(key, val);
}
}
「二叉树」
94. 二叉树的中序遍历
题目描述
给定一个二叉树的根节点 root ,返回它的中序遍历。
核心思路
二叉树的 DFS 遍历模版。
java
/// 二叉树的遍历框架
void traverse(TreeNode root) {
if (root == null) {
return;
}
// 前序位置
traverse(root.left);
// 中序位置
traverse(root.right);
// 后序位置
}
题解代码
java
class Solution {
List<Integer> res = new LinkedList<>();
public List<Integer> inorderTraversal(TreeNode root) {
traverse(root);
return res;
}
public void traverse(TreeNode root){
if(root == null){
return;
}
traverse(root.left);
res.add(root.val);
traverse(root.right);
}
}
时空分析
- 时间复杂度:(O(n))
- 空间复杂度:(O(h))
- 原因分析 :
- 时间复杂度 :
- 该代码实现了二叉树的中序遍历。在遍历过程中,每个节点都会被访问一次且仅一次。假设二叉树节点数为 n,每次访问节点时执行的操作(如添加节点值到结果列表)时间复杂度为常数 (O(1))。所以总的时间复杂度为 (O(n\times1)= O(n))。
- 空间复杂度 :
- 空间复杂度主要来源于递归调用栈。在最坏情况下,二叉树是一条链,递归深度达到树的高度 (h = n),此时空间复杂度为 (O(n))。而在平衡二叉树中,树的高度 (h =\log n),空间复杂度为 (O(\log n))。一般情况下,空间复杂度取决于树的高度 h,因此空间复杂度为 (O(h))。这里不考虑存储结果
res所占用的空间,如果考虑res,其空间复杂度为 (O(n)),因为最终res会存储 n 个节点的值。但按照常规分析递归算法空间复杂度的方式,不把存储结果的空间计算在内,所以空间复杂度为 (O(h))。
104. 二叉树的最大深度
题目描述
给定一个二叉树 root ,返回其最大深度。
二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。
核心思路
递归(分解问题) -> 输入一个节点,返回以该节点为根的二叉树的最大深度。
题解代码
java
class Solution {
// 定义:输入一个节点,返回以该节点为根的二叉树的最大深度
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
int leftMax = maxDepth(root.left);
int rightMax = maxDepth(root.right);
// 根据左右子树的最大深度推出原二叉树的最大深度
return 1 + Math.max(leftMax, rightMax);
}
}
时空分析
- 时间复杂度:(O(n))
- 空间复杂度:(O(h))
- 原因分析 :
- 时间复杂度 :
- 此代码通过递归方式计算二叉树的最大深度。每个节点在递归过程中都会被访问一次,且仅一次。设二叉树的节点数为 n,每次访问节点执行的操作(如递归调用、比较等)时间复杂度为常数 (O(1))。所以整体时间复杂度为 (O(n\times1)= O(n))。
- 空间复杂度 :
- 空间复杂度主要由递归调用栈的深度决定。在最坏情况下,二叉树为一条链,此时树的高度 (h = n),递归调用栈深度也为 n,空间复杂度为 (O(n))。对于平衡二叉树,树的高度 (h = \log n),递归调用栈深度为 (\log n),空间复杂度为 (O(\log n))。一般地,空间复杂度取决于树的高度 h,因此空间复杂度为 (O(h))。
226. 翻转二叉树
题目描述
给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。
核心思路
递归(分解问题)-> 把二叉树上的每个节点的左右子节点都交换一下。
题解代码
java
class Solution {
// 定义:将以 root 为根的这棵二叉树翻转,返回翻转后的二叉树的根节点
TreeNode invertTree(TreeNode root) {
if (root == null) {
return null;
}
// 利用函数定义,先翻转左右子树
TreeNode left = invertTree(root.left);
TreeNode right = invertTree(root.right);
// 然后交换左右子节点
root.left = right;
root.right = left;
// 和定义逻辑自恰:以 root 为根的这棵二叉树已经被翻转,返回 root
return root;
}
}
101. 对称二叉树
题目描述
给你一个二叉树的根节点 root , 检查它是否轴对称。
核心思路
递归(分解问题) -> 判断两棵树是否镜像对称,只要判断两棵子树都是镜像对称的就行了。
题解代码
java
class Solution {
public boolean isSymmetric(TreeNode root) {
if (root == null) return true;
// 检查两棵子树是否对称
return check(root.left, root.right);
}
// 定义:判断输入的两棵树是否是镜像对称的
boolean check(TreeNode left, TreeNode right) {
if (left == null || right == null) {
return left == right;
}
// 两个根节点需要相同
if (left.val != right.val) return false;
// 左右子树也需要镜像对称
return check(left.right, right.left) && check(left.left, right.right);
}
}
543. 二叉树的直径
题目描述
给你一棵二叉树的根节点,返回该树的 直径 。
二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root 。
两节点之间路径的 长度 由它们之间边数表示。
核心思路
二叉树的直径,就是左右子树的最大深度之和 -> 在求最大深度时,运用二叉树的后序遍历,在 maxDepth 的后序遍历位置顺便计算最大直径。
题解代码
java
class Solution {
int maxDiameter = 0;
public int diameterOfBinaryTree(TreeNode root) {
maxDepth(root);
return maxDiameter;
}
int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
int leftMax = maxDepth(root.left);
int rightMax = maxDepth(root.right);
// 后序遍历位置顺便计算最大直径
maxDiameter = Math.max(maxDiameter, leftMax + rightMax);
return 1 + Math.max(leftMax, rightMax);
}
}
102. 二叉树的层序遍历
题目描述
给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
核心思路
二叉树 BFS 遍历模版。
java
void levelOrderTraverse(TreeNode root) {
if (root == null) {
return;
}
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
// 记录当前遍历到的层数(根节点视为第 1 层)
int depth = 1;
while (!q.isEmpty()) {
int sz = q.size();
for (int i = 0; i < sz; i++) {
TreeNode cur = q.poll();
// 访问 cur 节点,同时知道它所在的层数
System.out.println("depth = " + depth + ", val = " + cur.val);
// 把 cur 的左右子节点加入队列
if (cur.left != null) {
q.offer(cur.left);
}
if (cur.right != null) {
q.offer(cur.right);
}
}
depth++;
}
}
题解代码
java
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new LinkedList<>();
if (root == null) {
return res;
}
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
// while 循环控制从上向下一层层遍历
while (!q.isEmpty()) {
int sz = q.size();
// 记录这一层的节点值
List<Integer> level = new LinkedList<>();
// for 循环控制每一层从左向右遍历
for (int i = 0; i < sz; i++) {
TreeNode cur = q.poll();
level.add(cur.val);
if (cur.left != null)
q.offer(cur.left);
if (cur.right != null)
q.offer(cur.right);
}
res.add(level);
}
return res;
}
}
108. 将有序数组转换为二叉搜索树
题目描述
给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 平衡 二叉搜索树。(平衡二叉树 是指该树所有节点的左右子树的高度相差不超过 1。)
核心思路
递归(分解问题)-> 二叉树的构建问题遵循固定的套路,构造整棵树可以分解成:先构造根节点,然后构建左右子树。一个有序数组对于 BST 来说就是中序遍历结果,根节点在数组中心,数组左侧是左子树元素,右侧是右子树元素。
题解代码
java
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
return build(nums, 0, nums.length - 1);
}
// 将闭区间 [left, right] 中的元素转化成 BST,返回根节点
TreeNode build(int[] nums, int left, int right) {
if (left > right) {
// 区间为空
return null;
}
// 构造根节点
// BST 节点左小右大,中间的元素就是根节点
int mid = (left + right) / 2;
TreeNode root = new TreeNode(nums[mid]);
// 递归构建左子树
root.left = build(nums, left, mid - 1);
// 递归构造右子树
root.right = build(nums, mid + 1, right);
return root;
}
}
98. 验证二叉搜索树
题目描述
给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
- 节点的左子树只包含 严格小于 当前节点的数。
- 节点的右子树只包含 严格大于 当前节点的数。
- 所有左子树和右子树自身必须也是二叉搜索树。
核心思路
递归(分解问题)-> BST 左小右大的特性是指 root.val 要比左子树的所有节点都更大,要比右子树的所有节点都小。通过使用辅助函数,增加函数参数列表,在参数中携带额外信息,将这种约束传递给子树的所有节点。
题解代码
java
class Solution {
public boolean isValidBST(TreeNode root) {
return isValidBST(root, null, null);
}
// 限定以 root 为根的子树节点必须满足 max.val > root.val > min.val
boolean isValidBST(TreeNode root, TreeNode min, TreeNode max) {
// base case
if (root == null) return true;
// 若 root.val 不符合 max 和 min 的限制,说明不是合法 BST
if (min != null && root.val <= min.val) return false;
if (max != null && root.val >= max.val) return false;
// 限定左子树的最大值是 root.val,右子树的最小值是 root.val
return isValidBST(root.left, min, root)
&& isValidBST(root.right, root, max);
}
}
230. 二叉搜索树中第 K 小的元素
题目描述
给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 小的元素(从 1 开始计数)。
核心思路
递归(遍历)-> BST 的中序遍历结果是有序的(升序),所以用一个外部变量记录中序遍历结果第 k 个元素即是第 k 小的元素。
题解代码
java
class Solution {
public int kthSmallest(TreeNode root, int k) {
// 利用 BST 的中序遍历特性
traverse(root, k);
return res;
}
// 记录结果
int res = 0;
// 记录当前元素的排名
int rank = 0;
void traverse(TreeNode root, int k) {
if (root == null) {
return;
}
traverse(root.left, k);
// 中序代码位置
rank++;
if (k == rank) {
// 找到第 k 小的元素
res = root.val;
return;
}
traverse(root.right, k);
}
}
199. 二叉树的右视图
题目描述
给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
核心思路
BFS 层序遍历 -> 每一层的最后一个节点就是二叉树的右侧视图。我们可以把 BFS 反过来,从右往左遍历每一行,进一步提升效率。
题解代码
java
class Solution {
public List<Integer> rightSideView(TreeNode root) {
List<Integer> res = new LinkedList<>();
if(root == null) return res;
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
while(!q.isEmpty()){
int sz = q.size();
TreeNode last = q.peek();
for(int i = 0; i < sz; i++){
TreeNode cur = q.poll();
if(cur.right != null){
q.offer(cur.right);
}
if(cur.left != null){
q.offer(cur.left);
}
}
res.add(last.val);
}
return res;
}
}
114. 二叉树展开为链表
题目描述
给你二叉树的根结点 root ,请你将它展开为一个单链表:
- 展开后的单链表应该同样使用
TreeNode,其中right子指针指向链表中下一个结点,而左子指针始终为null。 - 展开后的单链表应该与二叉树 先序遍历 顺序相同。
核心思路
递归(分解问题)-> 递归函数 flatten 的定义:给 flatten 函数输入一个节点 root,那么以 root 为根的二叉树就会被拉平为一条链表。
如何利用这个定义来完成算法?想想怎么把以 root 为根的二叉树拉平为一条链表?很简单,以下流程:
1、将 root 的左子树和右子树拉平。
2、将 root 的右子树接到左子树下方,然后将整个左子树作为右子树。

题解代码
java
class Solution {
// 定义:将以 root 为根的树拉平为链表
public void flatten(TreeNode root) {
// base case
if (root == null) return;
// 先递归拉平左右子树
flatten(root.left);
flatten(root.right);
// ***后序遍历位置***
// 1、左右子树已经被拉平成一条链表
TreeNode left = root.left;
TreeNode right = root.right;
// 2、将左子树作为右子树
root.left = null;
root.right = left;
// 3、将原先的右子树接到当前右子树的末端
TreeNode p = root;
while (p.right != null) {
p = p.right;
}
p.right = right;
}
}
105. 从前序与中序遍历序列构造二叉树
题目描述
给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的 先序遍历 , inorder 是同一棵树的 中序遍历,请构造二叉树并返回其根节点。
核心思路
构造二叉树,第一件事一定是找根节点,然后想办法构造左右子树。
二叉树的前序和中序遍历结果的特点如下:

前序遍历结果第一个就是根节点的值,然后再根据中序遍历结果确定左右子树的节点。

题解代码
java
class Solution {
// 存储 inorder 中值到索引的映射
HashMap<Integer, Integer> valToIndex = new HashMap<>();
public TreeNode buildTree(int[] preorder, int[] inorder) {
for (int i = 0; i < inorder.length; i++) {
valToIndex.put(inorder[i], i);
}
return build(preorder, 0, preorder.length - 1,
inorder, 0, inorder.length - 1);
}
// 定义:前序遍历数组为 preorder[preStart..preEnd]
// 中序遍历数组为 inorder[inStart..inEnd]
// 构造这个二叉树并返回该二叉树的根节点
TreeNode build(int[] preorder, int preStart, int preEnd,
int[] inorder, int inStart, int inEnd) {
if (preStart > preEnd) {
return null;
}
// root 节点对应的值就是前序遍历数组的第一个元素
int rootVal = preorder[preStart];
// rootVal 在中序遍历数组中的索引
int index = valToIndex.get(rootVal);
int leftSize = index - inStart;
// 先构造出当前根节点
TreeNode root = new TreeNode(rootVal);
// 递归构造左右子树
root.left = build(preorder, preStart + 1, preStart + leftSize,
inorder, inStart, index - 1);
root.right = build(preorder, preStart + leftSize + 1, preEnd,
inorder, index + 1, inEnd);
return root;
}
}
437. 路径总和 III
题目描述
给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。
路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
核心思路
递归(遍历) + 前缀和 + HashMap -> 这道题与 560. 和为 K 的子数组 思想类似答题步骤:
- 求前缀和
- 查看 HashMap(定义为:preSumToCount) 中是否有
preSum-target的 key,有则加上对应的 count。 - 将该 preSum 放入到 HashMap 中,并更新 count 次数。
note:数据大小,preSum 应该定义为 long 类型。
题解代码
java
class Solution {
// 记录前缀和
// 定义:从二叉树的根节点开始,路径和为 pathSum 的路径有 preSumCount.get(pathSum) 个
HashMap<Long, Integer> preSumCount = new HashMap<>();
long pathSum, targetSum;
int res = 0;
public int pathSum(TreeNode root, int targetSum) {
if (root == null) {
return 0;
}
this.pathSum = 0;
this.targetSum = targetSum;
this.preSumCount.put(0L, 1);
traverse(root);
return res;
}
void traverse(TreeNode root) {
if (root == null) {
return;
}
// 前序遍历位置
pathSum += root.val;
// 从二叉树的根节点开始,路径和为 pathSum - targetSum 的路径条数
// 就是路径和为 targetSum 的路径条数
res += preSumCount.getOrDefault(pathSum - targetSum, 0);
// 记录从二叉树的根节点开始,路径和为 pathSum 的路径条数
preSumCount.put(pathSum, preSumCount.getOrDefault(pathSum, 0) + 1);
traverse(root.left);
traverse(root.right);
// 后序遍历位置
preSumCount.put(pathSum, preSumCount.get(pathSum) - 1);
pathSum -= root.val;
}
}
236. 二叉树的最近公共祖先
题目描述
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科 中最近公共祖先的定义为:"对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。"
核心思路
递归(分解问题) -> 最近公共祖先模板
递归函数的定义:给该函数输入三个参数 root,p,q,它会返回一个节点:
情况 1,如果 p 和 q 都在以 root 为根的树中,函数返回的即使 p 和 q 的最近公共祖先节点。
情况 2,那如果 p 和 q 都不在以 root 为根的树中怎么办呢?函数理所当然地返回 null 呗。
情况 3,那如果 p 和 q 只有一个存在于 root 为根的树中呢?函数就会返回那个节点。
java
// 定义:在以 root 为根的二叉树中寻找值为 val1 或 val2 的节点
TreeNode find(TreeNode root, int val1, int val2) {
// base case
if (root == null) {
return null;
}
// 前序位置,看看 root 是不是目标值
if (root.val == val1 || root.val == val2) {
return root;
}
// 去左右子树寻找
TreeNode left = find(root.left, val1, val2);
TreeNode right = find(root.right, val1, val2);
// 后序位置,已经知道左右子树是否存在目标值
return left != null ? left : right;
}
题解代码
java
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
return find(root, p.val, q.val);
}
public TreeNode find(TreeNode root, int val1, int val2){
if(root == null) return null;
if(root.val == val1 || root.val == val2) return root;
TreeNode left = find(root.left, val1, val2);
TreeNode right = find(root.right, val1, val2);
if(left != null && right != null){
return root;
}
return left != null ? left : right;
}
}
124. 二叉树中的最大路径和
题目描述
二叉树中的 路径 被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。
路径和 是路径中各节点值的总和。
给你一个二叉树的根节点 root ,返回其 最大路径和 。
核心思路
递归(分解问题) -> 分解问题的思想:定义一个 oneSideMax 函数来计算从根节点 root 为起点的最大单边路径和。oneSideMax 函数和 maxDepth 函数原理类似,只不过 maxDepth 计算最大深度,oneSideMax 计算「单边」最大路径和。然后在计算单边路径和时顺便计算最大路径和。(后序遍历位置)

题解代码
java
class Solution {
int res = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
if (root == null) {
return 0;
}
// 计算单边路径和时顺便计算最大路径和
oneSideMax(root);
return res;
}
// 定义:计算从根节点 root 为起点的最大单边路径和
int oneSideMax(TreeNode root) {
if (root == null) {
return 0;
}
int leftMaxSum = Math.max(0, oneSideMax(root.left));
int rightMaxSum = Math.max(0, oneSideMax(root.right));
// 后序遍历位置,顺便更新最大路径和
int pathMaxSum = root.val + leftMaxSum + rightMaxSum;
res = Math.max(res, pathMaxSum);
// 实现函数定义,左右子树的最大单边路径和加上根节点的值
// 就是从根节点 root 为起点的最大单边路径和
return Math.max(leftMaxSum, rightMaxSum) + root.val;
}
}
🐮🐴