LeetCode Hot 100 题解笔记(Java)· 上
本文是 Hot 100 笔记上篇,覆盖哈希表到二叉树。下篇从图论开始。
本笔记记录我在刷 LeetCode Hot 100 过程中的题解与思路整理,覆盖常见分类(哈希、双指针、滑动窗口、二叉树、动态规划等)。
笔记约定:
代码统一为 Java,命名规范、边界情况尽量写全
每题包含:标签/难度徽章 → 题目 → 思路简述 → 参考代码 → 复杂度
分类下标有 ⭐ 的为 核心题,优先掌握;🌟 为个人收藏、认为比较重要的题
时间紧时,先过每个分类的 ⭐ 核心题,再补其余题目。
目录
1. 哈希表 (Hashing)
核心思想: 用 HashMap / HashSet 把查找、计数、去重做到接近 O(1)。常见模式:补数查找、频次统计、存在性判断。
题目列表
1. 两数之和 (Two Sum) ⭐ ✅ 2026-08-13 标签:数组, 哈希表
49. 字母异位词分组 (Group Anagrams) ✅ 2026-08-13 标签:数组, 哈希表, 字符串, 排序
128. 最长连续序列 (Longest Consecutive Sequence) ⭐ ✅ 2026-08-13 标签:并查集, 数组, 哈希表
1. 两数之和 (Two Sum)
数组哈希表简单
题目
给定一个整数数组 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²) 的算法?
思路简述
暴力枚举是 O(n²)。更优做法:遍历时用哈希表记录「已见过的值 → 下标」。对当前值 x,若表中已有 target - x,直接返回两个下标;否则把 x 存入表。一遍扫描即可。
参考代码
JAVA
class Solution {
public int[] twoSum(int[] nums, int target) {
// 哈希表:记录「已经遍历过的数值 → 该数值所在下标」
// 作用:O(1) 判断「补数」是否出现过,避免暴力双重循环
Map<Integer, Integer> indexByValue = new HashMap<>();
// 只遍历一遍数组,边查边存
for (int i = 0; i < nums.length; i++) {
// 当前数 nums[i] 想凑出 target,还差 need
// 例如 target=9、nums[i]=2,则 need=7
int need = target - nums[i];
// 若 need 已在表中,说明前面某个位置 j 满足:
// nums[j] + nums[i] == target,且 j != i(因为 i 还没入表)
if (indexByValue.containsKey(need)) {
// 返回 [补数下标, 当前下标]
return new int[]{indexByValue.get(need), i};
}
// 没找到配对:把当前值写入表,留给后面的元素当「补数」用
// 注意:必须先查再存,否则同一元素会被用两次(题目不允许)
indexByValue.put(nums[i], i);
}
// 按题意一定有解;此处仅为防御性返回,实际不会走到
int[] ans = new int[]{-1, -1};
return ans;
}
}
复杂度
时间:O(n),每个元素最多查 / 写哈希表一次
空间:O(n),最坏情况下表中存下全部元素
49. 字母异位词分组 (Group Anagrams)
数组哈希表字符串排序中等
题目
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
字母异位词:由相同字母重新排列得到的字符串(字母种类与出现次数相同,顺序可以不同)。
示例 1:
输入:strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出:[["bat"],["nat","tan"],["ate","eat","tea"]]
解释:
"bat" 无法与其他串互为异位词
"nat" 与 "tan" 互为异位词
"ate"、"eat"、"tea" 互为异位词
示例 2:
输入:strs =
[""]
输出:""
示例 3:
输入:strs =
["a"]
输出:"a"
提示:
1 <= strs.length <= 10^40 <= strs[i].length <= 100strs[i]仅包含小写字母
思路简述
异位词的本质是「字母多重集相同」。把每个字符串排序后得到的串作为分组键:同一组异位词排序结果必然相同。用哈希表维护 排序后的键 → 原串列表,遍历一遍即可分组,返回所有列表(顺序任意)。
用 26 位计数数组拼成键,避免排序,单串代价从 O(k log k) 降到 O(k)。
参考代码
JAVA
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
// 哈希表:排序后的「签名」→ 属于该签名的所有原字符串
// 例如 "eat" / "tea" / "ate" 排序后都是 "aet",会进同一组
Map<String, List<String>> groupsBySignature = new HashMap<>();
for (String word : strs) {
// 对当前串的字符排序,得到分组用的签名键
char[] chars = word.toCharArray();
Arrays.sort(chars);
String signature = new String(chars);
// 若该签名尚无分组则新建列表,再把原串加入对应组
groupsBySignature
.computeIfAbsent(signature, key -> new ArrayList<>())
.add(word);
}
// 哈希表的 values 即为各组异位词;题目允许任意顺序
List<List<String>> ans = new ArrayList<>(groupsBySignature.values());
return ans;
}
}
复杂度
时间:O(n · k log k),n 为字符串个数,k 为单串最大长度(每个串排序一次)
空间:O(n · k),哈希表存放全部原串及签名键
128. 最长连续序列 (Longest Consecutive Sequence)
并查集数组哈希表中等
题目
给定一个未排序的整数数组 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
示例 3:
输入:nums = [1,0,1,2]
输出:3
提示:
0 <= nums.length <= 10^5-10^9 <= nums[i] <= 10^9
思路简述
排序再扫是 O(n log n)。要 O(n):先把所有数放进 HashSet,实现 O(1) 存在性查询。只从「序列起点」开始向右扩展:若 x-1 也在集合中,说明 x 不是起点,直接跳过。对每个起点 x,不断查找 x+1, x+2, ... 直到断掉,用终点减起点得到长度并取全局最大。每个数最多被扩展访问常数次,整体 O(n)。
参考代码
JAVA
class Solution {
public int longestConsecutive(int[] nums) {
// 用 HashSet 去重,并支持 O(1) 判断某个数是否存在
Set<Integer> numSet = new HashSet<>();
for (int num : nums) {
numSet.add(num);
}
int ans = 0;
// 遍历集合中的每个数,只从「连续序列的起点」开始向右扩展
for (int num : numSet) {
// 若存在 num-1,说明 num 不是起点,跳过,避免重复计算同一段
if (numSet.contains(num - 1)) {
continue;
}
// 从 num 开始向右探:num, num+1, num+2, ...
int next = num + 1;
while (numSet.contains(next)) {
next++;
}
// next 停在「第一个不存在的数」,因此长度为 next - num
ans = Math.max(ans, next - num);
// 剪枝:已找到的长度不可能再被更长序列超过时提前结束
// (最长连续长度至多等于去重后的元素个数)
if (ans == numSet.size()) {
break;
}
}
return ans;
}
}
复杂度
时间:O(n),每个数最多进出集合 / 被扩展访问常数次
空间:O(n),HashSet 存放去重后的全部元素
2. 双指针 (Two Pointers)
核心思想: 用左右指针(或快慢指针)在有序 / 可单调推进的序列上收缩或移动,把 O(n²) 降到 O(n)。常配合排序、贪心。
题目列表
283. 移动零 (Move Zeroes) ✅ 2026-08-13 标签:数组, 双指针
11. 盛最多水的容器 (Container With Most Water) ⭐ ✅ 2026-08-13 标签:贪心, 数组, 双指针
15. 三数之和 (3Sum) ⭐ ✅ 2026-08-13 标签:数组, 双指针, 排序
42. 接雨水 (Trapping Rain Water) ⭐ ✅ 2026-08-13 标签:栈, 数组, 双指针, 动态规划, 单调栈
283. 移动零 (Move Zeroes)
数组双指针简单
题目
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
必须在不复制数组的情况下 原地 对数组进行操作。
示例 1:
输入:nums = [0,1,0,3,12]
输出:[1,3,12,0,0]
示例 2:
输入:nums = [0]
输出:[0]
提示:
1 <= nums.length <= 10^4-2^31 <= nums[i] <= 2^31 - 1
进阶:你能尽量减少完成的操作次数吗?
思路简述
双指针原地交换:left 指向「下一个非零应放位置」,right 从左到右扫描。每当 nums[right] != 0,就与 nums[left] 交换并把 left 右移。结束后非零按原相对顺序挤到前面,零落在末尾;全程 O(1) 额外空间。
参考代码
JAVA
class Solution {
public void moveZeroes(int[] nums) {
// left:下一个非零元素应该放入的下标(左侧 [0, left) 已是保持相对顺序的非零区)
int left = 0;
// right:扫描指针,负责发现每一个非零元素
for (int right = 0; right < nums.length; right++) {
if (nums[right] != 0) {
// 把非零元素换到 left 位置;若 left == right,交换等价于原地不动
int temp = nums[right];
nums[right] = nums[left];
nums[left] = temp;
left++;
}
}
// 循环结束时:[0, left) 为非零,[left, n) 全为 0
}
}
复杂度
时间:O(n),每个下标最多被
right访问一次空间:O(1),仅使用常数额外变量,原地修改
11. 盛最多水的容器 (Container With Most Water)
贪心数组双指针中等
题目
给定一个长度为 n 的整数数组 height。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i])。
找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。返回容器可以储存的最大水量。
说明:你不能倾斜容器。
示例 1:

