想象你来到一座陌生城市,手里只有一张地铁线路图。站点是圆的,轨道是线,换乘通道把不同线连在一起。你要判断从站 A 到站 B 怎么走最快,要设计一套“最少换乘”方案,要找出如果某条线路断了会不会有站点失联——这些问题背后都是一门叫“图论”的学科。对 AI 求职者来说,图论不是数学游戏,而是面试中最高频的算法题母题之一:推荐系统里的好友关系、自动驾驶里的路径规划、大模型训练任务的有向依赖,全都能抽象成一张图。这篇文章会用“城市地铁图”这个比喻贯穿全程,带你从零看懂图论算法面试的核心套路:怎么存图、怎么遍历、怎么求最短路径、怎么保证工程最省,以及面试时怎么做题不踩坑。
图论是算法面试的“地基性考点”,掌握它不仅能应对 hard 题,更能让你在面对社交网络、知识图谱、任务调度等 AI 场景时,快速把业务问题翻译成可计算的模型。
可视化演示
动态演示图论算法面试精讲的核心机制,通过输入/处理/存储/输出四种模式展示关键概念与交互流程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
图论算法面试精讲:图的表示 图的表示与遍历基础 最短路径算法精讲 核心机制可视化 图论算法面试精讲整体架构与关键组件
基础入门
如果你今天拿到一张陌生城市的地铁图,你会先看什么?多数人会先看两个东西:站点数量和轨道数量。站点太少的地图很空,轨道太多的地方容易绕晕。图论的第一课就是这个感觉:图由顶点(站点)和边(轨道)组成,我们关心顶点个数 \(V\) 和边数 \(E\),因为后续所有算法的时间复杂度都围绕它们展开。
从地铁图过渡到数学表达:顶点记作 \(V\),边记作 \(E\)。边有方向的是有向图,没有方向的是无向图。地铁线路通常是双向的,对应无向图;而“公众号关注关系”“网页链接”是单向的,对应有向图。面试题里还会出现“带权图”,可以理解成每段轨道有不同行驶时长;权值相同或未指定的图就是无权图。
图论里有一条最著名的“握手引理”:所有站点度数之和等于边数的两倍。度就是这个站点连接的轨道数。为什么?因为每一条边都连接两个端点,在计算总度数时恰好被数了两次。
回到地铁比喻:你不需要背公式,只要记住“每段轨道有两个头”。面试时如果题目给了站点和轨道数,优先检查数据规模,再决定用哪种算法。读图如读地图,先看规模,再做选择。
- 累加度数把每一个站点的连接轨道数量加起来。
- 每条边贡献两次一条轨道有两个端点,分别给两个站点各增加 1 度。
- 奇偶推论左边是偶数,说明奇数度站点的总数也必须为偶数。
存储选型
一座城市的地铁图有很多种保存方式。最笨的方法是做一块巨大的“全城换乘查询屏”,把每个站点到每个站点是否有轨道都标出来,这叫邻接矩阵。另一种做法是每个站点门口挂一块“本站可直达”的小牌子,这叫邻接表。还有一种是把轨道工程档案一条条列成数组,这叫边集数组。
三种方式各有适用场景。邻接矩阵用二维数组 \(graph[u][v]\) 表示站点 \(u\) 到 \(v\) 是否有边,判断两点是否相邻只需 \(O(1)\),但空间固定是 \(V^2\)。邻接表为每个顶点保存一个邻居列表,只记录真实存在的边,空间约等于 \(V + 2E\)。边集数组则把所有边按 (起点, 终点, 权值) 存放,特别适合需要按权值排序的算法,比如 Kruskal 最小生成树。
用比喻收尾:查询屏适合站点少、轨道密的“中心城区”;邻接表像每个站台挂一张小卡片,适合铺得很开的地铁网络;边集数组则是施工队的图纸清单。面试时先问自己:“这个图边多不多?” 一句话就能锁定存储方案。
- 矩阵空间邻接矩阵必须开一个 \(V \times V\) 的表格。
- 表空间邻接表只保存每个顶点和它真实的邻居。
- 对比结果当 \(E \ll V^2\) 时,邻接表能省下大量空间。
DFS 遍历
想象一位地铁检修员从 1 号线起点出发,沿着一条支线一直往前走,走到尽头后原路退回最近的岔路口,再换另一条支线继续走。这种“一条路走到黑,再回溯”的巡查方式,就是深度优先搜索(DFS)。
DFS 的关键是递归加上回溯。递归函数进入一个顶点后,标记它“已访问”,然后依次访问每一个未访问的邻居。等到所有邻居都处理完,函数自然返回上一层。这个过程可以抽象成下面这个模板:先做决定,再进邻居,最后撤销现场。
回到地铁检修员:他不需要地图,只需要记住“刚才从哪条路来的”,走完所有岔路后整张网的连通情况就清楚了。DFS 在面试中常用于判断连通分量、检测环、求路径存在性,但注意它不擅长找“最短路径”,因为深挖的路线通常不是最短的。
- 访问顶点每进入一个站点,就把它标成已访问。
- 递归邻居对每个未访问邻居 \(u\) 继续调用 DFS。
- 累计代价每个顶点进入一次,每条边被查看一次。
BFS 遍历
如果地铁失火,广播系统会怎么做?它不会沿着一条线路追到底,而是以事发站为圆心,一圈一圈向外通知:先通知直达站,再通知换乘站,再通知更远的站。这种“逐层扩散”的搜索方式,就是广度优先搜索(BFS)。
BFS 的实现核心是队列,先进先出。起点入队;每次从队首弹出一个站点,把它的所有未访问邻居入队;因为队列按进入顺序处理,所以先被发现的站点一定比后发现的站点离起点更近。因此在无权图中,BFS 天然能求出起点到每个点的最短路径长度。
用广播的比喻来理解:BFS 永远先通知“近的人”,再通知“远的人”,所以首次收到通知的路径一定是最少经过站点的路径。面试中,“最短换乘次数”“社交网络几度人脉”“迷宫最少步数”这类题目,都是在考 BFS。
- 起点距离起点到自己的距离是 0。
- 邻居发现从队列取出 \(u\),检查它的邻居 \(v\)。
- 距离累加如果 \(v\) 第一次被访问,则更新距离。
最短路径
坐地铁导航时,你希望 App 告诉你“从当前站到目的地,哪条路线总耗时最少”。如果每条轨道的行驶时间是已知的,这就是带权最短路径问题。最常见的算法有三种:Dijkstra、Bellman-Ford 和 Floyd。
Dijkstra 像一个精明的导航员:它维护一个“当前已知最短用时表”,每次都从尚未确定的站点里挑一个当前距离最小的站点出来,并用它去更新邻居。这个贪心策略要求所有边权非负,否则先选出来的“最小”可能是错的。核心更新操作叫“松弛”:
回到地铁场景:Dijkstra 适合“所有轨道都不会倒贴时间”的正常城市。如果存在负权边——相当于某条线路不仅不耗时还能“倒赚时间”,Dijkstra 就会被绕晕,这时要改用 Bellman-Ford,它会把所有边反复松弛 \(V-1\) 轮。如果面试要求所有站点两两之间的最短路径,则用 Floyd 动态规划,一次算出整个二维距离表。
- 当前距离\(d[v]\) 是目前已知从起点到 \(v\) 的最短用时。
- 候选路线经过 \(u\) 再到 \(v\) 的总用时。
- 取最小值如果候选更短,就更新当前纪录。
最小生成树
假设地铁公司要在多个新区之间铺设轨道,要求所有站点连通,且轨道总造价最低。注意,我们不是求“从 A 到 B 怎么走最快”,而是求“把所有站点连成一个整体最少花多少钱”——这就是最小生成树(MST)问题。
Prim 算法像是从一个已连通的“城市组团”出发,每次都从组团外挑一条通向组团的最短边扩进来。Kruskal 算法则把所有边按造价从小到大排序,从便宜到贵一条条挑选,只要不形成环就加入。两者背后共用同一条“切分性质”:把站点分成两组,横跨两组的边里造价最小的那条,一定属于某棵最小生成树。
用修地铁比喻:Prim 是从已开发的城区向外“长”出去,Kruskal 是先把所有候选轨道标好价格,从便宜到贵慢慢合并。面试里只要听到“连通所有点且总代价最小”,就立刻把题目翻译成 MST。
- 切分集合把顶点分成两个集合 \(S\) 和 \(V \setminus S\)。
- 最小跨边在所有跨两个集合的边里找到权值最小的那条。
- 必然出现这条最小的跨边一定存在于某个最小生成树中。
拓扑排序
如果你负责规划一整条地铁线路的施工:必须先挖隧道,再铺轨道,再装信号系统,最后试运行。这些任务之间有明显的前后依赖,不能乱序。把每个任务看作顶点,任务依赖看作有向边,就得到一个有向无环图(DAG)。
拓扑排序就是给 DAG 里的所有任务排出一个合法执行顺序:每条有向边都必须从“先做的事”指向“后做的事”。经典算法是 Kahn 算法:统计每个顶点的入度,把入度为 0 的顶点先输出,并删除它的所有出边,重复这个过程直到所有顶点输出完毕。如果最后还有顶点未被输出,说明图中存在环,任务计划不可执行。
回到施工比喻:拓扑序不是唯一的。比如铺轨道和装信号系统之间如果没有直接依赖,谁先谁后都可以。面试中如果题目提到“依赖关系”“编译顺序”“课程先修”,第一反应就应该是拓扑排序。
- 前驱约束如果边 \(u \rightarrow v\) 存在,则 \(u\) 必须先执行。
- 顺序要求在最终序列里 \(u\) 的下标必须小于 \(v\)。
- 入度消减Kahn 算法不断删掉入度为 0 的节点,直到删完。
面试实战
面试做题就像拿到一张城市地图但不告诉你该坐几号线。你需要快速完成三件事:读题建模、复杂度估算、代码规范。
读题建模是把业务语言翻译成图论语言。“好友推荐”是找邻居;“最少换乘”是 BFS;“物流运费最低”是带权最短路;“所有仓库联通成本最小”是 MST;“课程依赖”是拓扑排序。然后根据数据规模选择算法:如果 \(V\) 只有几十,Floyd 也可以;如果 \(V\) 是十万,必须用堆优化的 Dijkstra 或邻接表 BFS。
回到地铁比喻:优秀的面试者不是记路名,而是拿到地图先确认“有多少站、多少条轨道”,再决定用哪套出行工具。最后还要写出变量清晰的代码:graph、dist、visited 不要随手写成 g、d、vis。算法正确,再加上可读性,才是真正的面试高分答案。
- 朴素方案在稠密图上用邻接矩阵跑最短路。
- 优化方案用优先队列加邻接表后大幅下降。
- 规模判断稀疏图下两种方案差距巨大。
AI 应用场景
社交网络“几度人脉”推荐
阶段:特征工程与图挖掘。
解决的问题:传统好友推荐只看共同好友数量,无法表达“关系远近”。用 BFS 可以计算两个用户之间的最短熟人链长度,从而把“二度人脉”作为强推荐信号。
具体案例:招聘平台根据候选人与其潜在人脉的 BFS 距离,优先推荐距离为 2 的行业前辈;配合边权刻画亲密程度后,再用 Dijkstra 求“综合亲密度最高”的推荐路径。
AI训练任务依赖调度
阶段:数据流水线与训练任务编排。
解决的问题:一个模型训练要经过数据清洗、特征提取、模型训练、评估上线等步骤,存在严格依赖。用拓扑排序可以自动生成一条合法执行顺序,并检测依赖环。
具体案例:某 MLOps 平台把每个数据任务建模为 DAG,用 Kahn 算法排布每日训练流程;发现依赖环时自动告警,避免两个任务互相等待形成死锁。
物流路径规划
阶段:决策优化。
解决的问题:配送网络中存在大量站点和路线,不同路线成本不同,需要实时求最低成本路线。Dijkstra 的“松弛”思想可直接用于地图导航和路径规划。
具体案例:无人车配送系统把路口建模为顶点、道路建模为带权边,用优先队列优化 Dijkstra 计算每单的最优路线;若道路出现负向“奖励”,则切换 Bellman-Ford 保证正确性。
Python 演示
import heapq def dijkstra(n, edges, start): graph = [[] for _ in range(n)] for u, v, w in edges: graph[u].append((v, w)) graph[v].append((u, w)) dist = [float('inf')] * n dist[start] = 0 pq = [(0, start)] while pq: d, u = heapq.heappop(pq) if d > dist[u]: continue for v, w in graph[u]: nd = d + w if nd < dist[v]: dist[v] = nd heapq.heappush(pq, (nd, v)) return dist if __name__ == '__main__': edges = [ (0, 1, 4), (0, 2, 1), (2, 1, 2), (1, 3, 1), (2, 3, 5), ] print(dijkstra(4, edges, 0))
graph = [[] for _ in range(n)]建邻接表,每个顶点对应一个邻居列表。graph[v].append((u, w))无向图要加两条边,有向图只需加一条。dist[start] = 0起点到自己的最短距离是 0。heapq.heappop(pq)每次弹出当前距离最小的未确定顶点。if d > dist[u]: continue这是经典的“懒删除”,跳过已经过期的旧纪录。for v, w in graph[u]遍历当前站点的所有直达邻居。dist[v] = nd发现更短的路线就立刻更新。heapq.heappush(pq, (nd, v))新纪录进入优先队列,等待下一次扩展。
常见误区
误区我觉得 DFS 能走遍所有路径,只要把所有路径都找出来再取最小值,不就能求最短路径了吗?所以最短路径题都用 DFS。
正解DFS 确实能遍历所有路径,但路径数量是指数级增长。无权图求最少经过边数应优先用 BFS,带权非负图用 Dijkstra。只有遇到负权边且不能用 Dijkstra 时,才考虑 Bellman-Ford 或 Floyd。DFS 更适合判断连通性、找环,而不是求最短路。
为何常见:初学者容易把“能搜索”等同于“高效搜索”,忽略了指数量级的路径爆炸。
误区邻接矩阵写起来最直观,判断两个点是否相邻也快,我就一直用邻接矩阵。
正解邻接矩阵空间是 \(V^2\)。当顶点数达到 \(10^5\) 时,矩阵要开 \(10^{10}\) 个位置,直接爆内存。面试中应该先判断稀疏还是稠密:稀疏图用邻接表,稠密图才适合邻接矩阵。
为何常见:因为刷题初期数据规模小,矩阵的坏处没有暴露;遇到大数据用例才突然超时或超内存。
误区Dijkstra 每次都找最小距离,看起来任何边权都能处理,负权只是让距离变小而已。
正解Dijkstra 一旦确定某个点的最短距离,就不会再更新它。负权边可能让“已经确定”的点从另一条路径获得更小距离,算法因此失效。如果图中存在负权边但无负环,应使用 Bellman-Ford。
为何常见:贪心直觉在正权图中成立,换成负权时“当前最小”就不再可靠,这是贪心算法边界的典型误区。
误区拓扑排序把任务排成一个顺序,所以结果应该是唯一的,不然怎么保证执行正确?
正解只要两个任务之间没有直接或间接依赖关系,它们就可以互换顺序。比如同时有两个入度为 0 的任务,Kahn 算法可以任选一个输出。所以 DAG 的拓扑序通常有多个,只有全序关系下才唯一。
为何常见:大脑容易把“算法输出一个结果”理解成“必须只有一个结果”,忽视了 DAG 中本可以并行的任务。
误区Prim 和 Kruskal 都能生成最小生成树,所以这道题的答案应该一样。
正解当图中存在多条权值相同的边时,不同的选边顺序可能得到不同的最小生成树,但总权值一定相同。面试中如果问你“树是否唯一”,要额外检查是否存在权值相等的候选边。
为何常见:忽略“最小总权值”和“具体哪棵树”的区别,容易把求解目标误当成唯一解。
面试题
Q1. 图的基本组成是什么?
- A.顶点和边← 正确答案
- B.表格和行
- C.点和点集
- D.栈和队列
答案:A。图是由顶点和边构成的结构,顶点表示对象,边表示对象之间的关系。表格和行更像数据的存储形态,点和点集不是标准图论概念,栈和队列是线性数据结构,都不符合图的定义。
Q2. 无向图中所有顶点的度数之和等于多少?
- A.顶点数量的两倍
- B.边数的两倍← 正确答案
- C.边数本身
- D.顶点数本身
答案:B。每条无向边连接两个顶点,会在计算总度数时被两个端点各计一次,所以所有顶点度数之和等于边数的两倍。这是握手引理,常用来检查输入数据是否自洽。
Q3. 无权图中求起点到终点的最少经过边数,应优先选择哪种算法?
- A.深度优先遍历(DFS)
- B.广度优先遍历(BFS)← 正确答案
- C.随机游走
- D.朴素邻接矩阵乘法
答案:B。BFS 按层扩散,第一次到达终点时经过的边数一定最少。DFS 走到底再回溯,无法保证路径最短。随机游走没有确定性答案,矩阵乘法时间复杂度太高,面试中不是首选。
Q4. Dijkstra 算法能正确处理哪种图?
- A.边权全部非负的图← 正确答案
- B.存在负权边的图
- C.存在负环的图
- D.任意带权有向图
答案:A。Dijkstra 依赖贪心选择当前最小距离,一旦确认就不再更新。若存在负权边,当前最小可能被后来路径更新,贪心失效。因此它只能用于边权非负的图。
Q5. 图中存在负权边但无负环,求单源最短路应首选哪种算法?
- A.Dijkstra 算法
- B.Floyd 算法
- C.Bellman-Ford 算法← 正确答案
- D.Prim 算法
答案:C。Bellman-Ford 会反复松弛所有边,能正确处理负权边且无负环的情况。Dijkstra 不支持负权,Floyd 更适合全源最短路,Prim 是求最小生成树的算法,不是求最短路。
Q6. Prim 算法和 Kruskal 算法分别基于哪种核心思想?
- A.加点扩展;按边排序合并← 正确答案
- B.按边排序合并;加点扩展
- C.动态规划;分治思想
- D.随机贪心;暴力枚举
答案:A。Prim 从某个顶点开始,不断把离已连接部分最近的点加进来,本质是“加点扩展”。Kruskal 把所有边按权值从小到大排序,再逐个合并并查集,本质是“按边合并”。两者都是贪心思想,不是动态规划或随机算法。
Q7. 哪些情况下拓扑排序一定不存在?(多选)
- A.图中存在有向环← 正确答案
- B.图中不存在入度为零的点← 正确答案
- C.图中存在孤立点
- D.图中包含双向边← 正确答案
答案:ABD。有向环和双向边都构成环,拓扑排序无法输出一个合法的全序。如果一个有向图完全没有入度为零的点,说明每个任务都有前置依赖,必然存在环,也无法拓扑排序。孤立点入度为零,不影响拓扑排序。
Q8. 邻接表存储图有哪些主要优点?(多选)
- A.空间只与顶点数和边数相关← 正确答案
- B.遍历某个顶点的邻居时效率高← 正确答案
- C.任意两点之间邻接判断最快
- D.便于在稀疏图上做 BFS 和 DFS← 正确答案
答案:ABD。邻接表只存与顶点真实相连的边,空间约为 \(V+E\),遍历邻居时能直接拿到邻居列表,适合稀疏图。但判断任意两点是否相邻需要遍历邻居列表,不如邻接矩阵快。
Q9. 关于包含负权边的图,下列说法正确的是?(多选)
- A.无负环时 Bellman-Ford 可求最短路径← 正确答案
- B.有负环时不存在有限最短路径← 正确答案
- C.Dijkstra 算法一定无法使用
- D.Floyd 可以处理无负环负权图← 正确答案
答案:ABD。Bellman-Ford 要求没有负环,因为负环会让路径无限减小。Floyd 同样要求无负环。Dijkstra 虽然在部分负权图里碰巧正确,但缺乏保证,所以不能说“一定无法使用”,只能说它不可靠。
Q10. 顶点数 \(10^5\)、边数 \(2 \times 10^5\) 的带权稀疏图,求单源最短路哪种方案最优?
- A.优先队列优化 Dijkstra← 正确答案
- B.Floyd 算法
- C.朴素 Dijkstra 扫描数组
- D.直接枚举所有路径
答案:A。稀疏图用邻接表加堆优化 Dijkstra,复杂度 \(O(E\log V)\) 完全可承受。Floyd 是 \(O(V^3)\),朴素 Dijkstra 是 \(O(V^2)\),面对十万顶点都会超时;枚举路径更是指数爆炸。