2026算法面试高频30题精解:思路、代码与避坑指南

2018 年开始带校招、也做过不少模拟面试之后,我最大的体感是:技术面试不是在考“你会不会背题”,而是在考“你能不能把一个模糊的问题拆成清晰的子问题,并给出可运行的解决路径”。尤其到了 2026 年,面试官对 AI 生成答案已经形成免疫,照搬题解、背模板这类准备方式正在加速失效。反过来,把高频题真正吃透,做到“思路说得清、代码写得顺、边界想得全”的人,依然是所有大厂招聘表上的稀缺类型。

这篇文章不是随手整理的一百道理目清单,而是把 2026 年面试中真正值得投入时间的 30 道高频算法题,按题型、思路、代码、复杂度和避坑点做了一份可执行的精解。对象主要是有一定编程基础、但还没形成稳定刷题方法论的开发者,无论你投后端、前端还是算法岗,这份清单都适用。文中出现的代码以 Python 为主,个别排序类题目我补了 C++ 的写法,方便不同语言背景的同学参考。

1. 算法面试到底在考什么:不是让你背题

1.1 面试官真正观察的四件事

算法面试的核心从来不是“解出题”,而是“让面试官看到你解决问题的完整过程”。我参与过的多轮面试复盘里,评价表上几乎固定有四个维度:沟通、思路、代码、验证。其中沟通排在第一位,因为一个候选人在没听懂题目约束时就开始写代码,是最高频的翻车原因。

具体来说,第一件事是复述和澄清。面试官抛出题目后,你需要用自己的话确认输入输出、数据规模、是否允许额外空间、是否需要返回索引还是值。第二件事是给出暴力解再谈优化,这是大部分候选人会漏掉的一步。其实面试官真正想听的是你的思路演进轨迹:能不能先给出一个可行但不太优雅的方案,再一步步优化到符合复杂度要求。第三件事是代码落地,变量命名是否清晰、空值是否处理、边界是否覆盖。第四件事是主动测试,写完代码不要直接说“做完了”,而是挑一两个典型用例,带面试官走一遍代码逻辑。

这四个维度里,任何一个短板都会被无限放大。只刷题不练表达的人,容易卡在“思路很会,但说不清楚”;只表达不写码的人,又会在编译和边界上频繁翻车。所以 2026 年准备算法面试,最重要的一件事是放弃“我只要多刷题就行”的幻觉,改成“每道题都当成一次微型代码评审”来练。

1.2 30 道题是怎么筛出来的

“30 道高频题”不是某个平台的热门榜直接截取的前 30,而是综合了三类信息源之后,结合岗位分布和年份趋势筛出来的。

第一类是近期一线公司面经和模拟面试记录,特别是 2025 年之后的新题风向。第二类是刷题平台的高频题库,比如 LeetCode 高频题单、各大厂的 tag 列表。第三类是我自己带人时记录的“翻车频率”——有些题正确率不低,但候选人普遍在边界条件上卡很久,这类题即使出现频率不是最高,也值得单独列出来。

筛选的逻辑有两个原则:一是覆盖面优先,数组、哈希、双指针、链表、栈、队列、二叉树、回溯、动态规划、图论、排序、二分这些主流考点都要有;二是单题价值优先,一道题最好能承载多个考点切换,比如“三数之和”就同时覆盖了排序和双指针,“编辑距离”同时覆盖了二维动态规划和状态转移。按这个标准筛下来,30 道题基本能覆盖面试中 70% 以上的常见题型。

作为第一步,先把完整地图放在这里,然后我挑其中的代表题目做详细拆解。

