一、面试官在看的四个维度
一场典型的算法面试,评分通常落在四个维度上:
1. 问题理解与澄清。能否在动手前问出关键问题:输入规模多大?是否有序?有无重复?返回值是索引还是值?空输入怎么处理?这一步体现的是工程直觉——真实需求从来不是完整给定的。
2. 思路与权衡。能否先给出一个暴力解,再说明瓶颈在哪、如何优化。面试官往往更在意"你为什么选这个方案",而不是"你选对了方案"。
3. 代码质量。命名是否清晰、结构是否清晰、有没有冗余分支、是否一次跑通。写代码时的习惯,就是日常写代码时的习惯。
4. 复杂度与边界。能否准确说出时间与空间复杂度,能否主动覆盖空数组、单元素、全相同、极大值等边界。
值得注意的是,卡住不是致命问题,沉默才是。面试官给提示是正常流程,拒绝沟通、闷头硬写,才是最差的信号。
二、高频题型与核心范式
算法题千变万化,但底层范式有限。按"识别信号 → 套用范式"整理,比按题目分类更有效:
| 识别信号 | 核心范式 | 典型复杂度目标 |
|---|---|---|
| 有序数组、找边界 | 二分查找 | O(log n) |
| 连续子数组、最长/最短 | 滑动窗口、前缀和 | O(n) |
| 两数/多数关系、去重计数 | 哈希表、双指针 | O(n) |
| 括号匹配、单调性、下一个更大 | 栈、单调栈 | O(n) |
| 树/图的遍历、连通性 | DFS、BFS、并查集 | O(V+E) |
| 所有组合/排列/子集 | 回溯 | 指数级,需剪枝 |
| 最值、方案数、可划分 | 动态规划 | O(n²) 或 O(n) |
| 第 K 大、动态中位数 | 堆、快速选择 | O(n log k) |
准备时按范式而非按题目归类,做十道滑动窗口比做十道"随机中等题"收益高得多——因为前者让你形成"看到连续子数组就想到窗口"的条件反射。
三、解题的标准流程
一个稳定、可复用的五步流程,能显著降低现场失控的概率:
第一步:澄清(1~2 分钟) 。确认输入范围、数据特性、返回格式、异常处理。哪怕题目描述很清楚,也复述一遍你的理解,让面试官确认。
第二步:举例(1~2 分钟) 。用手写一个小例子推演,尤其是边界例子。这一步经常能提前暴露思路漏洞。
第三步:讲思路与复杂度(3~5 分钟) 。先说暴力解及其复杂度,再说优化方向。先说结论,再说推导——"我打算用滑动窗口,时间 O(n),空间 O(1),因为……"。
第四步:编码(10~15 分钟) 。边写边讲关键行在做什么。命名用业务含义而非 a、b、tmp。
第五步:验证(3~5 分钟) 。用第二步的例子手动走一遍代码,再补几个边界:空、单元素、全相同、极值。
四、几个值得背下来的模板
模板不是用来套题的,而是用来减少现场从零构造的时间。以下四段都很短,但覆盖了最高频的几类问题。
二分查找:统一用左闭右开,避免边界混乱
def lower_bound(nums, target):
lo, hi = 0, len(nums) # [lo, hi)
while lo < hi:
mid = (lo + hi) // 2
if nums[mid] < target:
lo = mid + 1
else:
hi = mid
return lo # 第一个 >= target 的位置
左闭右开的好处是:区间为空的条件始终是 lo == hi,不需要为 mid ± 1 反复纠结。求"第一个满足条件的位置"这类问题都可以套这个骨架。
滑动窗口:维护一个满足条件的窗口
def longest_unique(s):
seen, left, best = {}, 0, 0
for right, ch in enumerate(s):
if ch in seen and seen[ch] >= left:
left = seen[ch] + 1 # 收缩左边界
seen[ch] = right
best = max(best, right - left + 1)
return best
窗口题的关键是回答两个问题:右边界何时扩张、左边界何时收缩。把这两个条件写清楚,代码几乎是机械的。
回溯:做选择、递归、撤销选择
def subsets(nums):
res, path = [], []
def dfs(i):
if i == len(nums):
res.append(path[:]) # 必须拷贝
return
dfs(i + 1) # 不选
path.append(nums[i])
dfs(i + 1) # 选
path.pop() # 撤销
dfs(0)
return res
回溯的框架只有三行:加入、递归、撤销。真正需要思考的是剪枝条件和去重策略——排序后跳过相邻重复元素是最常见的去重手段。
动态规划:定义状态、写转移、定初值
def rob(nums):
prev, cur = 0, 0 # 前 i-2 项、前 i-1 项的最优解
for x in nums:
prev, cur = cur, max(cur, prev + x)
return cur
DP 的难点全在第一步:状态怎么定义。一个好用的检验方式是,状态定义写完,转移方程能否自然推出;如果推不出,多半是状态维度不够或定义错了。
五、复杂度要能"说清楚"
面试官问复杂度,不只是要一个 O 记号,而是想确认你理解它从哪来。
- 时间:数循环层数与每层的操作规模;注意
sort是 O(n log n),字符串拼接在循环里可能是 O(n²); - 空间:递归栈深度也是空间,回溯的 O(n) 栈空间常被忽略;
- 摊还分析:动态数组扩容、并查集路径压缩,要能说出"单次最坏 O(n),摊还 O(1)";
- 说清常数:哈希表平均 O(1)、最坏 O(n),面试时主动提一句会更专业。
一个常见失分点是只报结论不讲依据。更好的表述是:"外层 n 次,内层窗口左边界总共最多移动 n 次,所以是 O(n),而不是 O(n²)。"
六、白板与沟通技巧
- 先写函数签名和注释,把输入输出、约束写在最上面,既是给自己理清思路,也让面试官看到你的规划;
- 命名即文档:
left、right、window_sum比i、j、t好得多; - 边写边说:不要长时间沉默,卡住时说出你的困惑——"这里我在想如何避免重复计数";
- 主动留扩展点:写完主逻辑后说一句"如果要处理流式输入,这里需要改成维护计数而不是存全量";
- 接受提示:面试官提示通常是把你往正确方向推,顺着提示走比坚持自己的错误思路更能得分。
七、刷题策略:质量优先于数量
一个务实的准备路径:
第一阶段(2~3 周) :按范式分类刷,每个范式 5~10 题,重点是形成模板并理解为什么。这一阶段允许看题解,但看完必须合上重写一遍。
第二阶段(2~3 周) :限时训练,每题 25 分钟,模拟真实节奏。做不出来的题记入错题本,记录的是"当时卡在哪一步"而不是"正确答案"。
第三阶段(1~2 周) :模拟面试,找人互相出题、口述思路。口述和手写是两种能力,很多人能写出来但讲不清,而面试恰恰是讲给面试官听的。
关于题量,200~300 道高质量、分类清晰的题,覆盖度和熟练度通常已经足够;盲目刷到 800 道却每题只做一遍,收益远不如把 200 道吃透。同一道题隔两周重做,比做十道新题更能暴露真实掌握程度。
八、常见陷阱
- 一上来就写代码:思路没想清就动手,写到一半发现方向错,时间全浪费;
- 忽略输入约束:没看数据范围就选了 O(n²),而题目要求 n 到 10⁵;
- 边界缺失:空数组、单元素、全相同元素、负数、溢出,至少主动检查两类;
- 回溯忘记拷贝:
res.append(path)而非path[:],是最高频的低级错误; - 递归深度:n 到 10⁵ 时递归会爆栈,应改为迭代或显式栈;
- 过早优化:暴力解都没写出来就追求最优解,导致两头落空。
九、算法面试与工程能力的关系
需要承认,算法面试有其局限:它抽样的是"短时间内的抽象与实现能力",而非架构设计、协作、业务判断。这也是为什么成熟公司的面试流程通常还包含系统设计与行为面试。
但从候选人角度,算法面试仍有不可替代的价值:它逼你在约束下把一个问题想清楚、写正确、讲明白——这三种能力,在任何工程岗位上都是硬通货。准备算法面试的过程,实质是在训练一种可迁移的思维纪律:澄清需求、评估代价、先正确再优化、用边界检验自己的假设。
结语
算法面试不是记忆力测试,而是一场关于思考方式的对话。真正稳定的准备路径是:理解面试官在看什么,按范式建立模板,用标准流程控制节奏,用限时训练和模拟面试把"会"变成"稳"。
题目永远刷不完,但方法论是可以带进任何一道陌生题里的。面试时你写下的每一行代码,本质上都在回答同一个问题:面对不确定的问题,你能否有条理地把它变成确定的解。