1. 图:最容易拉开差距的板块
1.1 图的存储结构,为什么考官总爱从这里切入
很多同学复试准备数据结构,树和排序背得滚瓜烂熟,一到图就含糊了。这其实是个很危险的信号。图这块在笔试里可能只是选择题、填空题,但面试阶段几乎必考,而且考官特别喜欢从存储结构切入,然后一路往下追问。
图的存储无非就是邻接矩阵和邻接表两种。邻接矩阵其实就是一个二维数组,graph[i][j] = 1表示顶点 i 到顶点 j 有边,如果是带权图,就存权值,没有边就用无穷大(代码里常用一个很大的数,比如INT_MAX)表示。邻接矩阵最大的优点就是判断两个顶点之间是否有边是 O(1) 的,非常快;但缺点也明显,稀疏图会浪费大量空间。一个 n 个顶点的图,邻接矩阵固定要 n×n 的空间,不管实际有多少条边。
邻接表则是为每个顶点挂一个链表,链表里存的是与该顶点相邻的所有顶点。说白了就是“我认识谁,就把谁记在我的小本本上”。邻接表对稀疏图非常友好,空间复杂度是 O(n+e),e 是边的数量。但它判断两个顶点是否相邻就没那么快了,得顺着链表一个个找。
考官问存储结构,表面是考知识点,实际上是想看你能不能根据场景选型。我这里给你一个标准的回答思路,背下来不算完,要理解着说:如果图比较稠密,或者需要频繁判断顶点之间是否连通,就选邻接矩阵;如果图很稀疏,主要做遍历操作,就选邻接表。如果是有向图,还要关注入度和出度的问题,邻接表分“入边表”和“出边表”(也叫逆邻接表),这一点也能体现出你的细致程度。
1.2 DFS 和 BFS,别只背模板
深度优先搜索和广度优先搜索,听起来简单,面试里能问出的花样可太多了。先说DFS,它的本质是“不撞南墙不回头”,沿着一条路径一直走下去,走不通了再回来换一条路。实现上,递归写法非常直观,或者用显式栈来模拟递归。面试时建议主动说递归写法,因为代码短、逻辑清晰,但也要能说出递归可能导致的栈溢出问题,如果图特别大,递归深度过深会有风险,这时候可以用非递归版本替代。
BFS 则是“层层推进”,用队列实现。它的一个非常重要的特性是:在无权图中,BFS 第一次到达某个顶点时的路径长度,一定是从起点到该顶点的最短路径长度。这个性质几乎必考,你要能现场推导一下。
面试里还有一个高频追问:DFS 和 BFS 的时间复杂度是多少?如果你回答“都是 O(n+e)”,这不够严谨。实际上无论是邻接矩阵还是邻接表,遍历所有顶点和边的时间复杂度都是 O(n+e),因为每个顶点都会被访问一次,每条边也都会被检查到。但如果用邻接矩阵存储,由于要遍历整个二维数组来找到某个顶点的所有邻接点,所以是 O(n²)。这个细节很多人会忽略,我当年就是吃了这个亏,被考官纠正后冷汗都下来了。
另外,DFS 的生成树(或森林)和 BFS 的生成树也值得复习一下。DFS 生成树里有“树边、回边、前向边、横叉边”这些概念,而判断图中是否有环,可以用 DFS:如果在 DFS 过程中访问到一个已经入栈但尚未出栈的顶点,说明存在回边,也就是有环了。同理,拓扑排序可以用来判断有向图是否有环,BFS 和 DFS 两种方式都要会。
1.3 最短路径:Dijkstra 与 Floyd 的取舍
最短路径是图论里的老大哥,考研复试面试里几乎必问。Dijkstra 算法用于单源最短路径,也就是从一个给定的源点出发,求它到其他所有顶点的最短距离。它的核心思想是贪心:每次从未确定最短距离的顶点中选一个距离最小的顶点 u,然后以 u 为中转点,更新其他顶点的距离。整个过程需要维护一个dist[]数组,以及一个标记数组(或者用优先队列优化)。
考官可能会问:Dijkstra 为什么不能处理负权边?因为它是贪心策略,一旦选定一个顶点,就认为它的最短距离已经确定了,如果后面出现负权边,绕一圈可能反而更短,这就违背了贪心的前提。这个点一定要会解释,用一个简单的例子现场演示最好,比如从 A 到 C 直接距离 10,但 A 到 B 到 C 分别是 3 和 -5,总长是 -2,那么 Dijkstra 在第一步就会选中 B,再到 C,这时候才会发现 A→B→C 比 A→C 更短,但算法已经无法回头更新了(因为 C 可能已经被标记为确定状态)。
Floyd 算法则是多源最短路径,用动态规划思想,不断尝试用每一个顶点 k 作为中转点,看能不能让 i→j 的距离更短。状态转移方程是dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])。它的时间复杂度是 O(n³),代码只有三行,非常优雅,但只能用于顶点数不多的场景。
面试中经常让对比这两个算法,我建议你用表格回答:单源 vs 多源、贪心 vs 动态规划、O(n²)(或 O((n+e)log n) 堆优化)vs O(n³)、能否处理负权边。说完对比,再补一句实际应用场景,比如地图导航用 Dijkstra 的单源最短路径其实很合适,因为起点已知,而 Floyd 更适合计算任意两点间的路况。这种回答天然就有细节,考官印象分会高不少。
1.4 最小生成树:Prim vs Kruskal
最小生成树问题在通信网络、电路布线里面很常见,也是面试的高频考点。Prim 算法的思路是:从一个顶点开始,不断把“与当前生成树集合连通的最短边”对应的顶点纳入树中。你可以把它想象成滚雪球,从一点出发,每次找到离雪球最近的顶点粘上去,直到所有顶点都被粘住。它适合稠密图,时间复杂度 O(n²),优化后可以用优先队列达到 O((n+e)log n)。
Kruskal 算法则是全局视角:把所有的边按权值从小到大排序,然后依次选边,只要这条边的两个顶点不在同一个连通分量里,就可以选它加入生成树。这本质上是一个贪心 + 并查集的过程。Kruskal 适合稀疏图,时间复杂度主要是排序的 O(e log e)。
考官常问:为什么这两个算法的结果一定是最小生成树?这个问题其实是在考“贪心正确性”的证明思路。Prim 可以用“割定理”来说:对于任意一个割,穿过割的最短边一定属于某棵最小生成树。Kruskal 也可以用类似的反证法:如果某条被选择的边不在任何最小生成树里,就矛盾了。面试里不要求你像数学证明那样严谨,但你要能把这个逻辑讲清楚,让人听明白你不是在背代码,而是真懂原理。
我自己的经验是:一定要亲手画一遍两个算法在小规模图上的执行过程,画出每一步的选择和更新,考场上才不会卡壳。很多同学背了代码,但一旦考官画出图让你现场跑一遍,就乱了。所以准备阶段,拿张纸,画个七八个顶点的图,把每一步选了什么边、为什么选它,都标出来,练到闭着眼都能说清楚为止。
2. 查找:从二分到哈希,一道题串起整条知识链
2.1 二分查找的边界条件,面试官就爱看这个
二分查找看起来简单,但面试里翻车的概率特别高。原因在于边界条件和循环不变量的选择,稍不留神就写错。复试现场考官让你手写二分查找,基本都能写出来,但写出来的代码是否经得起各种边界情况的考验,直接就是你水平的体现。
我建议你掌握两个版本:左闭右闭[left, right]和 左闭右开[left, right),二选一,但要选得坚定,且能把循环不变量的定义清楚。以左闭右闭为例,初始化left = 0, right = n - 1,循环条件while (left <= right),当mid = (left + right) / 2(注意防止溢出,写成left + (right - left) / 2)时,如果target < nums[mid],则right = mid - 1;如果target > nums[mid],则left = mid + 1;相等则返回mid。
这一段代码本身不复杂,但面试官的追问往往一环扣一环:时间复杂度为什么是 O(log n)?因为每次查找范围缩小一半,最坏情况下需要 log₂n 次比较,这个可以用“每比较一次,搜索区间减半”来解释清楚。还有,为什么left + (right - left) / 2能防止溢出?因为left + right可能超过 int 的上限,而left + (right - left) / 2不会。这种细节说出来,面试官马上就觉得你不仅有理论,还有工程敏感度。
还有一个高阶追问是:如果一个数组有重复元素,如何找到第一个等于 target 的位置,或者最后一个等于 target 的位置?这就是“二分边界问题”,在算法题里经常出现。核心技巧是找到 target 后不要立刻 return,而是继续收缩边界,例如找左边界时执行right = mid - 1,直到循环结束,left就是第一个等于 target 位置。推荐你亲手写一遍这个变体,复试时遇到它的概率非常高。
2.2 二叉排序树:构建、删除与退化问题
二叉排序树(BST)的中序遍历是递增序列,这个性质几乎人人都知道。但面试常考的不只是这个,而是删除操作。删除一个节点有三种情况:叶子节点直接删;只有一个孩子,让孩子顶上;有两个孩子,用左子树的最大节点或右子树的最小节点来替换。第三个情况最容易出错,你要讲清楚为什么选这两个节点:因为左子树的最大节点一定小于根且大于左子树所有其他节点,用它替换后 BST 性质依然成立。
但相比删除,考官其实更关心 BST 的退化问题。如果插入顺序是 1, 2, 3, 4, 5,那 BST 会退化成一个链,查找复杂度从 O(log n) 直接变成 O(n)。这时候考官通常会顺带问:怎么避免这种退化?答案就是后面的 AVL 树或红黑树。你最好能主动衔接,比如“所以我们在工程里会用平衡树来避免这个问题”,这会显得你的知识是成体系的,而不是零散的知识碎片。
面试现场还可能会让你现场构造二叉排序树,注意要先约定插入顺序。这时候你要体现的是“过程性”:逐个节点插入时,比根节点小就进左子树,比根节点大就进右子树。手写代码时不建议用递归(虽然递归代码最好懂),因为很多同学在递归返回条件上容易疏忽,这里我建议你用递归写一遍,再自己走一遍递归过程,体验一下每个节点是怎么挂上去的,这样即使考官现场改需求,你也能调整过来。
2.3 平衡二叉树 AVL 的旋转细节
AVL 是在 BST 基础上加了平衡条件:每个节点的左右子树高度差绝对值不超过 1。面试考 AVL,几乎必考四种旋转:LL、RR、LR、RL。很多同学记这四个情况记到头晕,其实你只需要抓住一个本质:平衡因子为 2 或 -2 的节点,就是“失衡点”。找到失衡点后,看新插入节点在失衡点的哪一侧、再往下一层的哪一侧,就能判断是哪种旋转。
- LL 型:在失衡点左孩子的左子树插入,做一次右旋。
- RR 型:在失衡点右孩子的右子树插入,做一次左旋。
- LR 型:在失衡点左孩子的右子树插入,先左旋左孩子,再右旋失衡点。
- RL 型:在失衡点右孩子的左子树插入,先右旋右孩子,再左旋失衡点。
面试官如果让你现场构造一棵 AVL 树并演示插入过程,关键在于插入后要从插入节点往上回溯,找到第一个失衡点,在这个点做旋转。我给你的建议是:平时练习时不要只在纸上写代码,要在纸上画图。把四种旋转各画十遍,把“旋转后谁是根、左子树是谁、右子树是谁”画得清清楚楚,面试时你甚至不需要回忆代码,直接通过图形还原过程。
有个容易忽略的点是:AVL 树删除节点后同样可能需要旋转恢复平衡,而且可能需要沿着父路径一路向上检查,直到根节点。这一点比插入复杂得多,面试中如果被追问到,你至少要能说出“删除后也可能失衡,需要沿路径回溯调整”这个结论,并用一个简单例子说明。
2.4 哈希表:冲突处理是真正的考点
哈希表这节,面试官一般不太会问“什么是哈希表”,而会直接问冲突处理。开放定址法和链地址法是两大主流。开放定址法里常见的线性探测、平方探测,链地址法就是“数组+链表”(其实在 JDK8 之后 HashMap 还会转红黑树,但数据结构面试一般不需要你答到那么深)。
线性探测的缺点是容易产生“堆积”现象:一旦发生冲突,后面的元素一个接一个地往后找空位,导致同义词和非同义词都挤在一起。平方探测可以稍微缓解这个问题,它的探测序列是 d, d², d³……所以能更分散地找位置,有效降低堆积。但用平方探测要注意,装填因子很大时可能找不到空位,甚至陷入死循环,所以在实际工程中,链地址法用得更多。
考官经常继续追问“装填因子是什么”。装填因子 α = 表中元素个数 / 哈希表长度。α 越大,冲突概率越大,查找效率越低。所以哈希表不能填得太满,通常 α 在 0.7 左右就需要扩容了。这里你可以主动提一句:Java 的 HashMap 默认负载因子是 0.75,跟这个理论吻合,说明你在学数据结构时也关注到了工程实现。
哈希函数的选取也是追问的一部分。除留余数法是最常见的,hash(key) = key % p,这个 p 最好选一个不大于表长的质数,这样可以减少冲突。为什么是质数?因为如果 p 是合数,比如 10,而 key 恰好都是偶数十进制整数,那么 hash 值的分布就会非常不均匀,只会落在偶数位置。这个解释能体现你对“为什么会冲突”的理解,比死记“选质数”强得多。
3. 排序:复杂度、稳定性、场景选择三位一体
3.1 八大排序速查表:先背熟再理解
复试面试中排序算法是重头中的重头,考官问法一般有两种:一是直接让你比较各种排序的复杂度和稳定性,二是让你手撕某个排序,然后追问优化空间。我觉得不管哪种问法,你都要先把这张表刻在脑子里。
| 排序算法 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|
| 直接插入排序 | O(n²) | O(n²) | O(1) | 稳定 |
| 希尔排序 | O(n^1.3) 左右 | O(n²) | O(1) | 不稳定 |
| 冒泡排序 | O(n²) | O(n²) | O(1) | 稳定 |
| 快速排序 | O(n log n) | O(n²) | O(log n) | 不稳定 |
| 简单选择排序 | O(n²) | O(n²) | O(1) | 不稳定 |
| 堆排序 | O(n log n) | O(n log n) | O(1) | 不稳定 |
| 归并排序 | O(n log n) | O(n log n) | O(n) | 稳定 |
| 基数排序 | O(d(n+r)) | O(d(n+r)) | O(n+r) | 稳定 |
这张表你要能当场默写出来,而且要理解每个数字背后的原因。比如快排最坏情况为什么是 O(n²)?因为每次 partition 都选到最大值或最小值作为基准,导致左右极度不均衡,退化成冒泡。堆排序为什么空间是 O(1)?因为是在原数组上建堆调整,不需要额外大空间。归并排序为什么稳定?因为合并两个有序序列时,遇到相等元素可以先取左半部分的,从而保证稳定性。
有一个高频考点是“快排为什么不稳定”。举一个例子:数组是 [5a, 3, 1, 5b, 2],以 2 为基准,partition 过程中 5a 和 5b 的相对顺序就可能被调换。这种案例能现场画出来,考官就没法难住你。
3.2 快速排序的 partition 细节
快排的 partition 实现方式有很多种,面试时最常用的是挖坑法。挖坑法的核心是:选一个基准值(通常取第一个元素),把这个位置想象成“坑”,然后从右往左找比基准小的元素填坑,再从前往后找比基准大的元素填坑,最后把基准放到最后一个坑的位置。这个过程中,基准值所在的位置一直在“漂移”,直到所有比它小的元素都在它左边、比它大的都在右边。
有一个细节经常有人出错:如果基准取的是第一个元素,那第一步一定要从右边开始扫描,因为右边是“填坑方”,一开始的坑在最左边,右边的指针找到小元素后才能填到坑里。如果先从左边找大元素,那左边的元素往哪儿放?坑不在左边(已经空了),你没法填。这个逻辑想清楚,就不会写反了。
考官还特别喜欢问快排的优化策略。我总结常见的三种:三数取中法选基准(取 left、mid、right 三个位置的中间值作为基准,降低选到最值的概率,让 partition 更均衡);当子数组长度较小时改用插入排序(插入排序在数据规模小、接近有序时性能很好);以及把相等元素聚拢到中间(三路快排,解决大量重复元素导致的退化问题)。
3.3 堆排序的建堆与调整
堆排序在考研数据结构里是重点,在复试面试里也非常容易出“手写+分析”。你首先要说清楚堆的定义:完全二叉树,且父节点大于等于(大顶堆)或小于等于(小顶堆)子节点。然后是建堆过程:从最后一个非叶子节点开始,依次向上进行“向下调整”。
为什么从最后一个非叶子节点开始?因为叶子节点自身就是堆(只有一个节点),不需要调整。最后一个非叶子节点的下标是n/2 - 1(0 基下标)。向下调整的操作是:比较当前节点和它的左右孩子,找到三者中最大(或最小)的,如果最大(最小)的是孩子,就交换,然后继续往下调整,直到满足堆的性质。
堆排序的排序过程是:建堆之后,堆顶就是最大元素,把它和最后一个元素交换,然后对前n-1个元素重新调整堆,再取堆顶……如此反复。每趟确定一个最大元素,总共 n-1 趟。这段过程你最好能手写一遍 0 基下标的数组版本,注意左右孩子下标分别是2*i+1和2*i+2,别记成2*i和2*i+1,这是新手最容易犯的错误。
考官还可能问:为什么堆排序不稳定?因为堆排序交换的过程可能把相等元素的相对顺序打乱。举个例子,数组 [5a, 5b, 3],建堆时 5a 和 5b 的位置就可能会互换。这类问题你需要能现场演示,才能让考官信服。
3.4 排序算法的实际选型思路
理论讲了一堆,考官最后往往会来一句“那实际工作中你会选哪个排序?”这个问题看起来随意,其实是考察你对排序的理解深度。我的建议是分场景回答:
- 数据量不大、基本有序:插入排序。接近有序时插入排序几乎可以达到 O(n)。
- 数据量较大、不要求稳定性:快排,工程上用三数取中的优化版本,性能最好。
- 数据量很大且要求稳定性:归并排序。
- 内存非常紧张、又要求最坏情况也有保障:堆排序,空间 O(1)。
- 数据范围很有限、比如成绩是 0~100 分:基数排序或计数排序,可以用接近线性的时间完成。
这个选型思路不仅能体现理论,还能体现工程意识,面试官听了基本不会再追问。我自己当年面的是某所 985 院校,考官就从快排聊到了实际系统中怎么处理大数据量的排序,我借着这个话题讲了一下外部排序的思路(归并排序扩展),效果不错。你可以在准备时也顺着“大数据、外部排序、多路归并”这条路稍微做点功课,复试里惊喜的概率很高。
4. 高频进阶考点:KMP、堆应用、并查集
4.1 KMP 的 next 数组怎么求,才是真正的分水岭
KMP 字符串匹配算法是数据结构面试里的硬骨头,很多人笔试会做,但面试里被问到“next 数组怎么求”时就开始含糊。KMP 的核心思想是:当匹配失败时,模式串不回溯,主串指针不动,模式串跳到某个合适的位置继续匹配。要找到这个“合适的位置”,就要用到 next 数组。
next 数组的定义有很多版本,考试和工程用的不一定相同,所以面试时建议先和考官确认:请问您说的 next 数组是 0 基还是 1 基?这样既避免误解,又展示了你对版本差异有意识。
我习惯用常见定义:next[i] 表示模式串前 i 个字符组成的子串中,最长相等前后缀的长度。比如模式串 "ABABC",next 数组(从 1 开始)是 [0, 0, 1, 2, 0],因为 "ABA" 的最长相等前后缀是 "A","ABAB" 是 "AB"。
求 next 数组的过程本质上是模式串和自己的前缀匹配,用两个指针 i 和 j。j 并不来回回溯,而是利用已经算出的 next 数组前进(有点像动态规划的思想)。面试时不要只背诵代码,要手把手演示一遍“模式串 ABABCABD”的 next 数组是怎么一点点算出来的,这个过程一演示完,考官基本就不会再往下问了。
如果考官追问 KMP 为什么比朴素匹配快,你的答案要点是:朴素匹配在匹配失败时主串指针要回溯,KMP 利用已经匹配过的信息,让主串指针不回退,模式串按 next 数组跳转,整体时间复杂度从 O(n×m) 降到 O(n+m)。这个“利用已匹配信息”的思想,在面试里比算法本身更值钱,因为它能反映出你有没有“算法思维”。
4.2 堆的应用场景,别只会排序
堆除了堆排序,最常见的应用就是优先队列和 Top K 问题。面试官常常会给你一个场景:10 亿个数中找最大的 K 个,怎么办?如果用快速排序全排,时间复杂度 O(n log n),但内存可能扛不住。最优解是用一个大小为 K 的小顶堆,遍历一遍数据,只要比堆顶大,就把堆顶替换掉,再向下调整。遍历结束后,堆里就是最大的 K 个元素,时间复杂度 O(n log K)。
这个方案的妙处在于:空间复杂度只有 O(K),而且不用把数据全部载入内存,可以流式处理。实际工程里这种海量数据处理场景很常见,你把这个思路说出来,考官基本就知道你懂了堆的本质。
还有一批真题很喜欢考“如何用两个堆维护数据流的中位数”。思路是:一个大顶堆存较小的一半,一个小顶堆存较大的一半,且保证两个堆的大小差不超过 1。插入新元素时先根据大小决定放哪个堆,再调整让两堆平衡,中位数就能 O(1) 拿到。这种题目虽然偏难,但复试面试里一旦出现,答对就是决定性拉开差距。
4.3 并查集:看起来冷门,其实很爱考
并查集在图论题里出现频率非常高,尤其是 Kruskal 算法判断“两个顶点是否在同一连通分量”时,几乎必用并查集。数据结构的复试面试,很多学校也会直接问“你知道并查集吗?实现一下”。
并查集的核心操作只有两个:Find 找根(路径压缩)和 Union 合并(按秩合并)。路径压缩的意思是:在 Find 的过程中,把沿途经过的节点的父节点直接指向根,这样下次 Find 就快了。按秩合并的意思是:Union 的时候,把深度小的树挂到深度大的树上,防止树退化成链。这两招加上之后,单次操作的均摊复杂度可以逼近常数级别,也就是反阿克曼函数级别,几乎可以认为是 O(1)。
面试时你最好把并查集的代码直接写出来,代码量非常小,十几行就能搞定:
class UnionFind: def __init__(self, n): self.parent = list(range(n)) self.rank = [0] * n def find(self, x): if self.parent[x] != x: self.parent[x] = self.find(self.parent[x]) return self.parent[x] def union(self, x, y): rx, ry = self.find(x), self.find(y) if rx == ry: return if self.rank[rx] < self.rank[ry]: rx, ry = ry, rx self.parent[ry] = rx if self.rank[rx] == self.rank[ry]: self.rank[rx] += 1写完之后,考官一般会问你“路径压缩和按秩合并分别解决了什么问题?”我的建议是分开答:路径压缩解决的是树过高导致的 Find 效率退化问题,按秩合并解决的是 Union 过程中树不平衡的问题。两者相辅相成,都能让并查集保持非常高的效率。这个题答完,面试官对你这轮的评价基本就有保证了。
4.4 动态规划与贪心:数据结构之外的思想题
虽然动态规划和贪心严格来说属于算法设计,不是数据结构本身的范畴,但复试面试里它们和数据结构结合得非常紧密。比如求最短路径的 Floyd 是动态规划,Dijkstra 是贪心,图的很多问题都会用到这两种思想。
区分两者最经典的例子是找零钱问题:如果硬币面额是 25、10、5、1,贪心算法可以找到最优解;但如果面额是 10、7、1,贪心可能就不是最优了。比如要找 14 块,贪心会拿一个 10 和 4 个 1,一共 5 枚硬币;但最优解是两个 7,只有 2 枚。这就是贪心的局限性:局部最优不等于全局最优。
动态规划和贪心都是在做“决策”,区别在于贪心只关注当前这一步的最优,不回头;动态规划会考虑上一步的状态,把所有可能都算一遍。面试时你要能用自己的话把这个区别讲明白,最好附带上这个找零钱的例子,考官一听就懂。
还有一类是“递归、分治、动态规划”三者对比的题目。递归是思路,分治是递归的一种特殊应用(把大问题拆成互不相关的子问题),动态规划则是子问题有重叠时的优化(用表存储中间结果避免重复计算)。这三个概念一字排开,思路清楚,你怎么答都不会乱。
5. 面试现场的表现技巧与问题速查表
5.1 回答算法题的标准框架,很多人不知道
复试面试时,考官抛出算法题后,建议不要立刻提笔就写。我见过很多考生一听到“快排”就开始背代码,结果被考官打断问“为什么要这么写”,整个人就愣住了。正确的节奏应该是:先复述题目、确认输入输出与边界;再讲整体思路,复杂度是多少;最后才开始写代码,写完后主动用一个小测试样例验证。
三步走看起来很简单,但凡是照做的人,在面试官那里的评价都会明显不一样。因为考官考察的不只是你会不会这道题,而是你拿到一个陌生问题时,有没有一套科学的思考流程。未来读研做研究,遇到没见过的算法问题,靠的正是这种“拆解-设计-验证”的能力,而不是背题能力。
举个例子,如果考官问“如何判断一棵二叉树是否是二叉搜索树”,你可以先说:BST 的中序遍历是有序序列,所以我可以用中序遍历,看序列是否递增。然后再说:递归过程中可以维护一个区间 [min, max],每个节点值必须落在区间内,这样一次遍历就能验证。最后再补充:如果树很大,递归可能有栈溢出风险,可以用迭代中序遍历。这种回答层次清晰、由浅入深,面试官想扣分都很难。
5.2 常见追问与应答策略
面试官的追问往往比一开始的问题更凶险,但也不是没有规律。我做了一次系统梳理,把高频追问归纳成三类,并附上应对策略。
第一类,原理型追问:“为什么时间复杂度是这个?”这类追问考查你是否有真正理解算法的本质,而不是背结论。应对策略:时间复杂度的推导要从“基本操作执行次数”出发。比如插入排序为什么最坏是 O(n²),因为最坏情况下每个元素都要往前移动 i 次,总共约 n²/2 次比较与移动。
第二类,场景型追问:“如果数据量很大怎么处理?”这类追问考查你的工程直觉。应对策略:先考虑内存约束,再考虑时间约束,最后考虑是否能做分布式。比如海量数据排序,可以先内存部分用快排,再走外部多路归并,这就把理论和工程串起来了。
第三类,逻辑型追问:“这个算法有没有更好的方案?”这类追问考查你能不能跳出固定思维。应对策略:从复杂度上找突破口。比如一个朴素算法是 O(n²) 的,你可以思考能不能用哈希表或排序降到 O(n log n);如果能用堆或双指针降到 O(n),就更好了。表达时要说清楚每个优化换来了什么、代价是什么,不要上来就说“可以优化”,要把为什么能优化、用什么结构优化都讲透。
5.3 高频问题速查表,考前突击必备
我把复试里数据结构面试中出现频率最高的问题整理成一张速查表,每个问题必须先自己口述一遍,再对照参考答案,不要只看不练。
| 高频问题 | 核心答案要点 |
|---|---|
| 数组和链表的区别 | 随机访问 vs 顺序访问;连续空间 vs 分散空间;修改大小成本差异 |
| 栈和队列的区别 | LIFO vs FIFO;应用场景:递归调用栈、迷宫BFS队列 |
| DFS 和 BFS 的区别 | 栈/递归 vs 队列;适合连通块、拓扑 vs 适合最短路径、层次遍历 |
| 如何判断有向图是否有环 | DFS 检测回边;拓扑排序检测剩余入度非零节点 |
| 快排为什么快 | 平均 O(n log n),局部性好,减少数据搬运;最坏 O(n²) |
| 哈希冲突怎么办 | 链地址法、开放定址法;装填因子控制;再哈希法 |
| 中序遍历 BST 得到什么 | 递增序列;验证 BST 的常用方法 |
| 堆和优先队列关系 | 堆是实现优先队列的常见底层结构;插入与删除 O(log n) |
| KMP 核心思想 | 利用已匹配信息,主串不回溯;next 数组记录最长相等前后缀 |
| 如何求第 K 大元素 | 快排 partition 思路;堆 Top K;时间复杂度对比 |
这张表只是引子,考场上考官可能从任意一个点继续深挖。准备阶段的正确姿态是:每个问题都能顺着往下讲五到十分钟,而不是只能答出一句话。比如“栈和队列的区别”看似基础,考官完全可以追问“用两个栈模拟队列怎么做”“用两个队列模拟栈怎么做”——每一条都能延伸出一片知识区。
我个人在实际准备复试时有一个习惯,就是把每一道面试题都当成一次“讲课”来练习,想象自己面前坐着一位完全没有背景的听众,需要用最通俗的语言把原理讲清楚。这样练过几轮之后,面试现场即使遇到追问,你也已经习惯了“展开讲”的节奏,不会慌。数据结构的知识点虽然多,但真正高频的就那么几块,你只要把图和树吃透,再把排序和查找的相关算法都能现推现写,复试这块基本就能稳稳拿下。