序号经典题目核心考点推荐思路
1两数之和哈希表哈希存补数,O(n)
2三数之和双指针排序 + 固定首元素 + 双指针
3盛最多水的容器双指针移动较小边,维护最大面积
4最长无重复字符子串滑动窗口哈希记录位置,左指针收缩
5反转链表链表三指针迭代或递归
6删除链表倒数第 N 个节点链表虚拟头节点 + 快慢指针
7合并两个有序链表链表迭代或递归比较节点
8环形链表快慢指针快慢指针相遇判环
9有效括号栈左括号入栈,右括号匹配出栈
10每日温度单调栈维护递减栈,计算跨度
11接雨水双指针/单调栈前缀最大 + 后缀最大,或单调栈
12二分查找二分边界左闭右闭,注意 mid 更新
13搜索旋转排序数组二分变体判断升序区间,再缩小范围
14二叉树的层序遍历BFS队列逐层遍历
15二叉树的最大深度DFS/BFS递归或层次计数
16验证二叉搜索树中序/递归中序递增,或上下界递归
17二叉树的最近公共祖先递归左右递归结果合并
18全排列回溯选择–递归–撤销
19子集回溯/位运算枚举选或不选
20组合总和回溯+剪枝排序后剪枝,避免重复组合
21爬楼梯动态规划f(n)=f(n-1)+f(n-2)
22打家劫舍动态规划相邻不同取,滚动变量
23最长公共子序列二维 DPdp[i][j] 转移
24编辑距离二维 DP插入/删除/替换三种操作
25买卖股票的最佳时机动态规划维护最低买入价
26岛屿数量DFS/BFS/并查集遍历网格,感染标记
27课程表拓扑排序入度表 + 队列
28数组中的第 K 个最大元素堆/快选最小堆维护前 k 大
29合并区间排序+贪心按左端点排序后合并
30字符串相加模拟进位双指针按位加

1.3 复习顺序建议:先基础后抽象

有了地图之后,下一步要解决的是“我该从哪道题开始刷”。我见到太多人第一天就扑在动态规划和图论上,结果被状态转移方程劝退。合理的顺序应该是先掌握高频且不依赖复杂背景的题,再逐步进入抽象模型。

我一般会建议按这条路径推进:数组和哈希表 → 链表 → 栈和队列 → 双指针和滑动窗口 → 二分 → 二叉树 → 回溯 → 动态规划 → 图论。排序算法是穿插在数组和二分之间的,不需要单独花一整周,但快排和归并的思想要熟。这样安排的逻辑在于,很多后面的题型依赖于前面的基础能力。比如回溯本质上是对数组状态的深度遍历,如果数组索引和边界都不熟,很难写好回溯。动态规划又高度依赖遍历顺序和状态定义,前面链表、树的递归训练会帮你建立状态转移的直觉。

关于 30 道题的训练量,有一点我说句实在话:面试前只刷 30 道题确实不够,但这 30 道题作为精刷对象是足够的。我指的是“精刷”,也就是每一道题都做到独立在白纸上写出来,而不是看答案后觉得自己会了。真正应该做的是把 30 道题当成底料,每道题再额外找 2 到 3 道同类型变体作为补充练习,这样效果比盲目刷三百道要好得多。

2. 数组、哈希与双指针:最基础也最容易丢分

2.1 两数之和:从暴力到哈希表的思维升级

两数之和几乎算是算法面试的“开场白”了。题目很直白:给定一个整数数组和一个目标值,返回两个数字的索引,使它们相加等于目标值。它之所以高频,不是因为简单,而是因为它考察你能不能从暴力循环里走出来,主动用空间换时间。

最直观的解法是两层循环,遍历每一对组合,时间复杂度 O(n^2)。如果数组规模是 10^5,这个量级基本不可接受。优化的核心是:当我们遍历到某个数 num 时,真正需要判断的是“target - num”是否已经出现过,并且还要拿到它的索引。这时候哈希表就成了天然选择。

def two_sum(nums, target): seen = {} for i, num in enumerate(nums): need = target - num if need in seen: return [seen[need], i] seen[num] = i return []

这里有两个细节容易踩坑。第一是存入哈希的时机,必须是在查找补数之后,否则当 num 恰好等于 target / 2 时,会把当前元素自己当成补数。第二是题目通常要求“不能使用同一个元素”,用上面这种边查边存的方式天然规避了这个问题。还有情况是数组里存在重复值,比如 [3, 3] 目标 6,第一次遍历到第一个 3 时,哈希表为空,存入;第二次遍历到第二个 3 时,能找到补数 3,且索引不同,返回正确结果。

这类题在面试里的加分点在于主动说明:如果不要求索引、只要求判断是否存在,可以用先排序再双指针的做法,空间复杂度降到 O(1),但会丢失原索引。这种“一题多解”的意识,往往比代码本身更让面试官认可。

2.2 三数之和:排序 + 双指针怎么处理重复

