写出答案只是及格线
同样的题,两个人写出同样的代码,分数可能差一档。差别在于有没有先复述、再说复杂度、主动列边界。这份把每类题的"该说什么"和"该写什么"放在一起。
01先看数据范围n 到十万级就排除 O(n²),这是比想最优解更实用的判断。
02记动作不记代码双指针是对撞,回溯就三步,堆是替换堆顶——记住动作就能推代码。
03边界主动说空输入、单元素、重复值。自己说出来是加分,被问出来是减分。
1
面试算法考的不是最优解★
考的是你能不能在有压力时仍然有章法
同样的题,两个人写出同样的代码,分数可能差很远。区别在于:一个人上来就写,另一个人先说思路、复杂度、边界,再写。面试官打分的依据是"过程是否可控",不是"答案是否最短"。
复述 + 确认把题意用自己的话说一遍,问清输入范围
→
说思路暴力解先说,再说优化方向
→
报复杂度时间、空间,以及瓶颈在哪
→
写代码边写边说,变量名有意义
→
手动跑例拿一个例子走一遍,再补边界
- 先给暴力解不吃亏:它证明你理解了问题,而且给了兜底。直接沉默三分钟想最优解,在面试里是负分。
- 边界要主动列:空数组、单元素、全相同、负数、溢出。主动说出来是加分项,被面试官问出来是减分项。
- 写不出来时降级:从 O(n²) 写到 O(n log n) 也算进展,卡在最优解上什么都不写才是零分。
面试怎么答我一般先复述题意确认输入范围,然后给出暴力解和它的复杂度,再说优化方向。写之前会把边界列一下——空输入、单元素、重复值。写完用一个例子手动走一遍。这样即使最后没写出最优解,过程也是完整的。
2
双指针
有序数组和对撞扫描
双指针的价值是把 O(n²) 的枚举压成 O(n),前提是数组有序或者问题具有单调性。一旦发现"移动左指针会让结果单向变化",就可以砍掉一整个维度。
- 去重是三数之和的真正难点:三处都要判——外层 index、左指针、右指针。漏一处就会出重复三元组。
- 快慢指针是另一种双指针:判环、找中点、找倒数第 k 个,都在一条链表上用两个速度不同的指针。
- 不要一看到数组就上双指针:无序时对撞会漏解,必须排序,排序本身是 O(n log n)。
面试怎么答双指针把两重循环压成一重,前提是有序或者有单调性。两数之和有序版是左右对撞,和小于目标就左移、大于就右移。三数之和是排序后固定一个数,剩下的用双指针,重点在三处去重。
3
滑动窗口
连续子串问题的通用框架
滑动窗口解决的问题有同一个特征:求"连续"区间的最值。模板固定——右指针扩张、内层 while 收缩、过程中更新答案。写的时候只需要确定三件事:窗口里维护什么、什么时候收缩、答案在哪更新。
◐ 可交互:手动推一次滑动窗口
数组 [2, 3, 1, 2, 4, 3],找出「和 ≥ 7 的最短连续子数组」。点右指针开始。
- 为什么收缩要用 while 不是 if:有些题一次收缩不够(比如"最多 k 个不同字符"),要一直收到满足条件为止。用 if 会漏掉中间状态。
- 最长型 vs 最短型:最长型在收缩完之后更新答案;最短型在 while 内部、条件刚满足时就更新。这个区别写错就会差一个元素。
- 窗口内的"统计量"要能增量维护:如果每次收缩都重新算一遍窗口内容,复杂度就退化成 O(n²),窗口的意义就没了。
面试怎么答滑动窗口处理的是连续区间问题。模板是右指针扩张、内层 while 收缩、过程中更新答案。关键是窗口内的统计量要能增量更新,不能每次重算。最长型在收缩后更新答案,最短型在条件刚满足时更新。
4
链表题
指针操作,画图比想更快
链表题只有一个核心动作:改 next 之前先存住后面的节点。凡是写错的,都是因为先改了指针、把后半段弄丢了。会画三个格子加两个箭头,比在脑子里推要快得多。
- 为什么"相遇后回 head 再同速"能找到环入口:设头到环入口距离 a,入口到相遇点 b,环长 L。慢指针走了 a+b,快指针走了 a+b+kL,快是慢的两倍,解得 a = (k-1)L + (L-b),即从头走 a 步正好到入口。
- 哑节点(dummy)能省掉一堆判空:合并两个有序链表、删除倒数第 n 个,都用 dummy 指向 head,最后返回 dummy.next。
- K 个一组反转:先数够不够 k 个,不够就保持原样;够就反转这一段,然后递归或迭代处理后面。分界点是"反转前先记住下一段的头"。
面试怎么答链表题的核心是改指针前先保存后继节点。反转用 pre、cur、next 三个指针,最后返回 pre。判环用快慢指针,相遇即有环;找入口是相遇后一个指针回头部同速前进。这类题画三个节点两个箭头,比在脑子里想要可靠得多。
5
二叉树
四序遍历 + 递归返回值的设计
二叉树题分两类:遍历(按顺序访问)和递归返回值的设计(把子树的结果汇总上来)。前者背模板就行,后者才是区分度所在——要想清楚"我需要子树告诉我什么"。
- 层序的坑在"先记个数":不记 n 直接在 while 里遍历队列,会把下一层的节点混进当前层。
- 递归函数要回答一个问题:返回值代表什么?深度题返回深度,路径题返回路径,LCA 返回"找到的节点或 null"。定清楚这个,代码基本就出来了。
- 递归层数别超过 1e4:树退化成链表时递归会爆栈,面试里可以提一句"这里可以改成迭代 + 显式栈"。
面试怎么答二叉树题先判断是遍历型还是汇总型。遍历型背模板,前中后序只是 push 的位置不同,层序要用队列并先记下当前层节点数。汇总型的关键是定义递归函数的返回值含义,比如求深度就返回子树深度,求 LCA 就返回找到的节点或 null。
6
栈与单调栈
括号匹配是入门,单调栈才是高频
单调栈解决的是"找下一个更大/更小的元素",思路是维护一个单调的栈,谁破坏了单调就让谁出去。每个元素最多进出一次,所以是 O(n)。它不是靠记住,是靠想清楚"栈里存的是什么"。
- 栈里存下标而不是值:因为答案要的是"距离",需要下标相减。这是单调栈最常见的实现细节。
- 单调递增还是递减,看要找什么:找"右边第一个更大",栈从底到顶递减;找"右边第一个更小",栈递增。想不清就用具体数字试一遍。
- 循环数组(比如"下一个更大元素 II")把数组复制一遍接在后面,或者对下标取模遍历两轮。
面试怎么答括号匹配用普通栈,右括号必须和栈顶配对,最后栈要为空。单调栈用于找下一个更大或更小的元素,栈里存下标,谁破坏单调性就让谁出栈并结算答案。每个元素进出各一次,所以是线性复杂度。
7
哈希与 LRU★
LRU 是哈希加双向链表的标准组合
哈希表解决"查得快",但不解决"按访问顺序淘汰"。LRU 要同时满足 O(1) 查找和 O(1) 移动/删除,所以必须哈希存节点引用 + 双向链表维护顺序。单向链表做不到 O(1) 删除,因为没有前驱。
- 为什么必须双向:删除某个节点要知道它的前驱。单向链表只能从头找,就不是 O(1) 了。
- 为什么用哨兵:没有哨兵时,删除头节点、插入到空链表都要单独判空,代码会多出三四个 if。
- Map 的插入顺序:JS 的 Map 保持插入顺序,所以也有一种更短的写法——每次访问就 delete 再 set,淘汰时取
map.keys().next().value。面试可以先写这个,再补标准链表实现。
面试怎么答LRU 需要 O(1) 查找和 O(1) 的淘汰,所以要哈希表加双向链表:哈希存 key 到节点的引用,双向链表维护访问顺序,头部是最近使用,尾部是最久未使用。必须双向是因为删除要拿前驱。用哨兵节点能省掉大量判空。
8
二分查找
左右边界写不对,是因为边界定义没说清
二分的 bug 几乎都来自区间定义不一致:到底是
[lo, hi] 还是 [lo, hi)。先定死一种,然后 while 条件、mid 更新、返回值全部按它推,就不会乱。- 两个模板不要混用:左闭右闭配
lo <= hi和hi = mid - 1;左闭右开配lo < hi和hi = mid。混着写必错。 - "第一个满足条件的位置"是通用形式:找左边界、找插入位置、找旋转点,都能套 lowerBound 再加判断,比记三个变体省事。
- 旋转数组:先判断哪半边有序(比较 nums[mid] 和 nums[lo]),再判断 target 在不在有序那半边里,然后收缩。
面试怎么答二分的核心是先定死区间定义。我一般用左闭右开,所以 while 是 lo 小于 hi,收缩时 hi 等于 mid 而不是 mid 减一,返回 lo,它可能等于数组长度表示没找到。找边界就统一写成"第一个满足条件的位置",比记多个变体可靠。
9
排序:手写快排与归并
考的是分区和合并,不是背代码
手写排序只看两个动作:快排的分区(把小于基准的放左边)和归并的合并(两个有序数组合并)。能讲清这两个动作加复杂度分析就够了,剩下的细节(三路快排、随机基准)是加分项。
复杂度与稳定性对照快排 平均 O(n log n) 最坏 O(n^2)(已有序 + 固定基准) 空间 O(log n) 不稳定
归并 O(n log n) 稳定 空间 O(n) 稳定
堆排 O(n log n) 稳定 空间 O(1) 不稳定
冒泡 O(n^2),可提前退出成 O(n) 稳定
面试怎么答快排的核心是分区,选一个基准把小于它的换到左边,然后递归两边;平均 O(n log n),最坏是已有序且基准固定时退化成 O(n²),所以随机化基准。归并是分成两半分别排好再合并,稳定且最坏也是 O(n log n),代价是要 O(n) 额外空间。
10
动态规划★
先定状态,再想转移
DP 难在定义状态,不在写代码。判断能不能用 DP 看两点:最优子结构(大问题的最优解由小问题的最优解构成)和重叠子问题(同样的子问题会被算多次)。状态定义清楚后,转移方程基本是自然语言直译。
- 状态要带"以谁结尾":LIS 如果不定义"以 i 结尾",就无法从子问题转移。这是最容易错的地方。
- dp 数组要多开一位:涉及"前 i 个"的题,dp[0] 表示空,答案在 dp[n]。少开一位会到处越界。
- 空间能滚就滚:只依赖前一两行的题(爬楼梯、股票),用两个变量替掉整个数组,说出来是加分项。
面试怎么答DP 的第一步是定义状态,我一般先问"我要用前 i 个元素表示什么",再让它带一个"以 i 结尾"的限定,否则没法转移。然后写转移方程,再定边界。判断能不能用 DP 就看最优子结构和重叠子问题这两条。
11
回溯
排列、组合、子集,一套模板三种剪枝
回溯就是带撤销的 DFS。模板固定:选择 → 递归 → 撤销。三类题只差两个参数:什么时候收集结果、下一层从哪里开始。去重靠"排序 + 同层跳过相同元素"。
三类题的差别只有两处 下一层起点 何时收集
子集 i + 1 每个节点都收集(包括空集)
组合 i + 1 或 i(可重复) rest 减到 0 时
排列 从 0 开始 + used path 长度等于 n 时
面试怎么答回溯的模板是选择、递归、撤销三步。子集和组合的差别在下一层的起点,排列要额外用 used 数组标记已选。去重的方法是先排序,然后在同一层跳过相同的元素。剪枝就是当剩余目标已经不够时提前 break。
12
堆与 TopK
为什么 TopK 用最小堆而不是排序
求前 K 个最大,维护一个大小为 K 的最小堆:堆顶是这 K 个里最小的,新元素比堆顶大就替换它。时间 O(n log K),空间 O(K)。当 K 远小于 n 时,比排序的 O(n log n) 好,而且能处理流式数据。
- 方向别搞反:求最大的 K 个用最小堆(堆顶是门槛),求最小的 K 个用最大堆。记法:"堆顶要被淘汰的那一端"。
- JS 没有内置堆:要么手写(如上),要么用
nums.sort((a,b)=>b-a).slice(0,k)并说明这在 K 小时更实用——面试里说清取舍比硬写堆更好。 - 数据流场景才是堆的价值:内存放不下全部数据时,堆只需要 O(K) 空间,排序做不到。
面试怎么答求 TopK 用大小为 K 的最小堆,堆顶是候选里最小的,新元素比它大就替换。复杂度 O(n log K)、空间 O(K),比全排序的 O(n log n) 好,而且能处理流式数据、内存受限的场景。求最小的 K 个则用最大堆。
13
图:BFS、DFS 与拓扑排序
网格题和依赖题的两个套路
图的题先判断是遍历还是依赖顺序。遍历用 BFS(求最短步数)或 DFS(求连通块);依赖顺序用拓扑排序(BFS 版的入度法最不容易写错)。网格题本质是四邻接的图,方向数组能省掉四段重复代码。
- BFS 求最短步数,DFS 求连通性:无权图最短路径一定用 BFS,DFS 不保证最短。这一点答反了很致命。
- 拓扑排序的结果长度不等于节点数就是有环:这是检测循环依赖的标准方法,工程里做依赖解析也用它。
- 网格题的"标记"可以就地做:把访问过的 1 改成 0,省一个 visited 数组;但如果输入不能改,就得单独开数组。
面试怎么答图题先分类:求最短步数用 BFS,求连通块用 DFS,求依赖顺序用拓扑排序。网格题就是四邻接图,用方向数组统一处理。拓扑排序用 BFS 的入度法,最后出队节点数不等于总数就说明有环。
14
复杂度估算与答题节奏
面试里的"够不够快"是怎么判断的
按 1 秒约 10^7 ~ 10^8 次基本操作估算:n 到 1e5 就必须 O(n log n) 或 O(n),n 只有 1e3 时 O(n²) 也过。先看数据范围再定算法,这是比"想到最优解"更实用的能力。
数据范围 -> 可接受复杂度n <= 10 指数级都行(O(2^n)、全排列)
n <= 20 状压 DP O(2^n * n)
n <= 100 O(n^3)
n <= 1e3 O(n^2)
n <= 1e5 O(n log n)
n <= 1e7 O(n)
- 看范围反推算法是通用技巧:题面写"n 不超过 10^5"基本就排除了 O(n²),要么排序要么哈希要么双指针。
- 空间也要报:面试官经常追问"能不能 O(1) 空间",比如反转链表用递归是 O(n) 栈空间,迭代才是 O(1)。
- 写不出来时说清思路也能得分:把暴力解写完整 + 说清优化方向,比写半个最优解卡住更有价值。
面试怎么答我会先看数据范围估复杂度,n 到十万级就必须 O(n log n) 以内,只有一千左右 O(n²) 也能接受。写完会主动报时间和空间复杂度,并说明瓶颈在哪。如果想到更优解但时间不够,会先把暴力解写完整再讲优化思路。
15
速答清单
十句话收口
01面试算法到底考什么?
考过程:能否复述题意、给出思路和复杂度、主动列边界、写完自测。不是考最短代码。
02双指针什么时候能用?
数组有序,或者移动指针时结果有单调性。无序时先排序,代价 O(n log n)。
03滑动窗口的模板怎么记?
右指针扩张、内层 while 收缩、过程中更新答案。窗口内的统计量要能增量维护,否则退化成 O(n²)。
04最长型和最短型的区别?
最长型在收缩完之后更新答案,最短型在条件刚满足时(while 内部)更新。写错就差一个元素。
05链表题最容易错在哪?
改 next 之前没保存后继节点。所以反转要三个指针,先存 next 再改指向。
06LRU 为什么必须用双向链表?
删除节点需要前驱才能 O(1)。单向链表只能从头找,就退化了。哈希存节点引用解决查找。
07二分怎么写不出 bug?
先定死区间是左闭右闭还是左闭右开,然后 while 条件、收缩方式、返回值全按这个定义推,不要混用。
08快排最坏什么时候发生?
数组已有序且基准固定时,退化成 O(n²)。解法是随机化基准或用三数取中。
09DP 第一步该做什么?
定义状态,而且要带上"以 i 结尾"这类限定,否则无法从子问题转移。再写转移方程和边界。
10TopK 为什么用最小堆?
求最大的 K 个,用大小为 K 的最小堆,堆顶是门槛。时间 O(n log K),空间 O(K),能处理流式数据。
11BFS 和 DFS 怎么选?
无权图最短路径用 BFS,连通性和枚举路径用 DFS。DFS 不保证最短。