输入:height = [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.length2 <= n <= 10^50 <= height[i] <= 10^4
思路简述
暴力枚举所有线对是 O(n²)。更优:左右指针从两端夹逼。面积 = 宽度 × 较矮那边的高度。移动较矮一侧才可能抬高短板并刷新最大值;移动较高一侧只会缩小宽度且高度受短板限制,不可能更优。不断更新最大面积直到两指针相遇。
参考代码
JAVA
class Solution {
public int maxArea(int[] height) {
int ans = 0;
int left = 0;
int right = height.length - 1;
// 两端夹逼:宽度从最大开始往中间收缩
while (left < right) {
// 短板决定水位;面积 = 宽度 × 短板高度
int boardHeight = Math.min(height[left], height[right]);
ans = Math.max(ans, (right - left) * boardHeight);
// 只移动较矮的一侧,才有机会提高短板、刷新最大面积
// 两边等高时移动任意一侧均可(这里走 else,右指针左移)
if (height[left] < height[right]) {
left++;
} else {
right--;
}
}
return ans;
}
}
复杂度
时间:O(n),左右指针最多各走一遍数组
空间:O(1),仅使用常数额外变量
15. 三数之和 (3Sum)
数组双指针排序中等
题目
给你一个整数数组 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-10^5 <= nums[i] <= 10^5
思路简述
先 排序,再枚举第一个数 nums[i],剩余两个数用左右指针在区间 (i, n) 上夹逼,使三者之和为 0。为去重:相同的 nums[i] 只处理一次;找到一组解后,左右指针都跳过相同值。可再加剪枝:最小三数之和已 > 0 则后面更大,直接结束;当前数与最大两数之和仍 < 0 则换下一个 i。
参考代码
JAVA
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> ans = new ArrayList<>();
Arrays.sort(nums);
for (int i = 0; i < nums.length - 2; i++) {
// 最小的三个数都 > 0,后面更大,不可能再有和为 0
if (nums[i] + nums[i + 1] + nums[i + 2] > 0) {
break;
}
// 相同的第一个数只枚举一次,避免重复三元组
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
// 当前数即使配上最大的两个数仍 < 0,换更大的 nums[i]
if (nums[i] + nums[nums.length - 1] + nums[nums.length - 2] < 0) {
continue;
}
int left = i + 1;
int right = nums.length - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum < 0) {
left++;
} else if (sum > 0) {
right--;
} else {
ans.add(Arrays.asList(nums[i], nums[left], nums[right]));
left++;
right--;
// 跳过与刚加入解相同的左右值,保证不重复
while (left < right && nums[left] == nums[left - 1]) {
left++;
}
while (left < right && nums[right] == nums[right + 1]) {
right--;
}
}
}
}
return ans;
}
}
复杂度
时间:O(n²),排序 O(n log n),枚举第一个数后双指针扫完剩余区间
空间:O(1) 额外空间(不计返回列表);若计入排序则视语言实现而定
42. 接雨水 (Trapping Rain Water)
栈数组双指针动态规划单调栈困难
题目
给定 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.length1 <= n <= 2 * 10^40 <= height[i] <= 10^5
思路简述
每个位置能接的水 = min(左侧最高, 右侧最高) - 自身高度(若为正)。不必预处理两侧最高数组:用双指针从两端向中间走,并维护 maxLeft / maxRight。较矮一侧的水位只受本侧已见最高值限制(另一侧一定有不低于它的墙),因此先结算较矮一侧再移动指针,整体 O(n)、O(1) 额外空间。
参考代码
JAVA
class Solution {
public int trap(int[] height) {
int left = 0;
int right = height.length - 1;
// 扫描过程中已见过的左侧 / 右侧最高柱
int maxLeft = 0;
int maxRight = 0;
int ans = 0;
while (left < right) {
// 较矮一侧决定当前水位上界(另一侧必有不低于它的墙)
if (height[left] <= height[right]) {
maxLeft = Math.max(maxLeft, height[left]);
// 当前位置能接到的雨水高度
ans += maxLeft - height[left];
left++;
} else {
maxRight = Math.max(maxRight, height[right]);
ans += maxRight - height[right];
right--;
}
}
return ans;
}
}
复杂度
时间:O(n),左右指针最多共走一遍
空间:O(1),仅使用常数额外变量
3. 滑动窗口 (Sliding Window)
核心思想: 维护一个满足约束的可变区间 [left, right],右端扩张、左端收缩,常配哈希表做频次 / 去重统计。
题目列表
3. 无重复字符的最长子串 (Longest Substring Without Repeating Characters) ⭐ ✅ 2026-08-13 标签:哈希表, 字符串, 滑动窗口
438. 找到字符串中所有字母异位词 (Find All Anagrams in a String) ✅ 2026-08-13 标签:哈希表, 字符串, 滑动窗口
3. 无重复字符的最长子串 (Longest Substring Without Repeating Characters)
哈希表字符串滑动窗口中等
题目
给定一个字符串 s,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入:s = "abcabcbb"
输出:3
解释:因为无重复字符的最长子串是"abc",所以其长度为 3。注意"bca"和"cab"也是正确答案。
示例 2:
输入:s = "bbbbb"
输出:1
解释:因为无重复字符的最长子串是"b",所以其长度为 1。
示例 3:
输入:s = "pwwkew"
输出:3
解释:因为无重复字符的最长子串是"wke",所以其长度为 3。请注意,答案必须是 子串 的长度,"pwke"是一个子序列,不是子串。
提示:
0 <= s.length <= 10^5s由英文字母、数字、符号和空格组成
思路简述
滑动窗口:右指针不断纳入新字符,用哈希表记录 字符 → 最近一次出现的下标。若新字符曾在窗口内出现过,就把左指针跳到「该字符上次位置 + 1」,保证窗口 [left, right] 内无重复。每次更新 窗口长度 right - left + 1 的最大值。
参考代码
JAVA
class Solution {
public int lengthOfLongestSubstring(String s) {
int ans = 0;
int left = 0;
// 字符 → 最近一次出现的下标
Map<Character, Integer> lastIndexByChar = new HashMap<>();
for (int right = 0; right < s.length(); right++) {
char current = s.charAt(right);
// 若 current 曾出现在当前窗口内,左边界跳到「上次位置的下一位」
// getOrDefault(..., -1)+1:从未出现过则仍从原 left 起
left = Math.max(left, lastIndexByChar.getOrDefault(current, -1) + 1);
ans = Math.max(ans, right - left + 1);
lastIndexByChar.put(current, right);
}
return ans;
}
}
复杂度
时间:O(n),每个下标最多被左右指针处理常数次
空间:O(σ),σ 为字符集大小(哈希表最多存出现过的不同字符)
438. 找到字符串中所有字母异位词 (Find All Anagrams in a String)
哈希表字符串滑动窗口中等
题目
给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
示例 1:
输入:s = "cbaebabacd", p = "abc"
输出:[0,6]
解释:
起始索引等于 0 的子串是
"cba",它是"abc"的异位词起始索引等于 6 的子串是
"bac",它是"abc"的异位词
示例 2:
输入:s = "abab", p = "ab"
输出:[0,1,2]
解释:
起始索引等于 0 的子串是
"ab",它是"ab"的异位词起始索引等于 1 的子串是
"ba",它是"ab"的异位词起始索引等于 2 的子串是
"ab",它是"ab"的异位词
提示:
1 <= s.length, p.length <= 3 * 10^4s和p仅包含小写字母
思路简述
异位词 = 字母频次完全相同。在 s 上维护长度为 p.length() 的 定长滑动窗口:先统计 p 的频次数组 need,再统计窗口内频次 window;窗口右端每前进一步,左边移出一字、右边纳入一字。若两数组相等,则当前左端就是一个答案起点。
用一个 diff 计数「有多少个字母的频次仍不一致」,滑动时按增减更新 diff,可把每次比较从 O(26) 降到 O(1);下面给出更易读的定长窗口写法。
参考代码
JAVA
class Solution {
public List<Integer> findAnagrams(String s, String p) {
List<Integer> ans = new ArrayList<>();
int n = s.length();
int m = p.length();
if (n < m) {
return ans;
}
int[] need = new int[26];
int[] window = new int[26];
// 先填满长度为 m 的首个窗口,并统计 p 的目标频次
for (int i = 0; i < m; i++) {
need[p.charAt(i) - 'a']++;
window[s.charAt(i) - 'a']++;
}
if (Arrays.equals(need, window)) {
ans.add(0);
}
// 窗口整体右移:右边进一个字符,左边出一个字符
for (int right = m; right < n; right++) {
window[s.charAt(right) - 'a']++;
window[s.charAt(right - m) - 'a']--;
if (Arrays.equals(need, window)) {
// 当前窗口起点
ans.add(right - m + 1);
}
}
return ans;
}
}
复杂度
时间:O(n · Σ),Σ=26;每次滑动用
Arrays.equals比较两个长度 26 的数组空间:O(Σ),两个频次数组
4. 子串 (Substring)
核心思想: 子串 / 子数组问题常见三类套路:前缀和 + 哈希、滑动窗口、单调队列。先判断是「定长 / 可变窗口」还是「区间和查询」。
题目列表
560. 和为 K 的子数组 (Subarray Sum Equals K) ⭐ 🌟 ✅ 2026-08-13 标签:数组, 哈希表, 前缀和
239. 滑动窗口最大值 (Sliding Window Maximum) ⭐ 🌟 ✅ 2026-08-13 标签:队列, 数组, 滑动窗口, 单调队列, 堆(优先队列)
76. 最小覆盖子串 (Minimum Window Substring) ⭐ 🌟 ✅ 2026-08-13 标签:哈希表, 字符串, 滑动窗口
560. 和为 K 的子数组 (Subarray Sum Equals K)
数组哈希表前缀和中等
题目
给你一个整数数组 nums 和一个整数 k,请你统计并返回该数组中和为 k 的子数组的个数。
子数组是数组中元素的连续非空序列。
示例 1:
输入:nums = [1,1,1], k = 2
输出:2
示例 2:
输入:nums = [1,2,3], k = 3
输出:2
提示:
1 <= nums.length <= 2 * 10^4-1000 <= nums[i] <= 1000-10^7 <= k <= 10^7
思路简述
暴力枚举所有子数组是 O(n²)。利用前缀和:记 prefixSum[i] 为前 i 个元素之和,则区间 [j+1, i] 的和为 prefixSum[i] - prefixSum[j]。要它等于 k,即需要之前出现过前缀和 prefixSum - k。边扫边用哈希表统计「前缀和出现次数」,一次遍历即可。
参考代码
JAVA
class Solution {
public int subarraySum(int[] nums, int k) {
// prefixSumCount:前缀和 → 该前缀和截止到当前位置之前出现了几次
// 先放入 (0, 1):表示「空前缀」出现过 1 次,对应从下标 0 开始的子数组
Map<Integer, Integer> prefixSumCount = new HashMap<>();
prefixSumCount.put(0, 1);
int ans = 0;
// prefixSum:nums[0..i] 的和(扫到 i 时的当前前缀和)
int prefixSum = 0;
for (int i = 0; i < nums.length; i++) {
prefixSum += nums[i];
// 若存在前缀和 = prefixSum - k,说明中间那段子数组之和恰为 k
// 其出现次数都要累加进答案(多段不同起点可能对应同一差值)
if (prefixSumCount.containsKey(prefixSum - k)) {
ans += prefixSumCount.get(prefixSum - k);
}
// 再把当前前缀和登记进表,供后面的位置查询
// 必须先查询再写入,避免把「当前这个前缀」误当成更早的起点
prefixSumCount.put(prefixSum, prefixSumCount.getOrDefault(prefixSum, 0) + 1);
}
return ans;
}
}
复杂度
时间:O(n),每个元素只处理一次,哈希表操作均摊 O(1)
空间:O(n),最坏情况下前缀和各不相同,表中存 n 个键
239. 滑动窗口最大值 (Sliding Window Maximum)
队列数组滑动窗口单调队列堆(优先队列)困难
题目
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组最左侧移到最右侧。你只可以看到窗口内的 k 个数字。窗口每次只向右移动一位。
返回滑动窗口中的最大值。
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置
最大值
[1 3 -1] -3 5 3 6 73
1 [3 -1 -3] 5 3 6 73
1 3 [-1 -3 5] 3 6 75
1 3 -1 [-3 5 3] 6 75
1 3 -1 -3 [5 3 6] 76
1 3 -1 -3 5 [3 6 7]7
示例 2:
输入:nums = [1], k = 1
输出:[1]
提示:
1 <= nums.length <= 10^5-10^4 <= nums[i] <= 10^41 <= k <= nums.length
思路简述
暴力每窗扫一遍是 O(nk)。用 单调递减双端队列 存下标:队头始终是当前窗口最大值的下标。右端纳入新下标 j 前,从队尾弹出所有「值 ≤ nums[j]」的下标(它们不可能再当最大值);窗口左端越过队头下标时从队头弹出过期下标。这样每个下标最多进队出队一次。
参考代码
JAVA
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length;
// ans[i]:以 i 为起点、长度为 k 的窗口的最大值
int[] ans = new int[n - k + 1];
// indexDeque:存下标,对应值在队内从大到小(单调递减)
// 队头 indexDeque.peekFirst() 始终是「当前窗口内最大值」的下标
Deque<Integer> indexDeque = new ArrayDeque<>();
for (int right = 0; right < n; right++) {
// 新元素更大,则它左边更小的候选再也不可能是最大值,从队尾清掉
while (!indexDeque.isEmpty() && nums[indexDeque.peekLast()] <= nums[right]) {
indexDeque.pollLast();
}
indexDeque.offerLast(right);
// left:当前窗口的左边界(窗口为 [left, right],长度 k)
int left = right - k + 1;
if (left < 0) {
// 窗口尚未形成完整的 k 个元素
continue;
}
// 队头下标已滑出窗口,过期,从队头丢弃
while (indexDeque.peekFirst() < left) {
indexDeque.pollFirst();
}
ans[left] = nums[indexDeque.peekFirst()];
}
return ans;
}
}
复杂度
时间:O(n),每个下标最多入队、出队各一次
空间:O(k),双端队列中最多保留一个窗口内的下标
76. 最小覆盖子串 (Minimum Window Substring)
哈希表字符串滑动窗口困难
题目
给定两个字符串 s 和 t,长度分别是 m 和 n。返回 s 中覆盖 t 所有字符(含重复)的 最短窗口子串;若不存在则返回空串 ""。
测试用例保证答案唯一。
示例 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.lengthn == t.length1 <= m, n <= 10^5s和t由英文字母组成
进阶:能否设计 O(m + n) 的算法?
思路简述
可变滑动窗口:右指针扩张纳入字符,直到窗口 覆盖 t 的全部需求;再尽量右移左指针收缩,并在收缩过程中记录最短合法窗口。用数组记录「每种字符还差几个」,再用 unsatisfiedKinds 统计「还有几种字符没达标」;当它变为 0 时窗口合法。每个端点只移动一次,整体 O(m + n)。
参考代码
JAVA
class Solution {
public String minWindow(String s, String t) {
// need[c]:窗口相对目标还「欠」多少个字符 c
// >0 表示还缺;=0 表示刚好够;<0 表示窗口里该字符有多余
int[] need = new int[128];
// unsatisfiedKinds:t 中还有多少种字符尚未达到所需次数
int unsatisfiedKinds = 0;
for (char c : t.toCharArray()) {
// 某种字符第一次被记入需求时,未满足种类数 +1
if (need[c]++ == 0) {
unsatisfiedKinds++;
}
}
int left = 0;
// bestLeft / bestRight:当前找到的最短合法窗口两端(闭区间)
// 初始 bestRight-bestLeft 设得尽量大,表示尚未找到答案
int bestLeft = -1;
int bestRight = s.length();
for (int right = 0; right < s.length(); right++) {
char inChar = s.charAt(right);
// 右端纳入 inChar:需求减 1;若从 1 减到 0,说明这种字符刚好达标
if (--need[inChar] == 0) {
unsatisfiedKinds--;
}
// 窗口已覆盖 t:尝试收缩左端,寻找更短覆盖
while (unsatisfiedKinds == 0) {
if (right - left < bestRight - bestLeft) {
bestLeft = left;
bestRight = right;
}
char outChar = s.charAt(left);
// 左端移出 outChar:需求加 1;若从 0 加到 1,说明这种字符又开始缺
if (need[outChar]++ == 0) {
unsatisfiedKinds++;
}
left++;
}
}
String ans = (bestLeft == -1) ? "" : s.substring(bestLeft, bestRight + 1);
return ans;
}
}
复杂度
时间:O(m + n),
t预处理 O(n),s上左右指针各最多走一遍 O(m)空间:O(Σ),
need长度为字符集大小(此处 128)
5. 普通数组 (Array)
核心思想: 数组基础题,重点练前缀和、原地修改、排序后合并、以及「用下标当哈希」等技巧。
题目列表
53. 最大子数组和 (Maximum Subarray) ⭐ ✅ 2026-08-13 标签:数组, 分治, 动态规划
56. 合并区间 (Merge Intervals) ⭐ ✅ 2026-08-13 标签:数组, 排序
189. 轮转数组 (Rotate Array) ✅ 2026-08-13 标签:数组, 数学, 双指针
238. 除自身以外数组的乘积 (Product of Array Except Self) ✅ 2026-08-13 标签:数组, 前缀和
41. 缺失的第一个正数 (First Missing Positive) ⭐ 🌟 ✅ 2026-08-13 标签:数组, 哈希表
53. 最大子数组和 (Maximum Subarray)
数组分治动态规划中等
题目
给你一个整数数组 nums,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组是数组中的一个连续部分。
示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组[4,-1,2,1]的和最大,为 6。
示例 2:
输入:nums = [1]
输出:1
示例 3:
输入:nums = [5,4,-1,7,8]
输出:23
提示:
1 <= nums.length <= 10^5-10^4 <= nums[i] <= 10^4
进阶:若已实现 O(n) 解法,可尝试分治法。
思路简述
Kadane 算法:维护 以当前元素结尾的最大子段和 endingHere。若此前累计和已为负,对后面没有贡献,直接从当前元素重新开段;否则把当前元素接在后面。过程中用 ans 记录全局最大。一次遍历即可。
参考代码
JAVA
class Solution {
public int maxSubArray(int[] nums) {
// ans:全局最大子数组和(题目保证至少选一个元素)
int ans = Integer.MIN_VALUE;
// endingHere:以当前元素结尾的「最佳」连续子段和
int endingHere = 0;
for (int num : nums) {
// 此前和为负:丢掉前面那段,从 num 重新开始
// 此前和非负:把 num 接到后面,延续当前子段
endingHere = endingHere < 0 ? num : endingHere + num;
ans = Math.max(ans, endingHere);
}
return ans;
}
}
复杂度
时间:O(n),只扫一遍数组
空间:O(1),仅常数额外变量
56. 合并区间 (Merge Intervals)
数组排序中等
题目
以数组 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]边界相接,视为重叠并合并。
示例 3:
输入:intervals =
[[4,7],[1,4]]
输出:1,7
解释:排序后可发现重叠,合并为[1,7]。
提示:
1 <= intervals.length <= 10^4intervals[i].length == 20 <= starti <= endi <= 10^4
思路简述
先按 左端点排序,再从左到右扫描。维护答案列表中「当前最后一段」:若新区间左端 ≤ 最后一段右端,说明重叠(含端点相接),把右端更新为两者较大值;否则无法合并,把新区间追加进答案。
参考代码
JAVA
class Solution {
public int[][] merge(int[][] intervals) {
// 按区间左端点升序,保证扫描时只可能与「上一段」合并
Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));
// ans:已合并完成的区间列表(按左端点有序、互不重叠)
List<int[]> ans = new ArrayList<>();
ans.add(intervals[0]);
for (int i = 1; i < intervals.length; i++) {
// last:答案中当前最后一段,也是唯一可能与 intervals[i] 重叠的段
int[] last = ans.get(ans.size() - 1);
int[] cur = intervals[i];
// 重叠或首尾相接:扩展 last 的右端
if (last[1] >= cur[0]) {
last[1] = Math.max(last[1], cur[1]);
} else {
// 与上一段脱节,开启新段
ans.add(cur);
}
}
return ans.toArray(new int[ans.size()][]);
}
}
复杂度
时间:O(n log n),瓶颈在排序;之后线性扫描
空间:O(n),答案列表(不计排序额外空间)
189. 轮转数组 (Rotate Array)
数组数学双指针中等
题目
给定一个整数数组 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 <= 10^5-2^31 <= nums[i] <= 2^31 - 10 <= k <= 10^5
进阶:至少想出三种解法;能否用 O(1) 额外空间的原地算法?
思路简述
先统一 k %= n。常见三种解法:
额外数组:
ans[(i + k) % n] = nums[i],再拷回;最直观,空间 O(n)。环状替换:从下标 0 出发,把
nums[i]放到(i + k) % n,被挤出的数继续放,走完一圈再换起点;用gcd(k, n)条环覆盖全数组,空间 O(1)。三次反转(推荐):整体反转 → 前
k反转 → 后n-k反转;原地 O(1),实现最稳。
参考代码(解法一:额外数组)
JAVA
class Solution {
public void rotate(int[] nums, int k) {
int n = nums.length;
k %= n;
// ans[j]:轮转后下标 j 应放的值
int[] ans = new int[n];
for (int i = 0; i < n; i++) {
// 原下标 i 的元素右移 k 位后落到 (i + k) % n
ans[(i + k) % n] = nums[i];
}
System.arraycopy(ans, 0, nums, 0, n);
}
}
参考代码(解法二:环状替换)
JAVA
class Solution {
public void rotate(int[] nums, int k) {
int n = nums.length;
k %= n;
// 一共有 gcd(k, n) 个独立环,每个环走一遍即可覆盖全部位置
int cycles = gcd(k, n);
for (int start = 0; start < cycles; start++) {
int cur = start;
int prev = nums[start]; // 手中正要放入下一位置的值
do {
int next = (cur + k) % n;
int temp = nums[next];
nums[next] = prev;
prev = temp;
cur = next;
} while (cur != start);
}
}
private int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a % b);
}
}
参考代码(解法三:三次反转,推荐)
JAVA
class Solution {
public void rotate(int[] nums, int k) {
int n = nums.length;
// k 可能 ≥ n,取模后等价于实际需要右移的步数
k %= n;
// 三次反转:整体 → 前 k → 后 n-k
// 例:[1,2,3,4,5,6,7], k=3
// 整体反转 → [7,6,5,4,3,2,1]
// 前 3 反转 → [5,6,7,4,3,2,1]
// 后 4 反转 → [5,6,7,1,2,3,4]
reverse(nums, 0, n - 1);
reverse(nums, 0, k - 1);
reverse(nums, k, n - 1);
}
// 原地反转闭区间 [left, right]
private void reverse(int[] nums, int left, int right) {
while (left < right) {
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
left++;
right--;
}
}
}
复杂度
解法一:时间 O(n),空间 O(n)
解法二:时间 O(n),空间 O(1)
解法三:时间 O(n),空间 O(1)(推荐面试主写)
238. 除自身以外数组的乘积 (Product of Array Except Self)
数组前缀和中等
题目
给你一个整数数组 nums,返回数组 answer,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。
题目数据保证任意前缀/后缀乘积都在 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 <= 10^5-30 <= nums[i] <= 30保证
answer[i]在 32 位整数范围内
进阶:能否在 O(1) 额外空间完成?(输出数组不计入额外空间)
思路简述answer[i] = 左侧所有元素积 × 右侧所有元素积,且不能用除法。
基础版:开两个数组
left/right,分别存严格左侧、严格右侧前缀积,再相乘得到答案;空间 O(n)。进阶版(O(1) 额外空间):先把左侧积直接写入输出数组
ans,再用一个变量从右往左滚乘 后缀积 乘进ans[i]。输出数组不计入额外空间,故额外空间 O(1)。
参考代码(基础:左右乘积数组)
JAVA
class Solution {
public int[] productExceptSelf(int[] nums) {
int n = nums.length;
// left[i]:nums[0..i-1] 的乘积;right[i]:nums[i+1..n-1] 的乘积
int[] left = new int[n];
int[] right = new int[n];
int[] ans = new int[n];
left[0] = 1;
for (int i = 1; i < n; i++) {
left[i] = left[i - 1] * nums[i - 1];
}
right[n - 1] = 1;
for (int i = n - 2; i >= 0; i--) {
right[i] = right[i + 1] * nums[i + 1];
}
for (int i = 0; i < n; i++) {
ans[i] = left[i] * right[i];
}
return ans;
}
}
参考代码(进阶:O(1) 额外空间)
JAVA
class Solution {
public int[] productExceptSelf(int[] nums) {
int n = nums.length;
// ans 先存左侧积,最后再原地乘上右侧积;它是输出,不算额外空间
int[] ans = new int[n];
ans[0] = 1; // 下标 0 左侧为空,前缀积约定为 1
// 左 → 右:ans[i] = nums[0] * ... * nums[i-1]
for (int i = 1; i < n; i++) {
ans[i] = ans[i - 1] * nums[i - 1];
}
// suffixProduct:nums[i+1] * ... * nums[n-1],滚动维护右侧积
// 相当于把基础版的 right[] 压缩成一个变量
int suffixProduct = 1;
for (int i = n - 1; i >= 0; i--) {
ans[i] *= suffixProduct;
suffixProduct *= nums[i];
}
return ans;
}
}
复杂度
基础版:时间 O(n),空间 O(n)(两个辅助数组)
进阶版:时间 O(n),空间 O(1) 额外空间(不计输出数组
ans)
41. 缺失的第一个正数 (First Missing Positive)
数组哈希表困难
题目
给你一个未排序的整数数组 nums,请你找出其中没有出现的最小的正整数。
请实现时间复杂度 O(n)、只使用常数级额外空间的解法。
示例 1:
输入:nums = [1,2,0]
输出:3
解释:范围[1,2]中的数字都在数组中。
示例 2:
输入:nums = [3,4,-1,1]
输出:2
解释:1 在数组中,但 2 没有。
示例 3:
输入:nums = [7,8,9,11,12]
输出:1
解释:最小正数 1 没有出现。
提示:
1 <= nums.length <= 10^5-2^31 <= nums[i] <= 2^31 - 1
思路简述
答案一定在 [1, n+1]:若 1..n 都出现过,则答案是 n+1;否则是其中缺失的那个。把数组本身当哈希表,把值 x(1 ≤ x ≤ n)放到下标 x-1,即追求 nums[i] == i + 1。交换时用 while 直到当前位置放对或无法放置;最后再扫一遍,第一个不满足的位置 i 对应答案 i+1。
参考代码
JAVA
class Solution {
public int firstMissingPositive(int[] nums) {
int n = nums.length;
// 原地「哈希」:把值 x 放到下标 x-1(仅处理 1..n 范围内的数)
for (int i = 0; i < n; i++) {
// nums[i] 在 [1,n] 内,且目标坑位上还不是 nums[i] 本身(避免死循环)
// 例:nums[i]=3 应换到下标 2;若 nums[2] 已是 3 则停止
while (nums[i] >= 1 && nums[i] <= n && nums[nums[i] - 1] != nums[i]) {
int targetIndex = nums[i] - 1; // 值 nums[i] 应去的下标
int temp = nums[targetIndex];
nums[targetIndex] = nums[i];
nums[i] = temp;
}
}
// 第一个不满足 nums[i] == i+1 的位置,缺失的就是 i+1
for (int i = 0; i < n; i++) {
if (nums[i] != i + 1) {
return i + 1;
}
}
// 1..n 全都在,答案只能是 n+1
return n + 1;
}
}
复杂度
时间:O(n),每个数最多被交换到正确位置一次
空间:O(1),原地修改,仅用常数临时变量
6. 矩阵 (Matrix)
核心思想: 二维数组上的模拟、原地标记、旋转与遍历顺序控制;搜索题可结合从角落出发的消除法或二分。
题目列表
73. 矩阵置零 (Set Matrix Zeroes) ✅ 2026-08-13 标签:数组, 哈希表, 矩阵
54. 螺旋矩阵 (Spiral Matrix) ✅ 2026-08-13 标签:数组, 矩阵, 模拟
48. 旋转图像 (Rotate Image) ✅ 2026-08-13 标签:数组, 数学, 矩阵
240. 搜索二维矩阵 II (Search a 2D Matrix II) ⭐ ✅ 2026-08-13 标签:数组, 二分查找, 分治, 矩阵
73. 矩阵置零 (Set Matrix Zeroes)
数组哈希表矩阵中等
题目
给定一个 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.lengthn == matrix[0].length1 <= m, n <= 200-2^31 <= matrix[i][j] <= 2^31 - 1
进阶:O(mn) / O(m+n) / O(1) 额外空间,你能做到常量空间吗?
思路简述
不能边扫边置零(会污染后续判断)。常见三档:
O(mn):先拷贝一份矩阵,按原图里的 0 去改拷贝(或记录所有 0 的坐标再改)。
O(m+n):用两个布尔数组标记「哪些行/列需要清零」,扫完再统一置零。
O(1) 进阶(推荐):用 第一行 / 第一列当标记位;另用一个变量记「原第一列是否本就有 0」。先扫非第一列,把行首/列首打成 0;再从右下往左上根据标记置零,最后处理第一列,避免标记被提前覆盖。
参考代码(进阶:O(1) 额外空间)
JAVA
class Solution {
public void setZeroes(int[][] matrix) {
int m = matrix.length;
int n = matrix[0].length;
// isCol0Zero:原矩阵第一列是否已经出现过 0
// (第一列要单独记,因为 matrix[i][0] 还要兼作「第 i 行」的标记)
boolean isCol0Zero = false;
for (int i = 0; i < m; i++) {
if (matrix[i][0] == 0) {
isCol0Zero = true;
}
// j 从 1 起:用第 0 列当行标记,故不在这里改写「第一列是否该清」以外的逻辑
for (int j = 1; j < n; j++) {
if (matrix[i][j] == 0) {
// 行首、列首打标:表示整行 i / 整列 j 稍后要清零
matrix[i][0] = 0;
matrix[0][j] = 0;
}
}
}
// 倒序填写,先用完第一行/第一列上的标记,再改它们自己
for (int i = m - 1; i >= 0; i--) {
for (int j = n - 1; j >= 1; j--) {
if (matrix[i][0] == 0 || matrix[0][j] == 0) {
matrix[i][j] = 0;
}
}
if (isCol0Zero) {
matrix[i][0] = 0;
}
}
}
}
复杂度
时间:O(mn),常数遍扫描矩阵
空间:O(1) 额外空间(进阶写法);若用行列标记数组则为 O(m+n)
54. 螺旋矩阵 (Spiral Matrix)
数组矩阵模拟中等
题目
给你一个 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.lengthn == matrix[i].length1 <= m, n <= 10-100 <= matrix[i][j] <= 100
思路简述
维护矩形边界 top / bottom / left / right,按 右 → 下 → 左 → 上 一圈圈剥离。每走完一条边就收缩对应边界;若收缩后上下或左右交叉,说明已经走完,立刻结束。注意单行/单列时,后两条边可能走不到,靠边界判断 break 即可。
参考代码
JAVA
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> ans = new ArrayList<>();
// 当前仍未遍历的矩形边界(闭区间)
int top = 0;
int bottom = matrix.length - 1;
int left = 0;
int right = matrix[0].length - 1;
while (true) {
// 1. 从左到右走完上边,然后上边界下移
for (int j = left; j <= right; j++) {
ans.add(matrix[top][j]);
}
if (++top > bottom) {
break;
}
// 2. 从上到下走完右边,然后右边界左移
for (int i = top; i <= bottom; i++) {
ans.add(matrix[i][right]);
}
if (--right < left) {
break;
}
// 3. 从右到左走完下边,然后下边界上移
for (int j = right; j >= left; j--) {
ans.add(matrix[bottom][j]);
}
if (--bottom < top) {
break;
}
// 4. 从下到上走完左边,然后左边界右移
for (int i = bottom; i >= top; i--) {
ans.add(matrix[i][left]);
}
if (++left > right) {
break;
}
}
return ans;
}
}
复杂度
时间:O(mn),每个元素恰好访问一次
空间:O(1) 额外空间(不计返回列表
ans)
48. 旋转图像 (Rotate Image)
数组数学矩阵中等
题目
给定一个 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].length1 <= n <= 20-1000 <= matrix[i][j] <= 1000
思路简述
顺时针 90° 可拆成两步:先 转置(matrix[i][j] ↔ matrix[j][i]),再 左右翻转每一行。转置时只遍历上三角(j 从 i 起)避免重复交换。全程原地完成。
参考代码
JAVA
class Solution {
public void rotate(int[][] matrix) {
int n = matrix.length;
// 1. 转置:沿主对角线交换,只扫上三角以免换两次又换回去
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
int temp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = temp;
}
}
// 2. 每一行左右翻转:转置后再翻转 = 顺时针 90°
for (int i = 0; i < n; i++) {
int left = 0;
int right = n - 1;
while (left < right) {
int temp = matrix[i][left];
matrix[i][left] = matrix[i][right];
matrix[i][right] = temp;
left++;
right--;
}
}
}
}
复杂度
时间:O(n²),转置与逐行翻转各扫一遍矩阵
空间:O(1),原地修改
240. 搜索二维矩阵 II (Search a 2D Matrix II)
数组二分查找分治矩阵中等
题目
编写一个高效算法,搜索 m x n 矩阵 matrix 中的目标值 target。矩阵特性:
每行从左到右升序
每列从上到下升序
示例 1:

输入:matrix =
[[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]],target = 5
输出:true
示例 2:

输入:matrix =
[[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]],target = 20
输出:false
提示:
m == matrix.lengthn == matrix[i].length1 <= n, m <= 300-10^9 <= matrix[i][j] <= 10^9每行从左到右升序,每列从上到下升序
-10^9 <= target <= 10^9
思路简述
从 右上角(或左下角)出发做「消除」:当前位置大于 target 则整列不可能(只能往左),小于 target 则整行不可能(只能往下)。每次排除一行或一列,最坏 O(m+n)。
参考代码
JAVA
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
int m = matrix.length;
int n = matrix[0].length;
// 从右上角出发:左边更小,下边更大,便于二分支决策
int row = 0;
int col = n - 1;
while (row < m && col >= 0) {
int cur = matrix[row][col];
if (cur == target) {
return true;
}
if (cur < target) {
// 当前值偏小:本行左侧只会更小,整行可排除,下移一行
row++;
} else {
// 当前值偏大:本列下方只会更大,整列可排除,左移一列
col--;
}
}
return false;
}
}
复杂度
时间:O(m + n),每次循环至少排除一行或一列
空间:O(1)
7. 链表 (Linked List)
核心思想: 指针操作要细心;虚头节点简化边界;快慢指针处理环 / 中点 / 倒数第 k;递归适合「子问题结构相同」的翻转与合并。
题目列表
160. 相交链表 (Intersection of Two Linked Lists) ✅ 2026-08-13 标签:哈希表, 链表, 双指针
206. 反转链表 (Reverse Linked List) ⭐ ✅ 2026-08-13 标签:递归, 链表
234. 回文链表 (Palindrome Linked List) ✅ 2026-08-13 标签:栈, 递归, 链表, 双指针
141. 环形链表 (Linked List Cycle) ✅ 2026-08-13 标签:哈希表, 链表, 双指针
142. 环形链表 II (Linked List Cycle II) ⭐ ✅ 2026-08-13 标签:哈希表, 链表, 双指针
21. 合并两个有序链表 (Merge Two Sorted Lists) ✅ 2026-08-13 标签:递归, 链表
2. 两数相加 (Add Two Numbers) ✅ 2026-08-13 标签:递归, 链表, 数学
19. 删除链表的倒数第 N 个结点 (Remove Nth Node From End of List) ✅ 2026-08-13 标签:链表, 双指针
24. 两两交换链表中的节点 (Swap Nodes in Pairs) ✅ 2026-08-13 标签:递归, 链表
25. K 个一组翻转链表 (Reverse Nodes in k-Group) ⭐ 🌟 ✅ 2026-08-13 标签:递归, 链表
138. 随机链表的复制 (Copy List with Random Pointer) ✅ 2026-08-13 标签:哈希表, 链表
148. 排序链表 (Sort List) 🌟 ✅ 2026-08-13 标签:链表, 双指针, 分治, 排序, 归并排序
23. 合并 K 个升序链表 (Merge k Sorted Lists) ⭐ ✅ 2026-08-13 标签:链表, 分治, 堆(优先队列), 归并排序
146. LRU 缓存 (LRU Cache) ⭐ 🌟 ✅ 2026-08-13 标签:设计, 哈希表, 链表, 双向链表
160. 相交链表 (Intersection of Two Linked Lists)
哈希表链表双指针简单
题目
给你两个单链表的头节点 headA 和 headB,请找出并返回两个单链表相交的起始节点;若不存在相交,返回 null。
图示两个链表在节点 c1 开始相交:

题目数据保证整个链式结构中不存在环。注意:函数返回后,链表必须保持原始结构。
示例 1:

输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点值为 8。A 中相交前有 2 个节点,B 中有 3 个。注意值为 1 的节点在 A、B 中是不同对象;值为 8 的节点才是同一内存地址。
示例 2:

输入:intersectVal = 2, listA = [1,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Intersected at '2'
示例 3:

输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:No intersection
解释:两链表不相交,返回null。
提示:
listA 节点数为
m,listB 为n1 <= m, n <= 3 * 10^41 <= Node.val <= 10^50 <= skipA <= m,0 <= skipB <= n无交点时
intersectVal = 0;有交点时intersectVal == listA[skipA] == listB[skipB]
进阶:能否做到时间 O(m + n)、空间 O(1)?
思路简述
双指针「换头」走:pA 从 A 走完接到 B,pB 从 B 走完接到 A。若相交,两人走过的总长都是 a + b + c(各自独有段 + 公共段),会在 同一交点相遇;若不相交,最终同时走到 null。不破坏原链表结构,空间 O(1)。
参考代码
JAVA
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) {
return null;
}
// pA / pB:分别从 A、B 出发;走完本链后切换到另一条链的头
ListNode pA = headA;
ListNode pB = headB;
// 相交时在交点相遇;不相交时同时变为 null,循环结束
while (pA != pB) {
// 走到末尾(null)就换到另一条链表头部,相当于补齐长度差
pA = (pA == null) ? headB : pA.next;
pB = (pB == null) ? headA : pB.next;
}
// ans:相交起始节点;不相交时为 null(此时 pA == pB == null)
ListNode ans = pA;
return ans;
}
}
复杂度
时间:O(m + n),最坏各走完两条链
空间:O(1),仅两个指针
206. 反转链表 (Reverse Linked List)
递归链表简单
题目
给你单链表的头节点 head,请反转链表,并返回反转后的链表。
示例 1:

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

输入:head = [1,2]
输出:[2,1]
示例 3:
输入:head = []
输出:[]
提示:
节点数目范围
[0, 5000]-5000 <= Node.val <= 5000
进阶:链表可以选用迭代或递归方式完成反转。你能否用两种方法解决这道题?
思路简述
进阶要求两种写法,核心都是改 next 指向:
递归(尾插):先「递」到末尾,把末节点当作新头
ans;再在「归」时把当前head接到已反转段的末尾(即原来的head.next),并断开head.next防成环。迭代(头插):用
prev/curr边走边把curr.next指回prev,相当于把当前节点不断插到新链表头部;结束后prev即新头。
参考代码(解法一:递归)
JAVA
class Solution {
// 「递」到末尾拿到新头;「归」时把经过的节点依次尾插到已反转链表末尾
public ListNode reverseList(ListNode head) {
// 空链表或只剩一个节点:已是反转结果(也是递的终点)
if (head == null || head.next == null) {
return head;
}
// ans:反转后的新头(原链表末节点),一路从递归深处传回来
ListNode ans = reverseList(head.next);
// tail:当前已反转段的末尾;递回来时 head.next 正好指向它
ListNode tail = head.next;
tail.next = head; // 把 head 尾插到已反转段后面
head.next = null; // 必须断开,否则 head 与原后继互相指向成环
return ans;
}
}
复杂度(递归)
时间:O(n),每个节点访问一次
空间:O(n),递归栈深度为链表长度
参考代码(解法二:迭代)
JAVA
class Solution {
public ListNode reverseList(ListNode head) {
// prev:已反转部分的头;初始为 null,表示新链表暂空
ListNode prev = null;
// curr:正在处理的节点
ListNode curr = head;
while (curr != null) {
// next:先存后继,否则改 next 后会丢后面整段
ListNode next = curr.next;
curr.next = prev; // 当前节点头插到已反转链表
prev = curr; // prev 前进为新的「已反转头」
curr = next; // 继续处理原后继
}
// ans:反转后的头节点(原链表末节点);curr 已走到 null
ListNode ans = prev;
return ans;
}
}
复杂度(迭代)
时间:O(n),单次遍历
空间:O(1),仅常数个指针
234. 回文链表 (Palindrome Linked List)
栈递归链表双指针简单
题目
给你一个单链表的头节点 head,请判断该链表是否为回文链表。是则返回 true,否则返回 false。
示例 1:

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