三数之和是两数之和的升级版,也是双指针技巧的经典应用。题目要求找出数组中所有和为 0 的三元组,且不能包含重复三元组。如果沿用三重循环,复杂度是 O(n^3),面试官很难接受。标准解法是先排序,再固定一个数,剩下两个数用双指针在有序区间内夹逼。

去重是这道题的核心难点。固定第一个数时,如果当前数与前一个数相同,说明这一轮结果已经被上一轮覆盖过,应该直接跳过。双指针内部也要在找到一组答案后,主动跳过左右两端所有重复元素,避免产生相同的三元组。代码写出来大概是:

def three_sum(nums): nums.sort() res = [] n = len(nums) for i in range(n - 2): if nums[i] > 0: break if i > 0 and nums[i] == nums[i - 1]: continue left, right = i + 1, n - 1 while left < right: total = nums[i] + nums[left] + nums[right] if total == 0: res.append([nums[i], nums[left], nums[right]]) while left < right and nums[left] == nums[left + 1]: left += 1 while left < right and nums[right] == nums[right - 1]: right -= 1 left += 1 right -= 1 elif total < 0: left += 1 else: right -= 1 return res

这道题的高频翻车点不在整体框架,而在剪枝条件的顺序。比如“当前 nums[i] > 0 时 break”,这个优化成立的前提是数组已排序,后续所有数都大于 0,不可能再凑出和为 0 的三元组。去重时用nums[i] == nums[i-1]而不是nums[i] == nums[i+1],因为后者会把左侧指针起始位置的元素误跳过,导致漏解。

我在模拟面试里经常问一个变体:如果把“和为 0”改成“和最接近 target”,这道题要怎么改?实际上整体结构不变,只是把相等判断改成维护最小差值。这类小改动正是面试官判断你“真懂”还是“背题”的方式。

2.3 盛最多水的容器:双指针为什么要移动较小边

盛最多水的容器是一道看起来很“数学”的题,但它的双指针思路一旦理解,很难忘掉。题目给了一个整数数组,每个数代表竖线高度,选择两条线与 x 轴组成的容器,求最多能装多少水。面积公式是min(height[left], height[right]) * (right - left)。

暴力解是枚举所有左右组合,O(n^2)。优化到 O(n) 的思路是双指针从两端往中间移动。关键问题是:每次应该移动哪一边?面积由较短的那条边决定,因此如果移动较高的边,新的高度最多不会超过原来的较低边,同时距离一定减小,面积必然变小。反过来移动较矮的边,虽然距离减小,但新高度有可能变大,面积存在变大的可能。

def max_area(height): left, right = 0, len(height) - 1 ans = 0 while left < right: area = min(height[left], height[right]) * (right - left) ans = max(ans, area) if height[left] < height[right]: left += 1 else: right -= 1 return ans

这类双指针题背后其实是一种贪心思想:放弃不可能成为最优解的搜索分支。只要能向面试官解释清楚“为什么跳过这一侧不会漏掉最优解”,这道题就基本拿下了。我建议你复习时把所有双指针题放在一起对照,比如三数之和、容器接水、最长无重复子串,你会发现它们的移动条件各不相同,但都遵守“单调性”这个底层逻辑。

3. 链表、栈与滑动窗口:边界条件才是重灾区

3.1 反转链表:迭代三指针到底在反转什么

链表题是面试现场情绪崩溃率最高的题型之一,因为空指针和断链问题非常隐蔽。反转链表是其中最基础的题,也是很多后续题目的前置技能。看到“反转”两个字,第一反应不应该是背代码,而是想清楚链表反转的本质:把每个节点的 next 指针从前一个节点指到后一个节点,改成从后一个节点指向前一个节点。

迭代写法需要三个指针:prev 指向已经反转好的头部,cur 指向当前要处理的节点,nxt 暂时保存 cur.next,防止断链后丢失后续节点。每次循环做四件事:保存 nxt;把 cur.next 指向 prev;prev 挪到 cur;cur 挪到 nxt。循环结束后 prev 正好是新的头节点。

def reverse_list(head): prev = None cur = head while cur is not None: nxt = cur.next cur.next = prev prev = cur cur = nxt return prev

这里最容易写错的点是最后返回 prev 还是 cur。很多人条件反射返回 cur,但循环结束时 cur 一定是 None,prev才是原链表的最后一个节点,也就是新链表的头。调试时我建议在纸上画一个只有三个节点的链表,手动把每一步的引用关系走一遍。这个过程看起来笨,但比盯着代码找 bug 快得多。

