LeetCode Hot 100 精选
LeetCode Hot 100 精选题解
按题型分类,每道题含一句话思路 + 关键代码(Java)+ 时间复杂度 + 大厂考频。 重点攻克,事半功倍。
考频标注说明:🔴 超高频(近两年均出现)| 🟠 高频 | 🟡 中频
一、哈希表
哈希表是空间换时间的核心工具,将 O(n) 查找优化为 O(1)。
LC 1 两数之和 🔴
大厂考频:字节 / 阿里 / 腾讯 / 美团
一句话思路:遍历数组,用 HashMap 存已见过的数字及其下标,每次查 target - nums[i] 是否存在。
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int complement = target - nums[i];
if (map.containsKey(complement)) {
return new int[]{map.get(complement), i};
}
map.put(nums[i], i);
}
return new int[0];
}时间:O(n) | 空间:O(n)
LC 49 字母异位词分组 🟠
大厂考频:字节 / 阿里
一句话思路:将每个单词排序后的结果作为 HashMap 的 key,相同 key 的单词归为一组。
public List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> map = new HashMap<>();
for (String s : strs) {
char[] chars = s.toCharArray();
Arrays.sort(chars);
String key = new String(chars);
map.computeIfAbsent(key, k -> new ArrayList<>()).add(s);
}
return new ArrayList<>(map.values());
}时间:O(n × k log k),k 为单词最大长度 | 空间:O(nk)
LC 128 最长连续序列 🟠
大厂考频:字节 / 腾讯
一句话思路:用 HashSet 存所有数,只从连续序列的起点(num-1 不存在时)开始计数,避免重复枚举。
public int longestConsecutive(int[] nums) {
Set<Integer> set = new HashSet<>();
for (int n : nums) set.add(n);
int maxLen = 0;
for (int n : set) {
if (!set.contains(n - 1)) { // 只从序列起点开始
int cur = n, len = 1;
while (set.contains(cur + 1)) { cur++; len++; }
maxLen = Math.max(maxLen, len);
}
}
return maxLen;
}时间:O(n) | 空间:O(n)
二、双指针
双指针技巧大幅降低时间复杂度,关键是分析指针的移动策略。
LC 11 盛最多水的容器 🔴
大厂考频:字节 / 阿里 / 美团
一句话思路:左右指针从两端向中间收缩,每次移动较矮的那根柱子(移动较高的柱子不可能使面积更大)。
public int maxArea(int[] height) {
int left = 0, right = height.length - 1, max = 0;
while (left < right) {
int area = Math.min(height[left], height[right]) * (right - left);
max = Math.max(max, area);
if (height[left] < height[right]) left++;
else right--;
}
return max;
}时间:O(n) | 空间:O(1)
LC 15 三数之和 🔴
大厂考频:字节 / 阿里 / 腾讯 / 美团
一句话思路:排序后固定第一个数,用双指针找剩余两数,注意跳过重复元素避免重复三元组。
public List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
List<List<Integer>> res = new ArrayList<>();
for (int i = 0; i < nums.length - 2; i++) {
if (i > 0 && nums[i] == nums[i-1]) continue; // 去重
int left = i + 1, right = nums.length - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum == 0) {
res.add(Arrays.asList(nums[i], nums[left], nums[right]));
while (left < right && nums[left] == nums[left+1]) left++; // 去重
while (left < right && nums[right] == nums[right-1]) right--; // 去重
left++; right--;
} else if (sum < 0) left++;
else right--;
}
}
return res;
}时间:O(n²) | 空间:O(1)
LC 42 接雨水 🔴
大厂考频:字节 / 阿里 / 腾讯
一句话思路:双指针从两端向中间走,维护左右最大高度,当前位置能接的水 = min(leftMax, rightMax) - height[i]。
public int trap(int[] height) {
int left = 0, right = height.length - 1;
int leftMax = 0, rightMax = 0, water = 0;
while (left < right) {
if (height[left] <= height[right]) {
if (height[left] >= leftMax) leftMax = height[left];
else water += leftMax - height[left];
left++;
} else {
if (height[right] >= rightMax) rightMax = height[right];
else water += rightMax - height[right];
right--;
}
}
return water;
}时间:O(n) | 空间:O(1)
三、滑动窗口
滑动窗口适合求不含重复/满足某条件的子串/子数组,用左右指针维护窗口状态。
LC 3 无重复字符的最长子串 🔴
大厂考频:字节 / 阿里 / 腾讯 / 美团
一句话思路:用 HashMap 记录字符最近出现的位置,右指针扩展时若有重复则左指针跳过重复字符。
public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> map = new HashMap<>();
int max = 0, left = 0;
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
if (map.containsKey(c)) {
left = Math.max(left, map.get(c) + 1); // 左指针跳过
}
map.put(c, right);
max = Math.max(max, right - left + 1);
}
return max;
}时间:O(n) | 空间:O(min(m,n)),m 为字符集大小
LC 76 最小覆盖子串 🟠
大厂考频:字节 / 阿里
一句话思路:用两个频率表记录需要的字符和窗口中已有的字符,满足条件时收缩左指针,记录最小窗口。
public String minWindow(String s, String t) {
int[] need = new int[128], have = new int[128];
for (char c : t.toCharArray()) need[c]++;
int left = 0, count = 0, minLen = Integer.MAX_VALUE, start = 0;
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
have[c]++;
if (need[c] > 0 && have[c] <= need[c]) count++; // 满足一个字符需求
while (count == t.length()) { // 窗口已覆盖t
if (right - left + 1 < minLen) {
minLen = right - left + 1;
start = left;
}
char lc = s.charAt(left);
have[lc]--;
if (need[lc] > 0 && have[lc] < need[lc]) count--;
left++;
}
}
return minLen == Integer.MAX_VALUE ? "" : s.substring(start, start + minLen);
}时间:O(|s| + |t|) | 空间:O(1)(字符集固定大小)
四、栈
栈的核心是后进先出,适合处理括号匹配、单调栈(找左右最近更大/更小值)等问题。
LC 20 有效的括号 🟠
大厂考频:字节 / 美团
一句话思路:遇到左括号入栈,遇到右括号检查栈顶是否为对应左括号,最后栈为空则合法。
public boolean isValid(String s) {
Deque<Character> stack = new ArrayDeque<>();
for (char c : s.toCharArray()) {
if (c == '(' || c == '[' || c == '{') {
stack.push(c);
} else {
if (stack.isEmpty()) return false;
char top = stack.pop();
if (c == ')' && top != '(') return false;
if (c == ']' && top != '[') return false;
if (c == '}' && top != '{') return false;
}
}
return stack.isEmpty();
}时间:O(n) | 空间:O(n)
LC 84 柱状图中最大的矩形 🔴
大厂考频:字节 / 阿里
一句话思路:单调递增栈,遇到比栈顶矮的柱子时出栈,以出栈元素为高度、当前位置和新栈顶之间的宽度计算面积。
public int largestRectangleArea(int[] heights) {
int n = heights.length, max = 0;
int[] h = new int[n + 2]; // 两端加哨兵0
System.arraycopy(heights, 0, h, 1, n);
Deque<Integer> stack = new ArrayDeque<>();
stack.push(0);
for (int i = 1; i < h.length; i++) {
while (h[i] < h[stack.peek()]) {
int height = h[stack.pop()];
int width = i - stack.peek() - 1;
max = Math.max(max, height * width);
}
stack.push(i);
}
return max;
}时间:O(n) | 空间:O(n)
LC 394 字符串解码 🟡
大厂考频:腾讯 / 美团
一句话思路:用两个栈分别保存数字和字符串,遇到 [ 入栈,遇到 ] 出栈并重复拼接。
public String decodeString(String s) {
Deque<Integer> numStack = new ArrayDeque<>();
Deque<StringBuilder> strStack = new ArrayDeque<>();
StringBuilder curr = new StringBuilder();
int num = 0;
for (char c : s.toCharArray()) {
if (Character.isDigit(c)) {
num = num * 10 + (c - '0');
} else if (c == '[') {
numStack.push(num); num = 0;
strStack.push(curr); curr = new StringBuilder();
} else if (c == ']') {
int k = numStack.pop();
StringBuilder prev = strStack.pop();
for (int i = 0; i < k; i++) prev.append(curr);
curr = prev;
} else {
curr.append(c);
}
}
return curr.toString();
}时间:O(n × max_k),max_k 为最大重复次数
五、二叉树
二叉树题目几乎全部用递归解决,核心是想清楚"当前节点做什么,何时递归左右子树"。
LC 94 二叉树中序遍历 🟡 / LC 102 层序遍历 🟠
| 题号 | 遍历方式 | 核心思路 |
|---|---|---|
| 94 | 中序(左根右) | 递归或Morris遍历 |
| 102 | 层序 | BFS队列,每层单独收集 |
| 104 | 最大深度 | 1 + max(left, right) |
| 105 | 前+中序重建 | 前序首元素为根,在中序中分割左右子树 |
| 236 | 最近公共祖先 | 后序遍历,左右子树都找到则当前为LCA |
| 543 | 直径 | 对每个节点计算左右最大深度之和,更新全局最大 |
// LC 104 最大深度(字节高频)
public int maxDepth(TreeNode root) {
if (root == null) return 0;
return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
}
// LC 236 最近公共祖先(阿里高频)
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || root == p || root == q) return root;
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
if (left != null && right != null) return root; // 两边各找到一个
return left != null ? left : right;
}
// LC 543 二叉树的直径(美团常考)
int maxDiameter = 0;
public int diameterOfBinaryTree(TreeNode root) {
depth(root);
return maxDiameter;
}
private int depth(TreeNode node) {
if (node == null) return 0;
int left = depth(node.left), right = depth(node.right);
maxDiameter = Math.max(maxDiameter, left + right);
return 1 + Math.max(left, right);
}六、动态规划
DP 的核心:定义状态 → 找状态转移方程 → 确定初始值和边界。
高频 DP 题速览
| 题号 | 题目 | 状态定义 | 转移方程 | 考频 |
|---|---|---|---|---|
| LC 70 | 爬楼梯 | dp[i]:到第i级方法数 | dp[i] = dp[i-1] + dp[i-2] | 🟡 |
| LC 198 | 打家劫舍 | dp[i]:前i间最多抢多少 | dp[i] = max(dp[i-1], dp[i-2]+nums[i]) | 🟠 |
| LC 300 | 最长递增子序列 | dp[i]:以i结尾的LIS长度 | dp[i] = max(dp[j]+1) where j<i,nums[j]<nums[i] | 🔴 |
| LC 322 | 零钱兑换 | dp[i]:凑成i所需最少硬币 | dp[i] = min(dp[i], dp[i-coin]+1) | 🔴 |
| LC 416 | 分割等和子集 | dp[j]:能否凑成容量j | dp[j] = dp[j] or dp[j-num] | 🟠 |
| LC 1143 | 最长公共子序列 | dp[i][j]:s1前i与s2前j的LCS | dp[i][j] = dp[i-1][j-1]+1 或 max(dp[i-1][j], dp[i][j-1]) | 🔴 |
关键代码
// LC 300 最长递增子序列(字节/阿里高频)
// O(n²) DP
public int lengthOfLIS(int[] nums) {
int n = nums.length, max = 1;
int[] dp = new int[n];
Arrays.fill(dp, 1);
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
if (nums[j] < nums[i]) dp[i] = Math.max(dp[i], dp[j] + 1);
}
max = Math.max(max, dp[i]);
}
return max;
}
// LC 322 零钱兑换(字节/腾讯高频)
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
Arrays.fill(dp, amount + 1); // 初始化为不可能值
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
for (int coin : coins) {
if (coin <= i) dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
// LC 1143 最长公共子序列(阿里高频)
public int longestCommonSubsequence(String text1, String text2) {
int m = text1.length(), n = text2.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (text1.charAt(i-1) == text2.charAt(j-1)) {
dp[i][j] = dp[i-1][j-1] + 1;
} else {
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
}
}
}
return dp[m][n];
}七、图(图论专项)
| 题号 | 题目 | 方法 | 考频 |
|---|---|---|---|
| LC 200 | 岛屿数量 | DFS/BFS 染色 | 🔴 |
| LC 207 | 课程表 | 拓扑排序检测环 | 🔴 |
| LC 994 | 腐烂的橘子 | 多源BFS | 🟠 |
// LC 994 腐烂的橘子(多源BFS)
// 思路:所有腐烂橘子同时作为BFS起点,按分钟扩散,统计最终时间
public int orangesRotting(int[][] grid) {
int m = grid.length, n = grid[0].length;
Queue<int[]> queue = new LinkedList<>();
int fresh = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 2) queue.offer(new int[]{i, j});
else if (grid[i][j] == 1) fresh++;
}
}
if (fresh == 0) return 0;
int[][] dirs = {{0,1},{0,-1},{1,0},{-1,0}};
int minutes = 0;
while (!queue.isEmpty()) {
minutes++;
int size = queue.size();
for (int i = 0; i < size; i++) {
int[] curr = queue.poll();
for (int[] d : dirs) {
int ni = curr[0] + d[0], nj = curr[1] + d[1];
if (ni >= 0 && ni < m && nj >= 0 && nj < n && grid[ni][nj] == 1) {
grid[ni][nj] = 2;
fresh--;
queue.offer(new int[]{ni, nj});
}
}
}
}
return fresh == 0 ? minutes - 1 : -1;
}八、回溯
回溯 = 暴力枚举 + 剪枝。套路:选择 → 递归 → 撤销选择。
LC 46 全排列 🔴
大厂考频:字节 / 阿里 / 美团
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
boolean[] used = new boolean[nums.length];
backtrack(nums, used, new ArrayList<>(), res);
return res;
}
private void backtrack(int[] nums, boolean[] used,
List<Integer> path, List<List<Integer>> res) {
if (path.size() == nums.length) {
res.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
if (used[i]) continue;
used[i] = true;
path.add(nums[i]);
backtrack(nums, used, path, res);
path.remove(path.size() - 1); // 撤销
used[i] = false;
}
}时间:O(n × n!)
LC 78 子集 🟠 / LC 79 单词搜索 🔴
// LC 78 子集(枚举每个元素选或不选)
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
backtrack(nums, 0, new ArrayList<>(), res);
return res;
}
private void backtrack(int[] nums, int start,
List<Integer> path, List<List<Integer>> res) {
res.add(new ArrayList<>(path)); // 每个状态都是一个合法子集
for (int i = start; i < nums.length; i++) {
path.add(nums[i]);
backtrack(nums, i + 1, path, res);
path.remove(path.size() - 1);
}
}
// LC 79 单词搜索(网格DFS,字节超高频)
public boolean exist(char[][] board, String word) {
int m = board.length, n = board[0].length;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (dfs(board, word, i, j, 0)) return true;
}
}
return false;
}
private boolean dfs(char[][] board, String word, int i, int j, int k) {
if (k == word.length()) return true;
if (i < 0 || i >= board.length || j < 0 || j >= board[0].length
|| board[i][j] != word.charAt(k)) return false;
char tmp = board[i][j];
board[i][j] = '#'; // 标记已访问
boolean found = dfs(board, word, i+1, j, k+1) || dfs(board, word, i-1, j, k+1)
|| dfs(board, word, i, j+1, k+1) || dfs(board, word, i, j-1, k+1);
board[i][j] = tmp; // 撤销
return found;
}面试冲刺建议
刷题三要素
- 理解而非背诵:每道题搞懂核心思路后,合上题解自己写一遍
- 错题本:第一次做错的题,三天后重做,七天后再重做
- 限时模拟:力争在 20~25 分钟内写出中等难度题,符合真实面试节奏
知识星球引流
想要系统备战大厂算法面试?
扫码加入知识星球,获取:
- 完整 200 题题解(含 Hot 100 全部题目 + 高频附加题)
- 字节 / 阿里 / 腾讯 / 美团 近两年真实算法面试题汇总
- 每周定期打卡计划(25 题/周,共 8 周系统刷完)
- 专属 1v1 答疑 + 代码 Review
- 大厂面经分享 + 模拟面试机会
已有 2000+ 同学加入,平均拿到 2 个以上大厂 Offer。 早加入,早上岸。