输入:head = [1,2]
输出:false
提示:
节点数目在范围
[1, 10^5]内0 <= Node.val <= 9
进阶:能否用 O(n) 时间、O(1) 空间解决?
思路简述
进阶目标是 O(1) 空间,不能整表拷到数组再双指针。做法:快慢指针找中点 → 反转后半段 → 从前半、后半(反转后)同步比 val。奇数长度时中点会落在正中,反转后中点会与自身比一次,不影响结果;任一处不等即非回文。
参考代码
JAVA
class Solution {
public boolean isPalindrome(ListNode head) {
// mid:后半段起点(偶数时为右半第一个;奇数时为正中节点)
ListNode mid = findMid(head);
// head2:反转后半段后的新头,从尾向中逐个对齐
ListNode head2 = reverse(mid);
// ans:是否回文;默认 true,发现不等再改 false
boolean ans = true;
ListNode p1 = head;
ListNode p2 = head2;
// 只以后半长度为准:偶数比完右半即停;奇数多比一次中点与自身
while (p2 != null) {
if (p1.val != p2.val) {
ans = false;
break;
}
p1 = p1.next;
p2 = p2.next;
}
return ans;
}
// 快慢指针:fast 走两步、slow 走一步,fast 到末尾时 slow 在中点/右半起点
private ListNode findMid(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
// 迭代反转:把 curr 不断头插到 prev 前面,返回新头
private ListNode reverse(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode next = curr.next; // 先存后继,避免断链后丢后面
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
}
}
复杂度
时间:O(n),找中点、反转、比较各一遍
空间:O(1),只用若干指针(满足进阶)
141. 环形链表 (Linked List Cycle)
哈希表链表双指针简单
题目
给你一个链表的头节点 head,判断链表中是否有环。
若某节点可通过连续跟踪 next 再次到达,则存在环。评测用整数 pos 表示尾部接到的位置(从 0 起);pos 不传入函数,仅说明结构。有环返回 true,否则 false。
示例 1:

输入:head = [3,2,0,-4], pos = 1
输出:true
解释:尾部连接到第二个节点(值 2)。
示例 2:

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

输入:head = [1], pos = -1
输出:false
解释:无环。
提示:
节点数目范围
[0, 10^4]-10^5 <= Node.val <= 10^5pos为-1或链表中的有效索引
进阶:能否用 O(1) 内存解决?
思路简述
Floyd 判圈:slow 一次一步、fast 一次两步。无环时 fast 先到 null;有环时快指针会在环内追上慢指针,相遇即有环。只需两个指针,满足进阶 O(1) 空间。
参考代码
JAVA
public class Solution {
public boolean hasCycle(ListNode head) {
// 空链或单节点且无自环:不可能形成环
if (head == null || head.next == null) {
return false;
}
// slow / fast:同起点;slow 走 1、fast 走 2
ListNode slow = head;
ListNode fast = head;
// ans:是否存在环
boolean ans = false;
// fast 能走到 null 说明无环;能相遇说明有环
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
ans = true;
break;
}
}
return ans;
}
}
复杂度
时间:O(n),无环遍历整表;有环时在环内有限步内相遇
空间:O(1),仅快慢两个指针
142. 环形链表 II (Linked List Cycle II)
哈希表链表双指针中等
题目
给定链表头节点 head,返回开始入环的第一个节点;无环则返回 null。
评测用 pos 表示尾部接到的位置(从 0 起);pos 不传入函数。不允许修改链表。
示例 1:

输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:尾部连接到第二个节点(值 2)。
示例 2:

输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:尾部连接到第一个节点。
示例 3:

输入:head = [1], pos = -1
输出:null
解释:无环。
提示:
节点数目范围
[0, 10^4]-10^5 <= Node.val <= 10^5pos为-1或有效索引
进阶:能否用 O(1) 空间解决?
思路简述
在 141 判圈基础上再找入口。设环外长 a、环长 c,相遇时慢指针在环内走了 b:有 2(a+b)=a+b+kc ⇒ a=kc-b,即「头到入口」与「相遇点沿环走到入口」等距。因此:快慢相遇后,把一指针放回 head,两指针各走一步,再次相遇处即为入环点。全程 O(1) 空间。
参考代码
JAVA
public class Solution {
public ListNode detectCycle(ListNode head) {
if (head == null || head.next == null) {
return null;
}
// slow / fast:同起点判圈;相遇后用于找入口
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
// 相遇:把 slow 拉回头部,与 fast(仍在相遇点)同步各走 1 步
slow = head;
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
// ans:入环第一个节点(两指针再次相遇处)
ListNode ans = slow;
return ans;
}
}
// 快指针走到末尾:无环
return null;
}
}
复杂度
时间:O(n),判圈 + 找入口各线性遍
空间:O(1),仅快慢两个指针
21. 合并两个有序链表 (Merge Two Sorted Lists)
递归链表简单
题目
将两个升序链表合并为一个新的升序链表并返回。新链表由拼接给定两个链表的所有节点组成。
示例 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 <= 100l1、l2均按非递减顺序排列
思路简述
用 哑节点 dummy 挂结果链,curr 指向当前尾。两指针谁小接谁,接到非空的那条;一条走完后把另一条剩余整段直接接上。原地改 next,不新建值节点。
参考代码
JAVA
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
// dummy:虚头,避免单独处理「第一个节点」;最终 ans = dummy.next
ListNode dummy = new ListNode();
// curr:已合并段的尾节点,下一跳接到较小的那个
ListNode curr = dummy;
// 两边都还有节点:每次取较小者接到 curr 后面
while (list1 != null && list2 != null) {
if (list1.val > list2.val) {
curr.next = list2;
list2 = list2.next;
} else {
// 相等时优先接 list1,顺序稳定即可
curr.next = list1;
list1 = list1.next;
}
curr = curr.next;
}
// 一条已空:把另一条剩余整段直接挂上(可能为 null)
curr.next = (list1 == null) ? list2 : list1;
// ans:合并后的真正头节点
ListNode ans = dummy.next;
return ans;
}
}
复杂度
时间:O(m + n),每个节点恰被处理一次
空间:O(1),仅哑节点与若干指针(不计输出链表本身)
2. 两数相加 (Add Two Numbers)
递归链表数学中等
题目
给你两个非空链表,表示两个非负整数。每位数字按逆序存储,每个节点只存一位。请将两数相加,并以相同形式返回表示和的链表。可假设除数字 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列表表示的数字不含前导零
思路简述
逆序存储等价于从个位对齐相加。用哑节点建结果链,维护 进位 carry:每位 sum = carry + val1 + val2,新节点写 sum % 10,carry = sum / 10。循环条件含 carry != 0,保证最高位进位(如 999+1)也能多出一节点。某链较短时缺位按 0 处理。
参考代码
JAVA
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
// dummy:虚头;curr 指向当前结果尾,便于尾插
ListNode dummy = new ListNode();
ListNode curr = dummy;
// carry:来自上一位的进位,取值 0 或 1(两位最多 9+9+1)
int carry = 0;
// 只要还有数字或进位未消完就继续;短链缺位按 0
while (carry != 0 || l1 != null || l2 != null) {
int val1 = (l1 == null) ? 0 : l1.val;
int val2 = (l2 == null) ? 0 : l2.val;
int total = carry + val1 + val2;
carry = total / 10; // 新进位
curr.next = new ListNode(total % 10); // 本位写入结果
curr = curr.next;
if (l1 != null) {
l1 = l1.next;
}
if (l2 != null) {
l2 = l2.next;
}
}
// ans:和链表的真正头(个位)
ListNode ans = dummy.next;
return ans;
}
}
复杂度
时间:O(max(m, n)),与较长链长度同阶
空间:O(1) 额外(不计输出链表;输出最长 max(m,n)+1)
19. 删除链表的倒数第 N 个结点 (Remove Nth Node From End of List)
链表双指针中等
题目
给你一个链表,删除链表的倒数第 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 <= 300 <= Node.val <= 1001 <= n <= sz
进阶:能否一趟扫描实现?
思路简述
哑节点 + 双指针一趟删:fast 先走 n 步,再与 slow 同步前进,直到 fast 到末尾;此时 slow 恰在待删结点的前驱。slow.next = slow.next.next 跳过目标。哑节点统一处理删头结点的情况,返回 dummy.next。
参考代码
JAVA
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
// dummy:虚头接在 head 前,删头时也能用同一套逻辑
ListNode dummy = new ListNode(0, head);
// slow / fast:同从 dummy 出发;最终 slow 停在待删结点前驱
ListNode slow = dummy;
ListNode fast = dummy;
// fast 先拉开 n 步距离(此时 fast 与 slow 之间隔 n 个结点)
while (n > 0) {
fast = fast.next;
n--;
}
// 同步前进,直到 fast 到达最后一个结点
while (fast.next != null) {
fast = fast.next;
slow = slow.next;
}
// 跳过倒数第 n 个结点
slow.next = slow.next.next;
// ans:可能已变的新头(原头被删时为原第二个结点)
ListNode ans = dummy.next;
return ans;
}
}
复杂度
时间:O(sz),一趟扫描
空间:O(1),仅哑节点与双指针
24. 两两交换链表中的节点 (Swap Nodes in Pairs)
递归链表中等
题目
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。必须在不修改节点内部值的情况下完成(只能进行节点交换)。
示例 1:

输入:head = [1,2,3,4]
输出:[2,1,4,3]
示例 2:
输入:head = []
输出:[]
示例 3:
输入:head = [1]
输出:[1]
提示:
节点数目在范围
[0, 100]内0 <= Node.val <= 100
思路简述
哑节点 + 迭代改指针:prev 指向待交换对的前驱,取出 first / second,按 prev → second → first → 后续 重接,再把 prev 移到 first(已交换后的对尾),继续下一对。剩单个结点时循环条件不满足,自然不动。
参考代码
JAVA
class Solution {
public ListNode swapPairs(ListNode head) {
// dummy:虚头,统一处理「头两结点互换」后新头变化的情况
ListNode dummy = new ListNode(0, head);
// prev:当前待交换这一对的前驱
ListNode prev = dummy;
// 至少还有两个结点才交换;否则剩 0/1 个保持原样
while (prev.next != null && prev.next.next != null) {
// first / second:待交换的相邻两结点
ListNode first = prev.next;
ListNode second = first.next;
// 重接:prev → second → first → (原 second 之后)
prev.next = second;
first.next = second.next;
second.next = first;
// 下一对的前驱变为交换后的 first(现位于对尾)
prev = first;
}
// ans:交换后的头(可能仍是原 head,也可能是原第二个结点)
ListNode ans = dummy.next;
return ans;
}
}
复杂度
时间:O(n),每对结点常数次指针操作
空间:O(1),仅哑节点与若干指针
25. K 个一组翻转链表 (Reverse Nodes in k-Group)
递归链表困难
题目
给你链表头节点 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 <= 50000 <= Node.val <= 1000
进阶:能否只用 O(1) 额外内存空间?
思路简述
先数满 k 个再翻;不足 k 则整段原样返回。
递归:当前段够
k个则头插反转这k个,原head变成段尾,再把head.next接到 对剩余链表递归的结果;返回本段新头prev。写法清晰,栈深 O(n/k)。迭代(进阶 O(1)):哑节点串起结果;每次用指针探够
k个,不够则停;够则在组内反转,把前驱接到组新头、组旧头接到下一段,再推进前驱继续。
参考代码(解法一:递归)
JAVA
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
if (head == null) {
return head;
}
// 先探:从 head 起是否还有至少 k 个结点
ListNode curr = head;
int count = 0;
while (curr != null && count < k) {
count++;
curr = curr.next;
}
// 不足 k:剩余保持原序,直接返回本段原头
if (count < k) {
return head;
}
// 反转当前这 k 个:头插,结束后 prev 为本段新头,curr 为下一段起点
ListNode prev = null;
curr = head;
for (int i = 0; i < k; i++) {
ListNode next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
// 原 head 已成段尾,接到「后面各段递归翻转」的结果
head.next = reverseKGroup(curr, k);
// ans:本段翻转后的新头
ListNode ans = prev;
return ans;
}
}
复杂度(递归)
时间:O(n),每个结点参与常数次操作
空间:O(n/k),递归层数约分组数
参考代码(解法二:迭代,O(1) 额外空间)
JAVA
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
// dummy:虚头,方便连接每一组翻转后的新头
ListNode dummy = new ListNode(0, head);
// groupPrev:上一组的尾(也是下一组的前驱)
ListNode groupPrev = dummy;
while (true) {
// kth:从 groupPrev 再走 k 步,得到本组最后一个结点
ListNode kth = groupPrev;
for (int i = 0; i < k; i++) {
kth = kth.next;
if (kth == null) {
// 不足 k 个:后面保持原序,直接结束
ListNode ans = dummy.next;
return ans;
}
}
// groupNext:下一组起点;groupHead:本组原头(翻转后变组尾)
ListNode groupNext = kth.next;
ListNode groupHead = groupPrev.next;
// 反转 [groupHead, kth]:断开与 groupNext 的连接后头插
ListNode prev = groupNext;
ListNode curr = groupHead;
while (curr != groupNext) {
ListNode next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
// 前驱接到本组新头 kth;原头变成组尾,作为下一组前驱
groupPrev.next = kth;
groupPrev = groupHead;
}
}
}
复杂度(迭代)
时间:O(n)
空间:O(1) 额外,满足进阶
138. 随机链表的复制 (Copy List with Random Pointer)
哈希表链表中等
题目
给你长度为 n 的链表,每个节点额外有一个 random 指针,可指向链表中任意节点或空。请构造该链表的深拷贝:恰好 n 个全新节点,值与对应原节点相同;新链表的 next / random 只指向复制链表中的节点,且与原链表状态一致。返回复制链表的头节点。
输入/输出用 [val, random_index] 表示每个节点;random_index 为指向节点的下标,或 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-10^4 <= Node.val <= 10^4Node.random为null或指向链表中的节点
思路简述
不用哈希表也能 O(1) 额外空间:三步交错法。① 在每个原节点后插入其副本,形成 A → A' → B → B' → …;② 因 A'.next 即 A',有 A'.random = A.random.next(A.random 为空则副本 random 也为空);③ 再拆链:奇偶交错恢复原链表,抽出副本链。
参考代码
JAVA
class Solution {
public Node copyRandomList(Node head) {
if (head == null) {
return null;
}
// 1. 交错插入副本:A → A' → B → B' → …
Node curr = head;
while (curr != null) {
Node copy = new Node(curr.val);
copy.next = curr.next;
curr.next = copy;
curr = copy.next; // 跳到下一个原节点
}
// 2. 给副本挂 random:副本紧跟原节点,故原.random 的副本 = 原.random.next
curr = head;
while (curr != null) {
Node copy = curr.next;
copy.random = (curr.random == null) ? null : curr.random.next;
curr = copy.next;
}
// 3. 拆分:恢复原链表 next,同时串起副本链
// ans:复制链表头(原 head 的紧后副本)
Node ans = head.next;
curr = head;
while (curr != null) {
Node copy = curr.next;
// 原节点接回下一个原节点(跳过副本)
curr.next = copy.next;
// 副本接到下一个副本(若还有)
copy.next = (curr.next == null) ? null : curr.next.next;
curr = curr.next;
}
return ans;
}
}
复杂度
时间:O(n),三遍扫描
空间:O(1) 额外(不计新建的 n 个副本节点)
148. 排序链表 (Sort List)
链表双指针分治排序归并排序中等
题目
给你链表的头结点 head,请将其按升序排列并返回排序后的链表。
示例 1:

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