递归写法也值得掌握,因为它能帮你巩固递归思维:先反转当前节点之后的所有节点,再把当前节点的下一个节点的 next 指向自己,最后把当前节点的 next 置空。head.next.next = head这行是核心,理解它比背住整个递归函数更有价值。

3.2 删除倒数第 N 个节点:虚拟头节点和快慢指针

删除链表的倒数第 N 个节点,常规思路是先遍历一遍拿到链表长度,再走到正数第 L-N+1 个节点删除。这个答案可以过关,但面试官通常期待你能用“快慢指针”一趟完成。思路是让快指针先走 N 步,然后快慢指针同步走,当快指针走到末尾时,慢指针正好停在待删除节点的前一个位置。

这里有个细节容易被忽略:如果删除的是头节点,慢指针需要在头节点之前有一个前驱才能完成删除操作。因此需要引入虚拟头节点 dummy,它不存真实数据,只是为了让“删除第一个节点”的代码逻辑与其他节点一致。

def remove_nth_from_end(head, n): dummy = ListNode(0, head) fast = dummy slow = dummy for _ in range(n + 1): fast = fast.next while fast is not None: fast = fast.next slow = slow.next slow.next = slow.next.next return dummy.next

注意这里fast先走 n+1 步,而不是 n 步,目的是让 slow 最终落在待删节点的前一个节点。如果走 n 步,slow 会直接指向待删节点,删除时还需要额外保存前驱。虚拟头节点的使用是链表题里极其重要的套路,合并两个链表、反转区间、按组反转这些题目都依赖它。

3.3 有效括号与每日温度:栈的两个经典用法

栈是我在面试里非常爱考的一类题,因为它很好地区分“背代码的人”和“理解数据结构的人”。有效括号就是最经典的入门题。遍历字符串,遇到左括号就压栈,遇到右括号就比较当前栈顶是否是对应的左括号。如果匹配则出栈,不匹配直接返回 False。遍历结束后栈里必须为空,否则说明存在未被匹配的左括号。

def is_valid(s): stack = [] mapping = {')': '(', ']': '[', '}': '{'} for ch in s: if ch in mapping: top = stack.pop() if stack else '#' if top != mapping[ch]: return False else: stack.append(ch) return not stack

这里的小陷阱是栈空时的 pop 操作。遇到右括号但栈已经为空,说明右括号多余,应该立刻返回 False。用top = stack.pop() if stack else '#'可以避免抛异常。

每日温度的问题是单调栈的入门题。给定一个温度数组,返回每个位置下一个更高温度出现在几天后。暴力解是两层循环,O(n^2)。单调栈解法维护一个递减的栈,栈里存的是下标。遍历到新温度时,不断与栈顶下标对应的温度比较,如果新温度更高,就说明栈顶元素的下一个更高温度是当前位置,可以出栈并记录距离,然后把当前下标压入栈。这样每个元素最多入栈一次、出栈一次,时间复杂度 O(n)。

这个套路在接雨水、柱状图最大矩形、下一个更大元素等题里是同一个核心模型。我建议一两周内集中刷完这几道题,比分散在三个月里效果明显更好。

3.4 滑动窗口:最长无重复子串的窗口收缩逻辑

滑动窗口是数组和字符串题的常客,尤其适合处理“连续子区间满足某种条件”的问题。最长无重复字符子串这道题的描述很简单:给定一个字符串 s,请找出其中不含有重复字符的最长子串的长度。暴力枚举所有子串会超时,滑动窗口能把复杂度降到 O(n)。

窗口由 left 和 right 两个指针维护,right 负责扩展,left 负责收缩。每次 right 扩展一个字符后,检查当前字符是否在窗口里出现过。如果出现过,就需要把 left 移动到上次出现位置的下一个位置。为了快速判断,用字典记录每个字符最近一次出现的下标。

def length_of_longest_substring(s): seen = {} left = 0 res = 0 for right, ch in enumerate(s): if ch in seen and seen[ch] >= left: left = seen[ch] + 1 seen[ch] = right res = max(res, right - left + 1) return res

