嘿,朋友。我知道你现在的状态。
可能是刚打开LeetCode,看着那道”两数之和”发愣;也可能是刷了几十道题,每次面试遇到算法题还是脑子一片空白;又或者,你已经在刷题的苦海里挣扎了很久,感觉投入产出比低得吓人。
别急,我懂。我见过太多程序员——不管是大厂P7还是刚毕业的校招新人——都卡在这个瓶颈上。但我要告诉你的是:算法不是靠”刷题数量”堆出来的,而是靠”思维路径”打通的。
今天这篇指南,我不打算给你列一堆冷冰冰的题单,那是百度能搜到的。我要和你聊聊,一个真正精通算法的人,是怎么思考的,以及你如何用最聪明的方式,在有限的时间里,达到面试要求的水平。
一、先打破三个迷思
在开始之前,咱们先聊聊那些把你带偏的”常识”。
迷思一:”我只要刷够500题,算法肯定没问题”
这是最危险的幻觉。
我见过刷了800道题的人,面试官手写一个快排,他卡住;见过刷了300道的人,一道BFS(广度优先搜索)变种题直接秒杀。区别在哪?前者是在记忆题,后者是在理解模式。
LeetCode上有2000多道高质量题目,你不可能全做。面试官也不会问你”你刷了多少题”,他们会问”这道题你怎么想的”。
真相是:算法面试考察的是”问题归类能力”和”模式识别能力”,而不是记忆库的大小。
迷思二:”我必须先把《算法导论》啃完再刷题”
朋友,你是来面试的,不是来写论文的。
《算法导论》是好书,但它的厚度足以让99%的人望而却步。面试高频算法就那么几十种:双指针、滑动窗口、DFS/BFS、动态规划、贪心、回溯、树、图、堆……每种模式也就那几种典型题目。
你不需要知道”为什么快速排序的时间复杂度是O(n log n)“的严格数学证明,你需要知道什么时候用快排、怎么手写快排、快排在Java标准库里的实现细节。
真相是:面试算法是”应用导向”,不是”理论导向”。先刷题,遇到瓶颈再回溯理论,效率最高。
迷思三:”我Java熟练就行了,算法是次要的”
这句话,我在面试实习生时听过无数遍。
结果呢?简历上写着”精通Java”,面试一问”HashMap底层实现”,支支吾吾;一问”ConcurrentHashMap怎么保证线程安全”,只能背八股文。
算法和Java基础是同一个硬币的两面。 你写不出来一个高效的算法,说明你对语言特性、内存模型、时间复杂度的理解还停留在表面。反过来,算法刷得好的人,写出来的代码往往更健壮、更高效。
真相是:算法能力是程序员的”硬通货”,它直接决定了你能否进入大厂、拿到高薪。这不是鸡汤,是市场现实。
二、算法学习的”金字塔模型”
与其盲目刷题,不如先建立一个清晰的认知框架。我把算法学习分成四个层次,你可以对照一下自己现在在哪一层:
第一层:语法与基础数据结构(必须扎实)
这一层是地基。如果你连链表、树、栈、队列的基本操作都写不利索,后面所有的高级算法都是空中楼阁。
检查清单:
- 能否不查资料,手写一个单链表的反转?
- 能否清晰地解释Stack和Queue的区别,并在代码中正确实现?
- 能否在纸上画出二叉树的前序、中序、后序遍历?
代码示例:手写链表反转(Java)
public class ListNode {
int val;
ListNode next;
ListNode(int x) { val = x; }
}
public class Solution {
// 迭代法反转链表
public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode current = head;
while (current != null) {
ListNode nextTemp = current.next; // 暂存下一个节点
current.next = prev; // 反转指针
prev = current; // prev前移
current = nextTemp; // current前移
}
return prev; // prev是新的头节点
}
// 递归法反转链表(理解递归思维很有用)
public ListNode reverseListRecursive(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode newHead = reverseListRecursive(head.next);
head.next.next = head;
head.next = null;
return newHead;
}
}
很多人觉得这道题简单,但面试时手写出来、不报错、还能讲清楚两种方法的优缺点,并不简单。第一层不扎实,后面全白搭。
第二层:核心算法模式(重点突破)
这是算法学习的核心。我把高频模式分成几大类,每类你只需要掌握最典型的3-5道题目,就能触类旁通。
模式一:双指针
双指针是面试中最常见的模式之一,尤其适合数组和链表问题。
典型场景:
- 有序数组的两数之和(LeetCode 167)
- 删除有序数组中的重复项(LeetCode 26)
- 链表的中间节点(LeetCode 876)
- 接雨水(LeetCode 42)
代码示例:接雨水(经典难题)
public class Solution {
public int trap(int[] height) {
if (height == null || height.length < 3) {
return 0;
}
int left = 0;
int right = height.length - 1;
int leftMax = 0;
int rightMax = 0;
int 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)。
典型场景:
- 无重复字符的最长子串(LeetCode 3)
- 找到字符串中所有字母异位词(LeetCode 438)
- 最小覆盖子串(LeetCode 76)
代码示例:无重复字符的最长子串
import java.util.HashMap;
import java.util.Map;
public class Solution {
public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> charIndexMap = new HashMap<>();
int maxLen = 0;
int left = 0;
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
// 如果字符已经在窗口中,收缩左边界
if (charIndexMap.containsKey(c)) {
left = Math.max(left, charIndexMap.get(c) + 1);
}
// 更新字符的最新位置
charIndexMap.put(c, right);
// 更新最大长度
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}
}
关键点: 滑动窗口的核心是”左右两个指针,维护一个合法的窗口”。这道题的”合法性”是”窗口内无重复字符”。当遇到重复字符时,左指针跳到重复字符的下一个位置。
模式三:DFS/BFS(深度优先/广度优先搜索)
图论和树的问题,基本都逃不开DFS和BFS。
DFS vs BFS怎么选?
- DFS:适合找”是否存在”、”所有路径”,代码简洁,用递归或栈实现
- BFS:适合找”最短路径”,用队列实现
代码示例:二叉树的最大深度(DFS)
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
public class Solution {
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
int leftDepth = maxDepth(root.left);
int rightDepth = maxDepth(root.right);
return Math.max(leftDepth, rightDepth) + 1;
}
}
代码示例:二叉树的层次遍历(BFS)
import java.util.*;
public class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) {
return result;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int levelSize = queue.size();
List<Integer> currentLevel = new ArrayList<>();
for (int i = 0; i < levelSize; i++) {
TreeNode node = queue.poll();
currentLevel.add(node.val);
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
result.add(currentLevel);
}
return result;
}
}
模式四:动态规划(DP)
这是大多数人的噩梦,但也是面试中最能区分水平的考点。
DP的核心思想: 把大问题拆成小问题,记录小问题的解,避免重复计算。
典型场景:
- 爬楼梯(LeetCode 70)
- 背包问题(LeetCode 416)
- 最长公共子序列(LeetCode 1143)
- 股票买卖系列(LeetCode 121, 122, 123)
代码示例:0-1背包问题(简化版)
public class Solution {
public int knapsack(int[] weights, int[] values, int capacity) {
int n = weights.length;
// dp[i][w] 表示前i个物品,容量为w时的最大价值
int[][] dp = new int[n + 1][capacity + 1];
for (int i = 1; i <= n; i++) {
for (int w = 0; w <= capacity; w++) {
// 不选第i个物品
dp[i][w] = dp[i - 1][w];
// 选第i个物品(如果装得下)
if (w >= weights[i - 1]) {
dp[i][w] = Math.max(dp[i][w],
dp[i - 1][w - weights[i - 1]] + values[i - 1]);
}
}
}
return dp[n][capacity];
}
}
关键点: DP最难的不是写代码,而是”状态定义”和”状态转移方程”。这道题的状态是”前i个物品,容量为w”,转移方程是”选或不选第i个物品,取最大值”。
模式五:回溯算法
回溯是DFS的一种特殊形式,专门用于解决”组合”、”排列”、”子集”等问题。
典型场景:
- 全排列(LeetCode 46)
- 组合总和(LeetCode 39)
- N皇后(LeetCode 51)
代码示例:全排列
import java.util.*;
public class Solution {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
backtrack(nums, 0, result);
return result;
}
private void backtrack(int[] nums, int start, List<List<Integer>> result) {
if (start == nums.length) {
List<Integer> permutation = new ArrayList<>();
for (int num : nums) {
permutation.add(num);
}
result.add(permutation);
return;
}
for (int i = start; i < nums.length; i++) {
swap(nums, start, i);
backtrack(nums, start + 1, result);
swap(nums, start, i); // 回溯,恢复状态
}
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
关键点: 回溯的核心是”做选择-递归-撤销选择”。这道题每次交换一个元素,递归处理剩余元素,然后交换回来,保证下一次循环时状态正确。
第三层:题型归类与变体(举一反三)
掌握模式之后,你需要做的是”见多识广”。同一个模式,面试官可能会换着花样考你。
举个例子:二叉树的题目
- 基础:遍历(前序、中序、后序、层次)
- 进阶:验证BST、求最近公共祖先、序列化与反序列化
- 困难:二叉树展开为链表、 reconstruct a binary tree from preorder and inorder traversal
你只需要把这几类都做一遍,就能应对绝大多数二叉树面试题。
第四层:复杂度分析与优化(面试加分项)
写完代码只是第一步,面试官一定会问你:”时间复杂度和空间复杂度是多少?能优化吗?”
常见的优化方向:
- 用空间换时间(哈希表、DP数组)
- 用时间换空间(双指针代替排序)
- 剪枝(回溯、DFS中提前终止)
- 数学优化(避免模拟,直接计算)
代码示例:两数之和的优化
// 暴力解法:O(n^2)
public int[] twoSumBruteForce(int[] nums, int target) {
for (int i = 0; i < nums.length; i++) {
for (int j = i + 1; j < nums.length; j++) {
if (nums[i] + nums[j] == target) {
return new int[]{i, j};
}
}
}
throw new IllegalArgumentException("No two sum solution");
}
// 优化解法:O(n),用哈希表空间换时间
import java.util.*;
public int[] twoSumOptimized(int[] nums, int target) {
Map<Integer, Integer> numMap = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int complement = target - nums[i];
if (numMap.containsKey(complement)) {
return new int[]{numMap.get(complement), i};
}
numMap.put(nums[i], i);
}
throw new IllegalArgumentException("No two sum solution");
}
三、高效刷题的”四步法”
很多人刷题的效率低,是因为方法不对。我给你一个经过验证的”四步法”,建议你严格按照这个流程来。
第一步:审题(2-5分钟)
不要急着看代码! 先理解题目:
- 输入是什么?输出是什么?
- 有没有边界条件?(空数组、 null、负数、超大数)
- 有没有隐含的限制?(时间复杂度、空间复杂度)
技巧: 把题目用自己的话复述一遍,确保你真的理解了。
第二步:想思路(5-10分钟)
不要直接看题解! 这是最关键的一步。
- 这道题属于哪个模式?(双指针?DP?DFS?)
- 有没有类似的题目做过?
- 能不能画个图或举几个例子?
如果5分钟没想到思路,再看题解。但看完题解后,一定要自己重新写一遍。
第三步:写代码(10-15分钟)
- 先写伪代码或注释,理清逻辑
- 再写具体代码
- 注意变量命名、边界条件、异常处理
代码规范很重要! 面试时,代码写得整洁,会给面试官留下好印象。
第四步:复盘(5-10分钟)
这一步大多数人会忽略,但复盘才是提升的关键。
- 这道题考察的是什么模式?
- 我的解法和最优解法差在哪?
- 有没有更简洁的写法?
- 如果面试官追问”如果是XX情况怎么办”,我该怎么答?
建议: 准备一个”错题本”(可以用Notion、Obsidian或简单的Markdown文件),记录每道题的关键思路和易错点。
四、面试高频题单(精选100道)
我不打算给你200道题,那只会让你焦虑。以下这100道,是近3年大厂面试中出现频率最高的题目,覆盖了所有核心模式。
数组与字符串(15道)
| 题号 | 题目 | 模式 | 难度 |
|---|---|---|---|
| 1 | 两数之和 | 哈希表 | Easy |
| 3 | 无重复字符的最长子串 | 滑动窗口 | Medium |
| 11 | 盛最多水的容器 | 双指针 | Medium |
| 15 | 三数之和 | 双指针 |
