想象你站在一座巨大地下迷宫的入口,手里只有一个背包,宝物藏在某条通道尽头。通道交错纵横、岔路密布,你可能绕半天又回到原点。问题是:如何保证把每条该走的通道都探索一遍,还不迷路? 计算机科学给了两种截然不同的答案。第一种叫“一根筋探险家”:选一条通道走到底,走不通就原路退回,换下一个岔路再走到底,直到把所有角落摸清。这在图论里叫深度优先搜索(DFS)——遇到岔路先深入,实在不行才回头。第二种叫“水波搜索队”:你在入口倒下一桶水,水波均匀地朝所有方向扩散,一圈、两圈……水波最先漫过的地方,就是离你最近的出口。这在图论里叫广度优先搜索(BFS)——像投石入湖的涟漪。在计算机的世界里,“迷宫”叫图(Graph),“通道”叫边(Edge),“岔路口”叫顶点(Vertex)。DFS 和 BFS 是所有图算法中最基础的两把钥匙:社交推荐、地图导航、依赖检测,全都从它们出发。这篇文章,我们就跟着探险队和水波,把图遍历的来龙去脉走一遍。

岗位关联

图遍历是最短路、拓扑排序、连通分量等一切图算法的地基。读懂 DFS 与 BFS,你就能理解知识图谱如何推理、搜索引擎如何抓网页、地图 App 如何规划路线——这是 AI 从业者处理关系型数据的必备直觉。

目录
  1. 可视化演示
  2. DFS深度优先搜索
  3. BFS广度优先搜索
  4. 复杂度分析
  5. 连通分量
  6. 环检测应用
  7. 最短路径应用
  8. 遍历选型对比
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

图的深度优先搜索(DFS)和广度优先搜索(BFS)是图算法的基础,分别基于栈和队列实现。本文讲解DFS的递归与... 切换模式查看不同维度。

本节配有交互动效(进入视口后自动加载,约 3KB)。

DFS递归与:核心原理 动态展示关键流程与数据流动 交互控件调节参数实时渲染 f(x) = transform(x) 核心概念的可视化呈现

DFS深度优先搜索

探险家背着一捆绳子和一盒粉笔进了迷宫。他的策略很“轴”:在每一个岔路口,用粉笔在走过的入口画个叉,然后永远优先选择没画叉的最深通道往前走。万一前方是死胡同,他就顺着绳子一步步退回上一个岔路口,再试下一条路。粉笔防止他重复走进同一条通道,绳子则代表“回溯”的依据——在代码里,这条绳子就是栈,而粉笔就是 visited 访问标记。

这段“走到底再回头”的过程,就是深度优先搜索(DFS)。给定一个起点顶点,DFS 沿着一条路径尽可能深地访问,直到这条路上没有未访问的邻居,再回溯到最近的分叉点继续。实现上,递归调用天然就是“栈”:函数调用栈帮你保存了每一层的回溯现场;你也可以用显式栈把递归改写成循环,原理完全一致——后进先出,后访问的邻居先被深入。

在无向图中,DFS 的递归式可以写成:访问当前顶点,再递归访问它的每一个未访问邻居。

所以 DFS 的“一根筋”性格来自递归 + 栈:一路深入,无路可退时把绳子收回来。面试和工程中,显式栈版本能避免递归过深导致栈溢出,但核心思想不变:能深入就深入,深入不了就回头。

DFS(v)=visit(v)+∑u∈N(v), u∉visitedDFS(u)\text{DFS}(v) = \text{visit}(v) + \sum_{u \in N(v),\, u \notin \text{visited}} \text{DFS}(u)
这就像探险家在岔路口画叉后,对所有没画叉的通道依次“走到底”。
  1. 访问当前顶点把顶点 v 标记为已访问,并加入遍历序列。
    visit(v)\text{visit}(v)
  2. 取出邻居集合N(v) 是 v 的所有相邻顶点。
    N(v)N(v)
  3. 筛选未访问邻居只挑那些没有被粉笔标记过的邻居参与递归。
    u∉visitedu \notin \text{visited}
  4. 逐个递归深入对每个未访问邻居调用 DFS,也就是走向下一段通道。
    ∑uDFS(u)\sum_{u} \text{DFS}(u)