这里有三个地方需要重点理解。第一,为什么收缩条件是seen[ch] >= left:如果字符上次出现的位置在窗口左边界之前,说明它不属于当前窗口,不需要收缩。第二,更新 seen 的时机必须是在判断之后,否则当前字符会被提前写进字典,导致判断误伤。第三,更新结果用res = max(res, right - left + 1),而不是res += 1,因为窗口可能发生收缩,长度并不总是递增。

我能想到比较好的比喻是把窗口当成一条传送带,left 是传送带的起点,right 是终点,每次发现重复零件就从上次出现位置的后一格重新开始计数。只要把“窗口内状态如何维护”想清楚,多数滑动窗口题都能套上框架,再根据题目要求调整收缩逻辑。

4. 树、图、回溯与动态规划:从递归思维到状态转移

4.1 二叉树层序遍历和最近公共祖先

树的结构天然适合递归,所以很多候选人容易陷入“会写递归但不理解递归”的状态。层序遍历是打破这种状态的好题目,因为它强迫你从递归思维切换到迭代思维。层序遍历要求按层输出节点值,核心是使用队列。每一轮循环先记录当前队列长度,表示当前层的节点数,然后只处理这么多节点,并把这层所有节点的子节点加入队列。

from collections import deque def level_order(root): if not root: return [] res = [] q = deque([root]) while q: level = [] for _ in range(len(q)): node = q.popleft() level.append(node.val) if node.left: q.append(node.left) if node.right: q.append(node.right) res.append(level) return res

我记得自己早期很容易把len(q)写在每轮循环外,导致一层节点还没处理完,队列长度变化后就乱了。这里必须注意:for _ in range(len(q))中的len(q)是在进入循环前求值的,它固定了当前层的节点数量,不会因为 q 内部变化而改变,这正是层序的关键。

最近公共祖先(LCA)则是递归思维的代表题。给定两个节点 p 和 q,找出它们在二叉树中的最近公共祖先。解法并不需要额外的父指针或路径记录,而是利用递归的返回值做信息汇总。如果当前节点是 p 或 q,直接返回当前节点;递归左右子树后,如果左子树和右子树都返回了非空结果,说明 p 和 q 分别位于当前节点的左右两侧,当前节点就是 LCA;如果只有一侧非空,说明 p 和 q 都在同一侧,直接返回那一侧的结果。

这道题我在模拟面试中至少见过五六十次,能一字不差背出解法的人不少,但能解释清“为什么返回 left or right 就足够了”的人不到一半。面试时一定要把这一层递归语义说清楚,而不是只写代码。

4.2 回溯三要素:路径、选择、撤销

回溯算法在面试里的地位从没降低过,因为它太能在短时间看出候选人的抽象能力。全排列、子集、组合总和、括号生成、N 皇后,本质上都是同一个模型。我用三句话概括回溯:做选择,进递归,撤销选择。整个过程就像走迷宫,走到死路就退回上一个岔路口,换一条路再试。

以全排列为例,需要维护两个变量:path,记录当前已经选择的结果;used 数组,记录元素是否已经被使用。递归终止条件是 path 的长度等于 nums 的长度,此时把 path 的一个拷贝加入最终结果。然后在每一层遍历所有元素,如果 used[i] 为 True 就跳过,否则标记为已使用,加入 path,进入下一层递归,返回后撤销 used[i] 和 path 的最后一个元素。

def permute(nums): res = [] n = len(nums) used = [False] * n def backtrack(path): if len(path) == n: res.append(path[:]) return for i in range(n): if used[i]: continue used[i] = True path.append(nums[i]) backtrack(path) path.pop() used[i] = False backtrack([]) return res

组合总和 II 比全排列更强调剪枝。题目要求在数组中找到所有和为 target 的组合,且每个数字在每个组合中只能使用一次,结果不能包含重复组合。解题时先排序,再在递归中传入 start 索引,保证每次只能向后选择元素,天然避免了排列顺序带来的重复。同时,如果当前值已经大于剩余 target,由于数组有序,后续元素都不会满足,可以直接 break 剪枝。

回溯题在面试中的错误绝大多数出在“忘记撤销”或“撤销位置错误”。我有个很笨但有效的自查方法:每次写回溯,都先检查递归调用下面是否紧跟对应的状态恢复语句。path.pop()之后才允许进入下一个循环分支,否则状态会被污染。

4.3 动态规划的状态定义:爬楼梯、打家劫舍、编辑距离