输入:head = [-1,5,3,4,0]
输出:[-1,0,3,4,5]
示例 3:
输入:head = []
输出:[]
提示:
节点数目在范围
[0, 5 * 10^4]内-10^5 <= Node.val <= 10^5
进阶:能否在 O(n log n) 时间、常数级空间下完成?
思路简述
自底向上归并:step 从 1 倍增到覆盖全长。每轮把链表切成若干长为 step 的有序段,两两合并成长 2·step 的段,接到哨兵后的新链上。相对递归归并(找中点拆半),迭代版 额外空间 O(1),满足进阶。合并复用「21. 合并两个有序链表」,并返回头尾便于接链。
参考代码
JAVA
class Solution {
public ListNode sortList(ListNode head) {
int length = getListLength(head);
// dummy:整表前驱,方便每轮把合并结果重新挂回
ListNode dummy = new ListNode(0, head);
// step:本轮参与合并的有序段长度;每轮结束后有序段变长为 2*step
for (int step = 1; step < length; step *= 2) {
// newListTail:本轮已合并部分的尾,下一段合并结果接在其后
ListNode newListTail = dummy;
// cur:尚未处理的剩余链表起点
ListNode cur = dummy.next;
while (cur != null) {
// head1 / head2:切出两段长至多为 step 的链表
ListNode head1 = cur;
ListNode head2 = splitList(head1, step);
cur = splitList(head2, step); // 下一段起点(可能为 null)
// merged[0]/合并后头;merged[1]=合并后尾
ListNode[] merged = mergeTwoLists(head1, head2);
newListTail.next = merged[0];
newListTail = merged[1];
}
}
// ans:排序后的链表头
ListNode ans = dummy.next;
return ans;
}
private int getListLength(ListNode head) {
int length = 0;
while (head != null) {
length++;
head = head.next;
}
return length;
}
// 合并两条有序链表,返回 {头, 尾},便于外层 O(1) 接上一段
private ListNode[] mergeTwoLists(ListNode list1, ListNode list2) {
ListNode dummy = new ListNode();
ListNode cur = dummy;
while (list1 != null && list2 != null) {
if (list1.val < list2.val) {
cur.next = list1;
list1 = list1.next;
} else {
cur.next = list2;
list2 = list2.next;
}
cur = cur.next;
}
cur.next = (list1 != null) ? list1 : list2;
while (cur.next != null) {
cur = cur.next; // 走到合并结果真正的尾
}
return new ListNode[]{dummy.next, cur};
}
// 从 head 起切出前 size 个结点并断开;返回剩余段头。长度 ≤ size 则不切,返回 null
private ListNode splitList(ListNode head, int size) {
if (head == null) {
return null;
}
ListNode cur = head;
for (int i = 0; i < size - 1 && cur != null; i++) {
cur = cur.next;
}
if (cur == null || cur.next == null) {
return null;
}
ListNode nextHead = cur.next;
cur.next = null; // 断开,使前 size 个成为独立链表
return nextHead;
}
}
复杂度
时间:O(n log n),共 ⌈log n⌉ 轮,每轮合并扫一遍 n 个结点
空间:O(1) 额外(自底向上,满足进阶)
23. 合并 K 个升序链表 (Merge k Sorted Lists)
链表分治堆(优先队列)归并排序困难
题目
给你一个链表数组,每个链表已按升序排列。请将所有链表合并到一个升序链表中,返回合并后的链表。
示例 1:
输入:lists =
[[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组为
[
1->4->5,
1->3->4,
2->6
]合并后:
1->1->2->3->4->4->5->6
示例 2:
输入:lists = []
输出:[]
示例 3:
输入:lists =
[[]]
输出:[]
提示:
k == lists.length,0 <= k <= 10^40 <= lists[i].length <= 500-10^4 <= lists[i][j] <= 10^4lists[i]按升序排列所有
lists[i].length总和不超过10^4
思路简述
多路归并:用小根堆维护各链当前头结点。每次弹出最小结点接到结果尾,若该结点还有后继则再入堆。堆大小至多 k,每次弹出/插入 O(log k),共处理 N 个结点,故 O(N log k)。
参考代码
JAVA
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
// 空数组 / null:没有可合并的链
if (lists == null || lists.length == 0) {
return null;
}
// dummy / curr:结果链虚头与当前尾
ListNode dummy = new ListNode(0);
ListNode curr = dummy;
// pq:小根堆,存各链表「当前未合并的头」;比较结点 val
PriorityQueue<ListNode> pq = new PriorityQueue<>((a, b) -> a.val - b.val);
for (ListNode list : lists) {
if (list != null) {
pq.offer(list);
}
}
while (!pq.isEmpty()) {
// minNode:当前所有候选头中值最小者
ListNode minNode = pq.poll();
curr.next = minNode;
curr = curr.next;
// 该链还有后继则作为新候选入堆
if (minNode.next != null) {
pq.offer(minNode.next);
}
}
// ans:合并后的升序链表头
ListNode ans = dummy.next;
return ans;
}
}
复杂度
时间:O(N log k),
N为所有结点总数,k为链表条数空间:O(k),优先队列最多同时存 k 个结点指针
146. LRU 缓存 (LRU Cache)
设计哈希表链表双向链表中等
题目
请设计并实现满足 LRU(最近最少使用)约束的缓存。实现 LRUCache 类:
LRUCache(int capacity):以正整数容量初始化int get(int key):若key存在则返回其值,否则返回-1void put(int key, int value):若key已存在则更新值;否则插入。插入后若超出容量,逐出最久未使用的关键字
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 <= 30000 <= key <= 100000 <= value <= 10^5最多调用
2 * 10^5次get和put
思路简述
哈希表 + 双向链表:HashMap 保证 O(1) 定位结点;双向链表维护使用顺序——靠近假头为最近使用,靠近假尾为最久未用。get / 更新 put 时把结点 移到头部;容量满再插入则删尾并同步从 map 移除。假头假尾简化边界,避免空链特判。
参考代码
JAVA
class LRUCache {
// 双向链表结点:同时存 key,便于从尾删除时同步 map.remove
class DLinkNode {
int key;
int value;
DLinkNode prev;
DLinkNode next;
DLinkNode() {
}
DLinkNode(int key, int value) {
this.key = key;
this.value = value;
}
}
private final DLinkNode head; // 假头:head.next 为最近使用
private final DLinkNode tail; // 假尾:tail.prev 为最久未用
private int size;
private final int capacity;
// map:key → 对应链表结点,实现 O(1) 查找
private final Map<Integer, DLinkNode> map;
public LRUCache(int capacity) {
this.capacity = capacity;
this.size = 0;
this.map = new HashMap<>();
head = new DLinkNode();
tail = new DLinkNode();
head.next = tail;
tail.prev = head;
}
public int get(int key) {
DLinkNode node = map.get(key);
if (node == null) {
return -1;
}
moveToHead(node); // 访问即视为最近使用
return node.value;
}
public void put(int key, int value) {
DLinkNode node = map.get(key);
if (node != null) {
node.value = value;
moveToHead(node);
return;
}
// 新 key:头插;超容量则逐出最久未用(假尾前一个)
DLinkNode newNode = new DLinkNode(key, value);
addToHead(newNode);
map.put(key, newNode);
size++;
if (size > capacity) {
DLinkNode evicted = removeTail();
map.remove(evicted.key);
size--;
}
}
private void addToHead(DLinkNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
private void removeNode(DLinkNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void moveToHead(DLinkNode node) {
removeNode(node);
addToHead(node);
}
// 删除并返回最久未使用结点(tail 前驱)
private DLinkNode removeTail() {
DLinkNode node = tail.prev;
removeNode(node);
return node;
}
}
复杂度
时间:
get/put均摊 O(1)(哈希 + 常数次指针操作)空间:O(capacity),哈希表与链表结点各至多 capacity 个
8. 二叉树 (Binary Tree)
核心思想: DFS(前/中/后序)解决结构与路径问题,BFS 解决层序与视图问题;BST 题多用中序有序性质。
题目列表
94. 二叉树的中序遍历 (Binary Tree Inorder Traversal) ✅ 2026-08-13 标签:栈, 树, 深度优先搜索, 二叉树
104. 二叉树的最大深度 (Maximum Depth of Binary Tree) ✅ 2026-08-13 标签:树, 深度优先搜索, 广度优先搜索, 二叉树
226. 翻转二叉树 (Invert Binary Tree) ✅ 2026-08-13 标签:树, 深度优先搜索, 广度优先搜索, 二叉树
101. 对称二叉树 (Symmetric Tree) ✅ 2026-08-13 标签:树, 深度优先搜索, 广度优先搜索, 二叉树
543. 二叉树的直径 (Diameter of Binary Tree) ⭐ ✅ 2026-08-13 标签:树, 深度优先搜索, 二叉树
102. 二叉树的层序遍历 (Binary Tree Level Order Traversal) ⭐ ✅ 2026-08-13 标签:树, 广度优先搜索, 二叉树
108. 将有序数组转换为二叉搜索树 (Convert Sorted Array to Binary Search Tree) ✅ 2026-08-13 标签:树, 二叉搜索树, 数组, 分治, 二叉树
98. 验证二叉搜索树 (Validate Binary Search Tree) ⭐ ✅ 2026-08-13 标签:树, 深度优先搜索, 二叉搜索树, 二叉树
230. 二叉搜索树中第 K 小的元素 (Kth Smallest Element in a BST) ✅ 2026-08-13 标签:树, 深度优先搜索, 二叉搜索树, 二叉树
199. 二叉树的右视图 (Binary Tree Right Side View) ✅ 2026-08-13 标签:树, 深度优先搜索, 广度优先搜索, 二叉树
114. 二叉树展开为链表 (Flatten Binary Tree to Linked List) ✅ 2026-08-13 标签:栈, 树, 深度优先搜索, 链表, 二叉树
105. 从前序与中序遍历序列构造二叉树 (Construct Binary Tree from Preorder and Inorder Traversal) ⭐ 🌟 ✅ 2026-08-13 标签:树, 数组, 哈希表, 分治, 二叉树
437. 路径总和 III (Path Sum III) 🌟 ✅ 2026-08-13 标签:树, 深度优先搜索, 二叉树
236. 二叉树的最近公共祖先 (Lowest Common Ancestor of a Binary Tree) ⭐ ✅ 2026-08-13 标签:树, 深度优先搜索, 二叉树
124. 二叉树中的最大路径和 (Binary Tree Maximum Path Sum) ⭐ ✅ 2026-08-13 标签:树, 深度优先搜索, 动态规划, 二叉树
94. 二叉树的中序遍历 (Binary Tree Inorder Traversal)
栈树深度优先搜索二叉树简单
题目
给定二叉树根节点 root,返回它的中序遍历。
示例 1:

输入:root = [1,null,2,3]
输出:[1,3,2]
示例 2:
输入:root = []
输出:[]
示例 3:
输入:root = [1]
输出:[1]
提示:
节点数目在范围
[0, 100]内-100 <= Node.val <= 100
进阶:递归很简单,能否用迭代完成?
思路简述
中序顺序为「左 → 根 → 右」。递归最直观;进阶可用栈模拟,或用 Morris 线索遍历做到 O(1) 额外空间:有左子树时,找到左子树最右结点,第一次把它的 right 指回当前根(线索),再走左;第二次遇到同一线索则拆除,访问根并走右。
参考代码(解法一:Morris 线索,O(1) 额外空间)
JAVA
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
// ans:中序访问序列
List<Integer> ans = new ArrayList<>();
while (root != null) {
if (root.left != null) {
// prev:左子树中「中序意义下」的前驱,即左子树最右结点
TreeNode prev = root.left;
while (prev.right != null && prev.right != root) {
prev = prev.right;
}
if (prev.right == null) {
// 第一次到达:建立线索 prev.right → root,再深入左子树
prev.right = root;
root = root.left;
continue;
}
// 第二次到达:左子树已遍历完,拆除线索
prev.right = null;
}
// 无左子树,或左子树刚遍历完:访问当前根,再走右
ans.add(root.val);
root = root.right;
}
return ans;
}
}
复杂度(Morris)
时间:O(n),每个结点访问常数次
空间:O(1) 额外(不计答案列表;遍历中临时改指针,结束前已恢复)
参考代码(解法二:栈迭代)
JAVA
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> ans = new ArrayList<>();
// stack:待处理的祖先结点;沿左链压栈,弹出时访问再转右
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode cur = root;
while (cur != null || !stack.isEmpty()) {
while (cur != null) {
stack.push(cur);
cur = cur.left; // 一路向左
}
cur = stack.pop(); // 左空:访问根
ans.add(cur.val);
cur = cur.right; // 再处理右子树
}
return ans;
}
}
复杂度(栈迭代)
时间:O(n)
空间:O(h),
h为树高(最坏 O(n))
104. 二叉树的最大深度 (Maximum Depth of Binary Tree)
树深度优先搜索广度优先搜索二叉树简单
题目
给定二叉树 root,返回其最大深度。最大深度是从根节点到最远叶子节点的最长路径上的节点数。
示例 1:

输入:root = [3,9,20,null,null,15,7]
输出:3
示例 2:
输入:root = [1,null,2]
输出:2
提示:
节点数量在
[0, 10^4]区间内-100 <= Node.val <= 100
思路简述
DFS 递归:空树深度为 0;否则深度 = 左右子树深度的较大值 + 1(当前根占一层)。
参考代码
JAVA
class Solution {
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
// left / right:左右子树的最大深度
int left = maxDepth(root.left);
int right = maxDepth(root.right);
// ans:当前树的最大深度 = 较高子树深度 + 本层 1
int ans = Math.max(left, right) + 1;
return ans;
}
}
复杂度
时间:O(n),每个结点访问一次
空间:O(h),递归栈深度为树高(最坏 O(n))
226. 翻转二叉树 (Invert Binary Tree)
树深度优先搜索广度优先搜索二叉树简单
题目
给你一棵二叉树的根节点 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
思路简述
递归:先翻转左右子树,再 交换当前结点的左右指针;空结点直接返回。等价于对整棵树做镜像。
参考代码
JAVA
class Solution {
public TreeNode invertTree(TreeNode root) {
if (root == null) {
return null;
}
// left / right:翻转后的左右子树根
TreeNode left = invertTree(root.left);
TreeNode right = invertTree(root.right);
// 交换左右孩子,完成当前结点的镜像
root.left = right;
root.right = left;
// ans:翻转后仍以原 root 为根
TreeNode ans = root;
return ans;
}
}
复杂度
时间:O(n),每个结点访问一次
空间:O(h),递归栈深度为树高
101. 对称二叉树 (Symmetric Tree)
树深度优先搜索广度优先搜索二叉树简单
题目
给你一个二叉树的根节点 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
进阶:能否用递归和迭代两种方法解决?
思路简述
判断根的左右子树是否互为镜像:对应位置值相等,且一侧的左对另一侧的右、一侧的右对另一侧的左。递归直接比较两棵子树;迭代用队列成对取出结点,按 (左左, 右右) / (左右, 右左) 入队继续比。
参考代码(解法一:递归)
JAVA
class Solution {
public boolean isSymmetric(TreeNode root) {
if (root == null) {
return true;
}
// ans:左右子树是否互为镜像
boolean ans = check(root.left, root.right);
return ans;
}
// 判断 node1、node2 为根的两棵树是否镜像对称
private boolean check(TreeNode node1, TreeNode node2) {
if (node1 == null || node2 == null) {
// 只有两侧都空才对称;一空一非空则否
return node1 == null && node2 == null;
}
if (node1.val != node2.val) {
return false;
}
// 外侧对外侧、内侧对内侧
return check(node1.left, node2.right) && check(node1.right, node2.left);
}
}
复杂度(递归)
时间:O(n),每个结点至多看一次
空间:O(h),递归栈深度为树高
参考代码(解法二:队列迭代)
JAVA
class Solution {
public boolean isSymmetric(TreeNode root) {
if (root == null) {
return true;
}
// queue:成对存放待比较的镜像位置结点
Deque<TreeNode> queue = new ArrayDeque<>();
if (root.left != null) {
queue.offer(root.left);
}
if (root.right != null) {
queue.offer(root.right);
}
// 一侧有孩子、另一侧没有:队列长度必为奇数,直接不对称
if (queue.size() == 1) {
return false;
}
while (!queue.isEmpty()) {
TreeNode node1 = queue.poll();
TreeNode node2 = queue.poll();
if (node1.val != node2.val) {
return false;
}
// 外侧:node1.left ↔ node2.right
if (node1.left != null || node2.right != null) {
if (node1.left == null || node2.right == null) {
return false;
}
queue.offer(node1.left);
queue.offer(node2.right);
}
// 内侧:node1.right ↔ node2.left
if (node1.right != null || node2.left != null) {
if (node1.right == null || node2.left == null) {
return false;
}
queue.offer(node1.right);
queue.offer(node2.left);
}
}
boolean ans = true;
return ans;
}
}
复杂度(迭代)
时间:O(n)
空间:O(n),队列中结点数量
543. 二叉树的直径 (Diameter of Binary Tree)
树深度优先搜索二叉树简单
题目
给你一棵二叉树的根节点,返回该树的直径。直径是树中任意两节点之间最长路径的长度(边数),该路径可能经过也可能不经过根节点。
示例 1:

输入:root = [1,2,3,4,5]
输出:3
解释:路径[4,2,1,3]或[5,2,1,3],长度为 3。
示例 2:
输入:root = [1,2]
输出:1
提示:
节点数目在范围
[1, 10^4]内-100 <= Node.val <= 100
思路简述
DFS 求深度时顺带更新直径:经过某结点的最长路径边数 = 左子树深度 + 右子树深度。递归返回该结点深度 max(左, 右) + 1,全局取所有结点上该值的最大值。直径不一定过根,故每个结点都要算一遍。
参考代码
JAVA
class Solution {
// ans:全局最大直径(边数)
private int ans = 0;
public int diameterOfBinaryTree(TreeNode root) {
depth(root);
return ans;
}
// 返回以 root 为根的子树高度(结点数);空树为 0
private int depth(TreeNode root) {
if (root == null) {
return 0;
}
// left / right:左右子树高度
int left = depth(root.left);
int right = depth(root.right);
// 经过当前结点的路径边数 = 左高 + 右高
ans = Math.max(ans, left + right);
return Math.max(left, right) + 1;
}
}
复杂度
时间:O(n),每个结点访问一次
空间:O(h),递归栈深度为树高
102. 二叉树的层序遍历 (Binary Tree Level Order Traversal)
树广度优先搜索二叉树中等
题目
给你二叉树的根节点 root,返回其节点值的层序遍历(逐层、从左到右访问所有节点)。
示例 1:

输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]
示例 2:
输入:root = [1]
输出:1
示例 3:
输入:root = []
输出:[]
提示:
节点数目在范围
[0, 2000]内-1000 <= Node.val <= 1000
思路简述
BFS:用队列维护当前层结点。弹出一层时收集值,并把其子结点放入下一层队列;一层结束后把结果加入 ans,再 切换到下一层。也可用单队列 + size = queue.size() 划层,本质相同。
参考代码
JAVA
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
// ans:按层存放的遍历结果
List<List<Integer>> ans = new ArrayList<>();
if (root == null) {
return ans;
}
// curr:当前层待处理结点
Queue<TreeNode> curr = new ArrayDeque<>();
curr.offer(root);
while (!curr.isEmpty()) {
// level:本层从左到右的结点值
List<Integer> level = new ArrayList<>();
// next:下一层结点(本层所有孩子)
Queue<TreeNode> next = new ArrayDeque<>();
while (!curr.isEmpty()) {
TreeNode node = curr.poll();
level.add(node.val);
if (node.left != null) {
next.offer(node.left);
}
if (node.right != null) {
next.offer(node.right);
}
}
ans.add(level);
curr = next; // 进入下一层
}
return ans;
}
}
复杂度
时间:O(n),每个结点入队、出队各一次
空间:O(n),队列最宽一层可达 O(n)
108. 将有序数组转换为二叉搜索树 (Convert Sorted Array to Binary Search Tree)
树二叉搜索树数组分治二叉树简单
题目
给你一个整数数组 nums(元素已按升序排列),请将其转换为一棵平衡二叉搜索树。
示例 1:

输入:nums = [-10,-3,0,5,9]
输出:[0,-3,9,-10,null,5]
解释:下图也是正确答案之一:108-将有序数组转换为二叉搜索树-示例1b
示例 2:

输入:nums = [1,3]
输出:[3,1]
解释:[1,null,3]与[3,1]都是高度平衡 BST。
提示:
1 <= nums.length <= 10^4-10^4 <= nums[i] <= 10^4nums按严格递增顺序排列
思路简述
有序数组的中序遍历即自身。每次取区间中点作根,左半建左子树、右半建右子树,递归即可得到 BST;中点分割保证左右子树结点数至多差 1,故 天然高度平衡。答案不唯一(中点偏左/偏右均可)。
参考代码
JAVA
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
// ans:由整个数组区间建成的平衡 BST 根
TreeNode ans = build(nums, 0, nums.length - 1);
return ans;
}
// 用 nums[start..end] 建平衡 BST,返回子树根
private TreeNode build(int[] nums, int start, int end) {
if (start > end) {
return null;
}
// mid:取中点作根,保证左右规模尽量均衡
int mid = start + (end - start) / 2;
TreeNode root = new TreeNode(nums[mid]);
root.left = build(nums, start, mid - 1);
root.right = build(nums, mid + 1, end);
return root;
}
}
复杂度
时间:O(n),每个元素恰建一个结点
空间:O(log n),递归深度为平衡树高度(不计输出树本身)
98. 验证二叉搜索树 (Validate Binary Search Tree)
树深度优先搜索二叉搜索树二叉树中等
题目
给你一个二叉树的根节点 root,判断其是否是有效的二叉搜索树。有效 BST 定义:
左子树只含严格小于当前结点的数
右子树只含严格大于当前结点的数
左右子树自身也必须是 BST
示例 1:

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

输入:root = [5,1,4,null,null,3,6]
输出:false
解释:根为 5,但右子结点为 4。
提示:
节点数目范围在
[1, 10^4]内-2^31 <= Node.val <= 2^31 - 1
思路简述
不能只比「父结点 vs 左右孩子」:右子树里也可能出现小于祖先的值。递归时给每个结点带上开区间 (min, max),当前值须落在其中;进左子树收紧上界为 root.val,进右子树收紧下界为 root.val。边界用 long,避免 Node.val 取到 Integer 极值时溢出。
参考代码
JAVA
class Solution {
public boolean isValidBST(TreeNode root) {
// ans:整棵树是否为合法 BST;初始开区间为全体 long
boolean ans = validate(root, Long.MIN_VALUE, Long.MAX_VALUE);
return ans;
}
// 判断以 root 为根的子树是否合法,且所有结点值均满足 min < val < max
private boolean validate(TreeNode root, long min, long max) {
if (root == null) {
return true;
}
// 当前值必须严格落在 (min, max) 内
if (root.val <= min || root.val >= max) {
return false;
}
// 左子树:上界收紧为 root.val;右子树:下界收紧为 root.val
return validate(root.left, min, root.val)
&& validate(root.right, root.val, max);
}
}
复杂度
时间:O(n),每个结点检查一次
空间:O(h),递归栈深度为树高
230. 二叉搜索树中第 K 小的元素 (Kth Smallest Element in a BST)
树深度优先搜索二叉搜索树二叉树中等
题目
给定二叉搜索树根节点 root 与整数 k,找出其中第 k 小的元素(k 从 1 开始计数)。
示例 1:

输入:root = [3,1,4,null,2], k = 1
输出:1
示例 2:

输入:root = [5,3,6,2,4,null,null,1], k = 3
输出:3
提示:
节点数为
n,1 <= k <= n <= 10^40 <= Node.val <= 10^4
进阶:若 BST 经常被修改(插入/删除)且需频繁查第 k 小,如何优化?
思路简述
BST 的中序遍历是升序序列,第 k 个被访问的结点即答案。用 Morris 线索中序:有左子树时找前驱建/拆线索,访问当前结点时 k--,减到 0 即第 k 小。额外空间 O(1)。
每个结点维护子树规模 size;查第 k 小时比较左子树 size:若 leftSize+1 == k 则当前即答案,若更小则往右并令 k -= leftSize+1,否则往左。插入/删除同步更新路径上 size,单次查询 O(h)。
参考代码
JAVA
class Solution {
public int kthSmallest(TreeNode root, int k) {
// Morris 中序:按升序逐个访问,数到第 k 个
while (root != null) {
if (root.left != null) {
// prev:左子树最右结点(中序前驱)
TreeNode prev = root.left;
while (prev.right != null && prev.right != root) {
prev = prev.right;
}
if (prev.right == null) {
// 第一次:建线索后走左
prev.right = root;
root = root.left;
continue;
}
// 第二次:左子树已遍历完,拆线索
prev.right = null;
}
// 访问当前结点(中序位置)
k--;
if (k == 0) {
// ans:第 k 小的值
int ans = root.val;
return ans;
}
root = root.right;
}
return -1; // 题目保证 1 <= k <= n,正常不会走到
}
}
复杂度
时间:O(n),最坏遍历全部结点;找到第 k 个可提前结束,但建线索仍可能扫到较多结点
空间:O(1) 额外(Morris;不计修改指针的临时占用,结束前已恢复)
199. 二叉树的右视图 (Binary Tree Right Side View)
树深度优先搜索广度优先搜索二叉树中等
题目
给定二叉树根节点 root,想象站在右侧,按从上到下顺序返回从右侧所能看到的节点值。
示例 1:
输入:root = [1,2,3,null,5,null,4]
输出:[1,3,4]
解释:199-二叉树的右视图-示例1
示例 2:
输入:root = [1,2,3,4,null,null,null,5]
输出:[1,3,4,5]
解释:199-二叉树的右视图-示例2
示例 3:
输入:root = [1,null,3]
输出:[1,3]
示例 4:
输入:root = []
输出:[]
提示:
节点个数范围
[0, 100]-100 <= Node.val <= 100
思路简述
层序遍历:每一层从左到右出队,该层最后一个结点即右侧可见。用 size = queue.size() 划层,size 减到 0 时把当前结点值加入 ans。
参考代码
JAVA
class Solution {
public List<Integer> rightSideView(TreeNode root) {
// ans:自上而下每一层最右侧结点的值
List<Integer> ans = new ArrayList<>();
if (root == null) {
return ans;
}
Queue<TreeNode> queue = new ArrayDeque<>();
queue.offer(root);
while (!queue.isEmpty()) {
// size:本层结点数;用于划层
int size = queue.size();
while (size > 0) {
TreeNode node = queue.poll();
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
size--;
// 本层最后一个出队的结点 = 右视图上的点
if (size == 0) {
ans.add(node.val);
}
}
}
return ans;
}
}
复杂度
时间:O(n),每个结点入队、出队一次
空间:O(n),队列最宽一层可达 O(n)
114. 二叉树展开为链表 (Flatten Binary Tree to Linked List)
栈树深度优先搜索链表二叉树中等
题目
给你二叉树根结点 root,请将其展开为单链表:仍用 TreeNode,right 指向下一结点,left 恒为 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
进阶:能否用原地算法(O(1) 额外空间)展开?
思路简述
先序是「根 → 左 → 右」。对当前结点:若有左子树,找到左子树最右结点,把原右子树挂到其后,再把左子树挪到 right、left 置空。这样根的下一段就是原左子树,末尾自然接上原右子树。沿 right 继续处理即可。将递归改成 while 即 真正 O(1) 额外空间。
参考代码(解法一:递归)
JAVA
class Solution {
public void flatten(TreeNode root) {
if (root == null) {
return;
}
if (root.left != null) {
// prev:左子树最右结点(先序下左子树的最后一个)
TreeNode prev = root.left;
while (prev.right != null) {
prev = prev.right;
}
// 原右子树接到左子树末尾,保证先序「左完接右」
prev.right = root.right;
// 左子树整体挪到 right,left 清空
root.right = root.left;
root.left = null;
}
// 继续展开下一段(现已是原左或原右)
flatten(root.right);
}
}
复杂度(递归)
时间:O(n),每个结点被访问常数次;找最右均摊 O(n)
空间:O(h),递归栈
参考代码(解法二:迭代,O(1) 额外空间)
JAVA
class Solution {
public void flatten(TreeNode root) {
// 与解法一相同,沿 right 迭代推进,无递归栈
while (root != null) {
if (root.left != null) {
TreeNode prev = root.left;
while (prev.right != null) {
prev = prev.right;
}
prev.right = root.right;
root.right = root.left;
root.left = null;
}
root = root.right;
}
}
}
复杂度(迭代)
时间:O(n)
空间:O(1) 额外,满足进阶
105. 从前序与中序遍历序列构造二叉树 (Construct Binary Tree from Preorder and Inorder Traversal)
树数组哈希表分治二叉树中等
题目
给定两个整数数组 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 <= 3000inorder.length == preorder.length-3000 <= preorder[i], inorder[i] <= 3000preorder与inorder均无重复元素inorder均出现在preorder两数组分别保证为该树的先序、中序序列
思路简述
先序首元素即当前根。在中序中定位根下标,左边是左子树、右边是右子树;由左子树长度可切分先序区间。递归构造左右子树。用哈希表存 中序值 → 下标,定位根为 O(1)。
参考代码
JAVA
class Solution {
// indexMap:中序遍历中「值 → 下标」,避免每次线性找根
private Map<Integer, Integer> indexMap;
public TreeNode buildTree(int[] preorder, int[] inorder) {
indexMap = new HashMap<>();
for (int i = 0; i < inorder.length; i++) {
indexMap.put(inorder[i], i);
}
// ans:整棵树的根
TreeNode ans = build(preorder, 0, preorder.length - 1, 0, inorder.length - 1);
return ans;
}
// 用 preorder[preL..preR]、inorder[inL..inR] 对应区间建子树
private TreeNode build(int[] preorder, int preL, int preR, int inL, int inR) {
if (preL > preR) {
return null;
}
// 先序区间第一个即当前根
int rootVal = preorder[preL];
TreeNode root = new TreeNode(rootVal);
// rootIndex:根在中序中的位置;左侧长度为左子树结点个数
int rootIndex = indexMap.get(rootVal);
int leftLen = rootIndex - inL;
// 先序:根后紧跟 leftLen 个是左子树,其余是右子树
root.left = build(preorder, preL + 1, preL + leftLen, inL, rootIndex - 1);
root.right = build(preorder, preL + leftLen + 1, preR, rootIndex + 1, inR);
return root;
}
}
复杂度
时间:O(n),每个结点建一次;哈希预处理 O(n)
空间:O(n),哈希表 + 递归栈(最坏 O(n))
437. 路径总和 III (Path Sum III)
树深度优先搜索二叉树中等
题目
给定二叉树根节点 root 与整数 targetSum,求节点值之和等于 targetSum 的路径数目。路径不必从根开始、不必在叶子结束,但必须向下(只能从父到子)。
示例 1:

输入:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8
输出:3
解释:和为 8 的路径有 3 条,如图所示。
示例 2:
输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:3
提示:
节点个数范围
[0, 1000]-10^9 <= Node.val <= 10^9-1000 <= targetSum <= 1000
思路简述
前缀和 + DFS:维护根到当前结点的路径和 currSum,以及「此前出现过的前缀和 → 次数」。若存在前缀 currSum - targetSum,则从该前缀之后到当前结点的路径和恰为 targetSum。进入子树前把 currSum 计入 map,回溯时减回,保证只统计 当前根到叶路径上的祖先。用 long 防溢出;初始 prefixSum[0]=1 表示空前缀。
参考代码
JAVA
class Solution {
// ans:满足条件的路径条数
private int ans = 0;
// prefixSum:当前路径上「前缀和 → 出现次数」
private Map<Long, Integer> prefixSum;
public int pathSum(TreeNode root, int targetSum) {
prefixSum = new HashMap<>();
prefixSum.put(0L, 1); // 空前缀:路径从根开始时也能命中
dfs(root, 0L, targetSum);
return ans;
}
private void dfs(TreeNode root, long curr, int targetSum) {
if (root == null) {
return;
}
// currSum:根 → 当前结点的路径和
long currSum = curr + root.val;
// 若存在前缀 currSum - targetSum,则中间一段和为 targetSum
ans += prefixSum.getOrDefault(currSum - targetSum, 0);
prefixSum.put(currSum, prefixSum.getOrDefault(currSum, 0) + 1);
dfs(root.left, currSum, targetSum);
dfs(root.right, currSum, targetSum);
// 回溯:离开当前结点,恢复 map,避免影响兄弟分支
prefixSum.put(currSum, prefixSum.get(currSum) - 1);
}
}
复杂度
时间:O(n),每个结点访问一次
空间:O(n),哈希表与递归栈(最坏链状)
236. 二叉树的最近公共祖先 (Lowest Common Ancestor of a Binary Tree)
树深度优先搜索二叉树中等
题目
给定二叉树,找到两个指定节点的最近公共祖先(LCA):x 是 p、q 的祖先,且深度尽可能大(节点可以是自身的祖先)。
示例 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, 10^5]内-10^9 <= Node.val <= 10^9所有
Node.val互不相同p != q,且p、q均存在于树中
思路简述
后序递归:若当前结点是 p/q/空则返回自身。分别在左右子树找;若左右都非空,说明 p、q 分居两侧,当前结点即 LCA;否则返回非空那一侧(祖先在更深的子树,或该侧已命中其中一个目标)。
参考代码
JAVA
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// 命中目标或走到空:直接返回(题目保证 p、q 都在树中)
if (root == null || root == p || root == q) {
return root;
}
// left / right:左右子树中「找到的 p 或 q 或已求得的 LCA」
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
// 两侧都找到:p、q 分居左右,root 为最近公共祖先
if (left != null && right != null) {
return root;
}
// 只在一侧:LCA 在该侧(或该侧返回的就是 p/q 本身)
TreeNode ans = (left != null) ? left : right;
return ans;
}
}
复杂度
时间:O(n),每个结点至多访问一次
空间:O(h),递归栈深度为树高
124. 二叉树中的最大路径和 (Binary Tree Maximum Path Sum)
树深度优先搜索动态规划二叉树困难
题目
二叉树中的路径是一条节点序列,相邻节点有边相连;同一节点至多出现一次;至少含一个节点,不一定经过根。路径和为各节点值之和。给你根节点 root,返回最大路径和。
示例 1:

输入:root = [1,2,3]
输出:6
解释:最优路径2 → 1 → 3,和为 6。
示例 2:

输入:root = [-10,9,20,null,null,15,7]
输出:42
解释:最优路径15 → 20 → 7,和为 42。
提示:
节点数目范围
[1, 3 * 10^4]-1000 <= Node.val <= 1000
思路简述
后序 DFS:对每个结点计算「作为路径拐弯最高点」时的和 = val + 左贡献 + 右贡献,并更新全局最大值。返回给父结点的贡献只能选左右一侧:val + max(左, 右);子树贡献为负则取 0(不走该侧)。路径可在任意结点 「拐弯」,故不一定过根。
参考代码
JAVA
class Solution {
// ans:全局最大路径和(至少含一个结点,初值取极小)
private int ans = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
maxGain(root);
return ans;
}
// 返回:从 node 出发向下单边延伸时,能贡献给父结点的最大和(可为 0 表示不选)
private int maxGain(TreeNode node) {
if (node == null) {
return 0;
}
// leftGain / rightGain:左右子树的单边最大贡献;负贡献直接丢弃
int leftGain = Math.max(maxGain(node.left), 0);
int rightGain = Math.max(maxGain(node.right), 0);
// 以当前结点为「最高点」同时连接左右时的路径和
int pathThrough = node.val + leftGain + rightGain;
ans = Math.max(ans, pathThrough);
// 父结点只能接一侧,取较大边
return node.val + Math.max(leftGain, rightGain);
}
}
复杂度
时间:O(n),每个结点访问一次
空间:O(h),递归栈深度为树高