BFS广度优先搜索

假设探险家换了一种策略:他不再自己走,而是在入口倒下一桶水,水波会同时朝所有方向漫延。第一圈的波纹覆盖所有紧挨着入口的岔路口,第二圈覆盖岔路口的岔路口……波纹扩散得越远,走过的步数越多。这种“一圈圈往外推”的方式,就是广度优先搜索(BFS)。

BFS 的核心是队列:先进先出(FIFO),保证先发现的顶点先被扩展。过程像流水线:从起点出发,把起点所有未访问邻居全部“入队”,然后一个一个出队,每出队一个顶点,又把它的未访问邻居继续入队。这样一来,第 1 层的顶点会在第 2 层之前全部出队处理,天然形成“逐层扫描”。

如果我们用 dist 数组记录“起点到每个顶点的层数”,那么当顶点 v 出队、首次发现邻居 w 时,w 的层数一定比 v 多 1:

BFS 的“水波”性格来自队列 + 逐层扩散:先见先处理,同层全部处理完才进入下一层。正因为如此,在无权图上,BFS 首次访问到某个顶点时,就一定走的是最短步数——水波最先到的,一定是最近的岸。

dist[w]=dist[v]+1\text{dist}[w] = \text{dist}[v] + 1
水波每向外扩一圈,步数就加 1;层数就是“波纹的圈数”。
  1. 当前顶点出队从队列头部取出顶点 v,此时它是本层中的一个节点。
    v=queue.dequeue()\text{v} = \text{queue.dequeue}()
  2. 已知层数dist[v] 表示起点到 v 经过的边数。
    dist[v]\text{dist}[v]
  3. 邻居被首次发现w 是 v 的邻居,且此前从未入过队。
    w∈N(v), w∉visitedw \in N(v),\, w \notin \text{visited}
  4. 层数加一w 比 v 晚一层被水波覆盖。
    dist[w]=dist[v]+1\text{dist}[w] = \text{dist}[v] + 1

复杂度分析

探险队出发前要估算体力。走完整张图需要多少时间、多少背包空间,取决于你手里拿的是什么样的“地图”。邻接表像一张手绘通道清单:每个岔路口只记下直接相连的通道;邻接矩阵则像一张巨大的全景照片:所有点对之间“有无通道”都清清楚楚。

用邻接表存储时,DFS 和 BFS 的时间开销都很省:每个顶点被访问一次,每条边被检查一次(无向图中每条边会被两条邻接表记录各看一次,但常数倍可忽略),所以总时间是顶点数加边数。空间上,递归栈或队列在最坏情况下可能装下所有顶点。

如果换成邻接矩阵,检查一个顶点的所有邻居必须扫描一整行(V 个格子),所有顶点加起来就是 O(V²)。所以对稀疏图(边少),邻接表完胜;对稠密图,邻接矩阵的 O(V²) 也未必差——毕竟边数本身就有 V² 的量级。选哪种存储,就像选手绘清单还是全景照片,取决于迷宫的密度。

T(V,E)=O(V+E),S(V,E)=O(V)T(V,E) = O(V + E), \qquad S(V,E) = O(V)
这张“体力账单”告诉我们:用邻接表遍历,时间和边数成正比;空间则和顶点数成正比。
  1. 顶点总数V 是图中所有岔路口的数量。
    VV
  2. 边总数E 是所有通道的数量。
    EE
  3. 遍历每点每边每个顶点入队/入栈一次,每条边被扫描一次。
    O(V+E)O(V + E)
  4. 空间上限栈、队列和标记数组最坏要存下所有顶点。
    O(V)O(V)

连通分量

地下迷宫往往不止一个洞窟:有些通道彼此相连成一片,但和另一片之间完全隔绝。在无向图中,这些“互不相通却内部连成一片”的顶点集团,就叫连通分量(Connected Component)。整个图被分成几块,就有几个连通分量。

怎么数清楚迷宫有几座“孤岛”?办法很简单:把所有顶点挨个询问一遍——如果某个顶点还没有被粉笔标记过,就说明它属于一座还没探索过的洞窟,于是从它启动一次 DFS 或 BFS,把这座洞窟里的所有顶点全部标记;计数加一。重复直到所有顶点都被标记过。