动态规划在 2026 年的面试中依然是压轴级别的存在,但它的高频考点其实集中在少数几个模型上:一维 DP、二维 DP、背包、股票问题、区间 DP。只要状态定义和转移方程想清楚,代码往往只有十几行。

先看爬楼梯。楼梯一共有 n 级,每次可以爬 1 级或 2 级,问有多少种不同方法爬到楼顶。这个问题的递推关系是 f(n) = f(n-1) + f(n-2),因为到达第 n 级的最后一步要么是从 n-1 级走 1 级,要么是从 n-2 级走 2 级。代码可以用滚动变量,把空间从 O(n) 降到 O(1)。

def climb_stairs(n): if n <= 2: return n a, b = 1, 2 for _ in range(3, n + 1): a, b = b, a + b return b

打家劫舍则引入了“选或不选”的状态博弈。相邻房子不能同时被偷,求能偷到的最大金额。定义 dp[i] 为“考虑前 i 个房子能得到的最大金额”,转移方程是dp[i] = max(dp[i-1], dp[i-2] + nums[i])。这个方程的意思是:第 i 个房子不偷,则结果等于前 i-1 个房子的最优解;偷,则结果等于前 i-2 个房子的最优解加上当前金额。滚动变量写法是:

def rob(nums): prev2, prev1 = 0, 0 for num in nums: cur = max(prev1, prev2 + num) prev2, prev1 = prev1, cur return prev1

编辑距离是二维 DP 的代表。题目给两个字符串 word1 和 word2,允许插入、删除、替换字符,求把 word1 转换成 word2 的最少操作数。定义 dp[i][j] 为 word1 前 i 个字符转换成 word2 前 j 个字符的最少操作数。初始化时,dp[i][0] = i,表示删除 i 个字符;dp[0][j] = j,表示插入 j 个字符。转移时,如果 word1[i-1] == word2[j-1],dp[i][j] = dp[i-1][j-1];否则取三种操作的最小值再加一。面试时这道题最容易被卡住的点,是把下标 i 和 i-1 搞混,因为二维数组的 i 对应的是字符串第 i 个字符,而代码里访问字符串时要用 i-1。建议先画一张二维表,把前两行和前两列手动填一遍,再做转移。

4.4 图的遍历:岛屿数量与课程表

图论题看起来复杂,但高频题基本集中在 DFS/BFS 模板和拓扑排序两类。岛屿数量就是 BFS/DFS 的模板题。给定一个由 '1'(陆地)和 '0'(水)组成的二维网格,计算岛屿数量。岛屿由水平和垂直方向相邻的陆地连接而成。标准解法是遍历每个格子,发现 '1' 就把岛屿数量加一,然后通过 DFS 把这个格子附近所有相邻的 '1' 都改成 '0',防止重复计数。

def num_islands(grid): if not grid: return 0 rows, cols = len(grid), len(grid[0]) count = 0 def dfs(i, j): if i < 0 or i >= rows or j < 0 or j >= cols or grid[i][j] == '0': return grid[i][j] = '0' dfs(i + 1, j) dfs(i - 1, j) dfs(i, j + 1) dfs(i, j - 1) for i in range(rows): for j in range(cols): if grid[i][j] == '1': count += 1 dfs(i, j) return count

这道题要注意网格的行列越界判断,四个方向都要检查。很多人习惯把grid[i][j] == '0'放在前面,但必须先检查下标范围,否则会抛 IndexError。此外,修改原数组grid[i][j] = '0'是一种可接受的原地标记方式,但在面试时最好主动说明“我这里直接修改了输入数组,如果不希望破坏原始数据,可以用 visited 集合来记录”。

课程表问题是拓扑排序的经典应用。给定课程数量和先修关系,判断是否可以完成所有课程。建图后统计每个节点的入度,把所有入度为 0 的课程加入队列,弹出时把依赖它的课程的入度减一,如果减到 0 再入队。最后如果课程完成数量等于总课程数,说明图中不存在环。这道题在代码里通常有两部分数据处理:用 List[List[int]] 存邻接表,用数组存入度。面试时可以先跟面试官确认课程编号范围和依赖关系的输入格式,再开始写代码,这类沟通动作本身就是加分项。

5. 刷题方法与常见问题排查

5.1 拿到一道题的正确做题姿势

我见过很多候选人拿到题之后,连数据范围都不看就开始写代码。这是算法面试中最常见的战术错误。正确姿势的第一步永远是审题:输入规模是什么级别,能不能用 O(n^2),要不要考虑数组为空、只有一个元素、元素全是重复值这些边界情况。

第二步是主动和面试官对齐“最小可行动解”。即使你一眼就看出了最优解,也建议先说一句“我先说下暴力思路,再聊怎么优化”,这是沟通策略,不是示弱。暴力解能确认你对题目的理解没错,优化思路则展示你的思维深度。面试官最怕的是候选人闷头写了一个看起来很复杂的解法,最后发现题目理解都错了。

第三步是写完代码后主动验证。不要只说“我觉得没问题”,而是挑一个典型用例,带着面试官走一遍:输入为 xxx 时,循环第一次执行,变量值是多少;走到某一步,为什么 left 要移动。这个过程既是对代码的检查,也是在告诉面试官你具备代码走读能力。

5.2 常见 Bug 与排查技巧

我在批改模拟面试代码时,发现有一些高频 bug 几乎每周都能遇到。整理成一张表,方便你自查。

常见问题典型场景排查思路
数组越界二分、双指针、网格 DFS检查所有访问数组的索引,先判断下标范围再取值
栈溢出递归过深,或漏掉终止条件打印递归入口参数,观察是否出现重复状态
死循环while 中指针没有更新检查 while 内部所有分支,确保每个分支都会移动指针或改变状态
状态未撤销回溯题输出重复或错误查看递归调用后是否恢复 used、path、visited
返回值类型错误链表题返回了中间节点而非头节点画图确认最终需要返回的引用指向哪个节点
一维/二维下标弄混编辑距离、最长公共子序列把 i、j 与字符串下标关系写在注释里,或画二维表

排查时最直接的工具就是打印。有些人觉得面试时打印不算优雅,但相比闷头盯代码,显式的打印输出能更快暴露问题。在循环开头打印指针位置、当前元素、栈内容,然后手动跑一个小用例,通常三到五分钟就能定位到问题。真正忌讳的不是调试,而是调试完后不跟面试官解释你发现了什么、为什么这么改。

5.3 复杂度分析:别只说 O(n)

很多候选人能写出正确代码,但被问复杂度时只会说“O(n)”。面试官想听到的不只是量级,还有空间复杂度、最坏情况 vs 平均情况、以及你做的优化换来了什么代价。比如两数之和,空间 O(n) 换时间 O(n),这个空间在 n=10^6 时是否可接受,都需要想清楚。

递归题的复杂度尤其容易说错。比如二叉树的最大深度,递归版本的时间复杂度是 O(n),空间复杂度是 O(h),h 是树高,最坏情况下退化成链表变成 O(n)。如果只说“O(n)”等于没回答重点。回溯题则要区分节点数和解的数量,很多回溯问题的时间复杂度用“指数级”来描述,但具体底数是什么、是否受剪枝影响,最好能说清楚。

我建议准备一个小本子,每刷完一题就记录两个数据:时间复杂度和空间复杂度,并写一句“为什么是这个复杂度”。这个习惯看起来繁琐,但坚持 30 道题之后,你会发现复杂度分析能力会明显好于只刷题不记录的人。

5.4 面试前的最后 72 小时怎么安排

最后冲刺阶段我不建议再刷新题,而是把 30 道精解题重新过一遍,重点放在三件事上:一是限时重写高频题,每道题控制在 15 分钟以内;二是把每道题的思路压缩成两到三句话,像一个 elevator pitch,保证面试官问“这题你怎么想”时你能流畅表达;三是用白板或空白纸写代码,不用 IDE 的自动补全,练出手写状态下的准确度。

我自己在面试前还会做一件事:把所有题目的复杂度表打印出来,逐行看一遍。只看了三十秒,但能把重要信息重新激活一遍。如果某道题发现自己已经想不起解法,就在旁边画个星号,当天优先重做它。

最后再分享一个很小但很有用的习惯:面试前一晚睡前列一个“要主动询问的边界条件清单”,比如“数组可能为空吗”“元素是整数还是浮点数”“能否修改原数组”,第二天看到题目先过一遍这个清单。准备算法面试,到最后拼的不是稀奇古怪的难题,而是稳定的输出能力。稳定不是天赋,是靠流程练出来的。