这个思想在图像处理里被称作洪水填充(Flood Fill):从某个像素点出发,像倒水一样把颜色相同的连通区域全部染成新颜色——Photoshop 的魔棒工具就是这么识别一块区域的。对 AI 从业者来说,连通分量也是图聚类、社群发现最朴素的雏形:把社交网络分成互不交流的“朋友圈”,本质就是找连通分量。

components=∑v∈V[ v∉visited⇒从v启动一次遍历, count+1 ]\text{components} = \sum_{v \in V} \big[\, v \notin \text{visited} \Rightarrow \text{从v启动一次遍历},\ \text{count} + 1 \,\big]
每遇到一座“未被闯入的洞窟”,就派一支探险队进去全部标一遍,队伍数量就是洞窟数。
  1. 逐个检查顶点遍历所有岔路口 v。
    v∈Vv \in V
  2. 判断是否已标记如果 v 从未被访问,说明它所在洞窟还没被探索。
    v∉visitedv \notin \text{visited}
  3. 启动一次完整遍历从 v 出发做 DFS/BFS,把同属一个洞窟的顶点全部标记。
    traverse(v)\text{traverse}(v)
  4. 分量计数加一每启动一次,连通分量数加 1。
    count=count+1\text{count} = \text{count} + 1

环检测应用

迷宫里最怕遇到“回环通道”:你走了一圈,发现自己又回到了粉笔标记过的地方。在图中,这就是环(Cycle)。如果一张图里存在环,某些“依赖”关系就无法成立——比如课程 A 依赖 B,B 依赖 C,C 又依赖 A,那谁都没法先修。检测环,是拓扑排序和依赖解析的前提。

DFS 检测环有个优雅的“三色粉笔”技巧:白色表示完全没走过,灰色表示正在走但还没走完(还在当前探索栈中),黑色表示已经彻底探索完毕。当 DFS 在深入过程中碰到一条边,指向一个灰色顶点时,说明你绕了一圈回到了当前路径上的祖先——这就是环。

在 AI 工程中,环检测最常见的舞台是有向无环图(DAG)校验:机器学习流水线、任务调度框架(如 Airflow)都会先用 DFS 三色标记检查依赖图是否含环。一旦发现环,立刻报警,否则任务永远排不出执行顺序。

state[u]=GRAY ∧ (u,v)∈E ∧ state[v]=GRAY  ⇒  存在环\text{state}[u] = \text{GRAY} \,\wedge\, (u,v) \in E \,\wedge\, \text{state}[v] = \text{GRAY} \;\Rightarrow\; \text{存在环}
灰色就像“探险队此刻正在这条路上的标记”;如果在迷宫中撞见自己的队伍,说明走进了回环。
  1. 白色未访问顶点还没被任何探险队碰到。
    state[v]=WHITE\text{state}[v] = \text{WHITE}
  2. 灰色在栈中顶点已被访问,但它的邻居还没全部探索完,仍留在递归栈里。
    state[v]=GRAY\text{state}[v] = \text{GRAY}
  3. 黑色结束回溯该顶点的所有邻居都探索完,递归已返回。
    state[v]=BLACK\text{state}[v] = \text{BLACK}
  4. 灰边判环当前顶点 u 有一条边指向灰色顶点 v,说明 u 能绕回自己的祖先。
    state[u]=GRAY∧state[v]=GRAY\text{state}[u] = \text{GRAY} \wedge \text{state}[v] = \text{GRAY}

最短路径应用

水波搜索队最神奇的能力是:它天生知道最短路径。假定所有通道长度一样(无权图),我们从起点倒水,波纹到达某个岔路口的“圈数”,就是从起点到这个岔路口的最少步数。这个性质来自 BFS 的逐层扩散——第 k 层的顶点,最短距离恰好是 k。

算法上,我们用一个 dist 数组记录起点到每个顶点的距离,初始时起点为 0,其他顶点为无穷大。BFS 过程中,每当从队列中取出顶点 v、第一次发现邻居 w 时,就设置 dist[w] = dist[v] + 1。因为 BFS 保证顶点按层数从小到大出队,所以这个“第一次赋值”就是最短距离。

如果想还原整条路径,只需在设置 dist[w] 时顺手记下“前驱” prev[w] = v,最后从终点沿着 prev 一路回溯到起点,就得到完整路线。这在迷宫求解、电梯调度、社交“几度人脉”都是标准操作。注意:一旦通道有了不同长度(带权图),BFS 就不保证最短了,那要请出 Dijkstra 算法——那是后话。

δ(s,w)=distBFS[w]\delta(s, w) = \text{dist}_{\text{BFS}}[w]
真实最短距离 δ 恰好等于 BFS 记录的层数,水波不会“抄近路”,也不会绕远。
  1. 真实最短距离δ(s, w) 表示从起点 s 到顶点 w 的最少边数。
    δ(s,w)\delta(s, w)
  2. BFS 距离数组dist 数组记录 BFS 过程中各顶点首次被发现时的层数。
    distBFS[w]\text{dist}_{\text{BFS}}[w]
  3. 两者相等BFS 首次发现即走最短路径。
    δ(s,w)=distBFS[w]\delta(s, w) = \text{dist}_{\text{BFS}}[w]

遍历选型对比

现在你背包里有了两种工具:探险家的绳子(DFS)和水波的扩散桶(BFS)。干活前先想清楚任务类型。如果问题是“有没有一条路径通往宝藏”“打印所有可能的路线”“检测有没有回环”,DFS 更顺手——它可以快速深入解空间,配合回溯枚举所有情况。如果问题是“从入口到出口最少几步”,BFS 是唯一正确答案——它逐层扩散,第一次碰到出口时就是最短步数。

工程上还有个常被忽略的差异:空间占用。DFS 的空间和路径深度成正比,在树形分叉多的图上很省;BFS 的空间和“当前层最宽处”成正比,如果图是“扇形展开”,BFS 队列可能瞬间膨胀。另外 DFS 的递归实现代码极短,但递归太深有栈溢出风险,显式栈可以化解;BFS 用队列实现,逻辑直观,没有递归深度问题。

剪枝策略也常让选择改变:DFS 配合可行性剪枝,能在巨大的解空间里快速排除死路(比如八皇后、数独);BFS 配合分支限界,则能在搜索树上按层剪掉次优分支。理解了 DFS 与 BFS 的底层性格,你就掌握了图算法世界的第一对“阴阳”。

Choose={BFS需要最短路径 / 逐层扩散DFS需要优先深入 / 枚举解空间\text{Choose} = \begin{cases} \text{BFS} & \text{需要最短路径 / 逐层扩散} \\ \text{DFS} & \text{需要优先深入 / 枚举解空间} \end{cases}
探洞用绳子,渡水要放波;不是谁更高级,而是看任务要“深度”还是要“广度”。
  1. 任务分类先判断问题核心是“求最短/分层”还是“求可行解/探底”。
    问题类型\text{问题类型}
  2. 选 BFS 的情形无权最短路、层序遍历、波纹式扩散。
    需要最短路径⇒BFS\text{需要最短路径} \Rightarrow \text{BFS}
  3. 选 DFS 的情形路径存在性、深度优先枚举、回溯剪枝搜索。
    需要优先深入⇒DFS\text{需要优先深入} \Rightarrow \text{DFS}

AI 应用场景

知识图谱中的实体可达性查询

阶段:数据预处理与图结构分析

解决的问题:知识图谱(如 Google Knowledge Graph)中,“A 是否间接关联到 B”是高频查询。传统 SQL 递归查询在深层次关联上性能差、写法复杂。用 DFS 从实体 A 出发深度遍历,遇到 B 即返回,天然适配“可达性”语义。

具体案例:在电商知识图谱中,判断“用户浏览的球鞋品牌”是否与“某环保材料供应商”存在供应链关联。用 DFS 在商品—品牌—材料—供应商的多跳路径中搜索,命中即返回路径,支持反欺诈和合规审查。

社交网络的好友推荐与分层

阶段:特征工程与图算法应用

解决的问题:社交 App 要推荐“你可能认识的人”,本质是找你二度人脉。BFS 从当前用户出发扩散两层,把第一层邻居排除,第二层邻居按共友数排序即可。相比暴力全图计算,BFS 只扩两层,计算量小一个量级。

具体案例:在一个百万级 DAU 的社区产品中,后端用图数据库执行 BFS 层序遍历:第 1 层是直接好友,第 2 层是“好友的好友”,按交集规模生成推荐列表。这个方案比离线全量计算快 3 倍,且天然支持实时更新。

机器学习中的依赖环检测

阶段:模型训练流水线治理

解决的问题:机器学习平台用有向无环图(DAG)描述“数据清洗→特征工程→训练→评估”的依赖关系。若配置人员不小心把依赖写成环,任务会永远无法调度。用 DFS 三色标记法在提交 DAG 时做一次环检测,违规直接报错。

具体案例:某公司的特征平台每天有上千个特征任务依赖。平台在发布前对 DAG 执行 DFS 环检测,一旦发现“特征 A 依赖 B、B 依赖 A”的循环依赖,立即拒绝发布并提示具体环节。上线后调度死锁事件下降了 90%。

Python 演示

python85 行
from collections import deque def build_graph(edges):    graph = {}    for u, v in edges:        graph.setdefault(u, []).append(v)        graph.setdefault(v, []).append(u)    return graph def dfs_recursive(graph, start, visited=None, order=None):    if visited is None:        visited = set()        order = []    visited.add(start)    order.append(start)    for neighbor in graph.get(start, []):        if neighbor not in visited:            dfs_recursive(graph, neighbor, visited, order)    return order def dfs_stack(graph, start):    visited = set()    stack = [start]    order = []    while stack:        node = stack.pop()        if node in visited:            continue        visited.add(node)        order.append(node)        for neighbor in reversed(graph.get(node, [])):            if neighbor not in visited:                stack.append(neighbor)    return order def bfs_queue(graph, start):    visited = {start}    queue = deque([start])    order = []    while queue:        node = queue.popleft()        order.append(node)        for neighbor in graph.get(node, []):            if neighbor not in visited:                visited.add(neighbor)                queue.append(neighbor)    return order def count_components(graph):    visited = set()    count = 0    for node in graph:        if node not in visited:            dfs_recursive(graph, node, visited, [])            count += 1    return count def has_cycle(graph):    state = {}     def dfs(v):        state[v] = 1        for u in graph.get(v, []):            if u not in state:                if dfs(u):                    return True            elif state[u] == 1:                return True        state[v] = 2        return False     for v in graph:        if v not in state:            if dfs(v):                return True    return False if __name__ == "__main__":    edges = [(0, 1), (0, 2), (1, 3), (1, 4), (2, 5)]    graph = build_graph(edges)    print("DFS recursive:", dfs_recursive(graph, 0))    print("DFS with stack:", dfs_stack(graph, 0))    print("BFS order:", bfs_queue(graph, 0))    print("components:", count_components(graph))    print("has cycle:", has_cycle(graph))
目的:用同一张图演示 DFS(递归与栈两种写法)、BFS、连通分量计数和环检测,输出结果可对照正文概念。
  1. def build_graph(edges):把边列表转成邻接表,方便按顶点取邻居。
  2. visited.add(start)相当于探险家在岔路口用粉笔画叉,防止重复进入。
  3. for neighbor in graph.get(start, []):逐个取出当前顶点的邻居通道。
  4. if neighbor not in visited:只走没被标记过的通道。
  5. node = stack.pop()显式栈后进先出,模拟递归的“回溯现场”。
  6. for neighbor in reversed(graph.get(node, [])):逆序入栈,让访问顺序与递归版一致。
  7. queue = deque([start])队列初始化,起点先入队。
  8. node = queue.popleft()从队头取出顶点,先进先出保证逐层扩散。
  9. count += 1每启动一次完整遍历,就多一座“洞窟”(连通分量)。
  10. state[v] = 1DFS 环检测中把顶点标记为灰色,表示正在递归栈中。

常见误区

误区“树遍历也不需要 visited,图不就是多几根线吗?我直接递归不就行了?”

正解树天然没有环,从根出发不会走回祖先;但图可能带环,如果不做访问标记,DFS/BFS 会在环里无限循环,就像探险家在回环通道里绕圈永远出不来。visited 标记(粉笔)是图遍历和树遍历的本质区别。

为何常见:初学者先学二叉树遍历,习惯了“自上而下无重复”的思维,迁移到图上时忽略了“回边”的存在。

误区“BFS 能找到最短路径,那地图导航直接用 BFS 不就行了?”

正解BFS 保证最短的前提是无权图(所有边长度相同)。一旦边有权重(比如公路有长有短),BFS 只按“跳数”扩散,可能先到一条“跳数少但总里程长”的路。带权最短路要交给 Dijkstra 或 A* 算法。

为何常见:教材通常用无权图讲 BFS 最短路,“首访即最短”的印象太深刻,忽略了权重这个隐藏前提。

误区“图太大了递归会爆栈,所以 DFS 在大图上根本不能用。”

正解递归只是 DFS 的一种实现形式,函数调用栈帮你保存回溯现场。你完全可以用显式栈(自己管理一个栈数据结构)改写为迭代版本,空间开销可控,还能避免系统栈溢出。正文 Python 演示中的 dfs_stack 就是现成例子。

为何常见:教材常以递归写法为主,很多人没有意识到“递归”与“栈”是一体两面,显式栈改写其实很机械。

误区“复杂度公式不是 O(V+E) 吗?不管邻接表还是邻接矩阵,都一样吧。”

正解O(V+E) 来自邻接表的存储特性——每条边在表里出现一次,扫描所有邻接表恰好 O(V+E)。邻接矩阵检查一个顶点的所有邻居必须扫一整行,V 个顶点就是 O(V²)。即使边很少,矩阵也要扫完全部 V² 个格子。

为何常见:公式记熟了,却没和“存储结构如何决定访问方式”联系起来,于是忽略了常数背后的结构性差异。

误区“我用 visited 标记,访问到已标记的节点不就说明有环吗?”

正解visited 只能防止死循环,不能判环。无向图中“遇到已访问邻居”不一定是环(可能只是回退到父节点);有向图中“遇到黑色已访问节点”也不是环,因为那条边可能是跨子树的横叉边。正确做法是三色标记:只有遇到灰色(仍在当前递归栈中)的顶点,才说明存在环。

为何常见:初学时把“访问过”当成唯一状态,没有意识到 DFS 递归过程中顶点其实有“访问中”和“访问完”两种不同阶段。

面试题

Q1. 深度优先搜索(DFS)遍历图时,底层依赖哪种数据结构来模拟“回溯”过程?

单选题 · easy

  • A.队列 queue
  • B.栈 stack← 正确答案
  • C.最小堆 heap
  • D.哈希表 hash

答案:B。DFS 的“走到底再回头”本质是后进先出——最后进入的节点最先被深入,这正是栈的特性。队列是 BFS 用的,堆和哈希表与回溯无关。就像探险家顺绳子原路退回,绳子就是栈。

Q2. 广度优先搜索(BFS)逐层扩散时,保证“先发现的顶点先被扩展”需要用到哪种结构?

单选题 · easy

  • A.栈 stack
  • B.队列 queue← 正确答案
  • C.优先队列 priority queue
  • D.链表 linked list

答案:B。BFS 要求按发现顺序处理顶点,先进先出的队列天然满足。栈会让后发现的先处理,变成 DFS;优先队列用于 Dijkstra 等带权场景;链表本身不提供先进先出的队列语义。

Q3. 在遍历带环图时,为什么必须设置 visited 访问标记?

单选题 · easy

  • A.为了统计图中的顶点个数
  • B.防止重复访问造成死循环← 正确答案
  • C.为了让遍历结果按编号排序
  • D.标记只是编程习惯,不影响正确性

答案:B。图中存在回边时,若不标记已访问顶点,DFS/BFS 会在环上无限绕圈。visited 相当于探险家的粉笔标记,保证每个岔路口只进一次。选项 A、C 都不是标记的目的,D 明显错误。

Q4. 用邻接表存储图时,从单一起点出发完成一次 DFS 或 BFS 的时间复杂度是?(V 为顶点数,E 为边数)

单选题 · mid

  • A.O(V)
  • B.O(E)
  • C.O(V + E)← 正确答案
  • D.O(V × E)

答案:C。邻接表遍历时每个顶点入栈/入队一次,每条边被扫描一次,总计 O(V+E)。此复杂度只适用于邻接表;若用邻接矩阵则需要 O(V²)。

Q5. 下列哪些场景适合使用 BFS 求解?

多选题 · mid

  • A.无权图的最短路径← 正确答案
  • B.社交网络“几度人脉”的层数计算← 正确答案
  • C.记忆化回溯搜索所有可行解
  • D.递归下降解析表达式

答案:AB。BFS 逐层扩散,天然适合无权图最短路和层数统计。选项 C、D 需要深度探索与回溯,属于 DFS 的典型场景。BFS 不擅长枚举所有解,因为它需要逐层保存大量状态。

Q6. 用三色标记法做环检测时,GRAY(灰色)状态表示什么?

单选题 · mid

  • A.顶点从未被访问
  • B.顶点已被访问,但其邻居尚未完全探索完,仍在递归栈中← 正确答案
  • C.顶点已完全探索完毕并回溯
  • D.顶点已被删除

答案:B。灰色表示 DFS 正在处理该顶点的子树,它仍留在递归栈中。若当前节点发现一条指向灰色节点的边,说明绕回了祖先,存在环。A 是白色,C 是黑色。

Q7. 一个无向图被分为 3 个连通分量,若用“逐点检查+启动遍历”的方法数分量,至少需要启动几次遍历?

单选题 · mid

  • A.1 次
  • B.2 次
  • C.3 次← 正确答案
  • D.取决于边的数量

答案:C。每个连通分量内部互相可达,但分量之间不相连。从任意一个未访问顶点启动遍历,只能覆盖它所在的那一个分量;要覆盖 3 个分量,必须分别在每个分量里启动一次,共 3 次。

Q8. 用显式栈将递归 DFS 改写为迭代版本,下列说法正确的有?

多选题 · hard

  • A.入栈时标记 visited 可避免同一顶点重复入栈← 正确答案
  • B.若要保留与原递归一致的访问顺序,通常需要把邻接点逆序压栈← 正确答案
  • C.出栈时标记 visited 可能导致某些顶点被多次压入栈中← 正确答案
  • D.改成迭代后不再需要回溯思想

答案:ABC。迭代 DFS 仍需维护“回溯现场”,只是从系统调用栈换成了显式栈,D 错。入栈即标记能防止重复入栈;由于栈后进先出,逆序压栈才能恢复递归版的邻居访问顺序;若等到出栈才标记,同一顶点的多条边可能把它重复压入栈中。

Q9. 关于 DFS 与 BFS 的对比,正确的有?

多选题 · hard

  • A.BFS 在无权图上首次到达某顶点时即为最短路径← 正确答案
  • B.DFS 适合在解空间中深度探索“找一条可行路径”← 正确答案
  • C.BFS 的空间复杂度一定低于 DFS
  • D.两者若不加 visited 标记,在带环图上都可能死循环← 正确答案

答案:ABD。BFS 逐层扩散,首访即最短,A 对;DFS 深入到底回溯,适合解空间搜索,B 对;visited 对两者都必需,D 对。C 错:BFS 空间取决于“队列最宽处”,DFS 取决于“路径深度”,没有绝对高低。

Q10. 对 n 个顶点的完全图(每对顶点之间都有边)用邻接矩阵存储,执行一次 BFS 的时间复杂度是?

单选题 · extreme

  • A.O(n)
  • B.O(n²)← 正确答案
  • C.O(n³)
  • D.O(n log n)

答案:B。完全图的边数本就是 O(n²),若用邻接表,BFS 为 O(V+E)=O(n+n²)=O(n²);但题中指定邻接矩阵,遍历每个顶点的邻居都必须扫描一整行(n 格),n 个顶点共扫 n² 格。两种角度都指向 O(n²)。容易误选 C,是因为把“每个邻居处理一次”错算成“每个顶点扫描全矩阵”。