递归DFS需在进入前标记visited以防重复访问和栈溢出,迭代DFS须逆序压栈并出栈时标记;需路径时递归更易回溯,无向图需防父节点回溯。

递归实现 DFS 时节点访问顺序容易出错
递归写法最直观,但新手常把 visited 标记位置放错,导致重复访问或漏访问。必须在进入递归前就标记当前节点,而不是在循环里或递归调用后才标。
- 正确顺序:标记 → 访问 → 遍历邻接点 → 递归
- 错误写法示例:
foreach (var next in neighbors) { DFS(next); visited[next] = true; }—— 这会导致子节点被多次递归进入 - 图中存在环时,不提前标记
visited会直接栈溢出 - 递归深度受限于 .NET 默认线程栈(约 1MB),超深树(如 >10000 层)可能抛
StackOverflowException(.NET 6+ 在非 Windows 上不可捕获)
用 Stack 手动模拟递归要反向压栈
迭代版 DFS 用 Stack<T> 实现,关键在于:邻接节点必须「逆序」入栈,才能保证和递归版一致的访问顺序(比如左子树先于右子树)。
- 若按自然顺序压栈(如先压左再压右),弹出时变成右先访问 —— 和递归行为不一致
- 正确做法:
for (int i = neighbors.Count - 1; i >= 0; i--) stack.Push(neighbors[i]); -
visited仍需在出栈后、访问前立即标记,避免同一节点被多次压入 - 注意不要在入栈时就标记,否则未访问的节点被提前“锁定”,会漏掉路径
DFS 返回路径时,递归比栈更容易回溯
当需要返回从起点到目标的完整路径(如迷宫求解),递归天然支持回溯:函数返回即自动“撤出”当前节点;而栈版本需额外维护父指针或路径栈,逻辑更重。
- 递归可直接用局部
List<T>累积路径,命中目标后直接返回该列表 - 栈实现需为每个入栈节点保存其前驱(如用
Dictionary<T, T>),或让栈元素是元组(node, pathSoFar),内存开销明显上升 - 若只判断连通性(bool 返回值),两者性能差异不大;但涉及路径重建,递归代码更紧凑、不易错
遍历无向图时记得跳过父节点防自环
对树或无向图做 DFS,若不加限制,从子节点回看父节点会被当作新邻居,造成“原地振荡”。这不是 bug,而是图结构特性导致的,必须显式规避。
- 递归版可在参数中传入
parent,遍历时跳过:if (next != parent) DFS(next, current); - 栈版本则需在压栈前检查:
if (next != currentNode) stack.Push(next);(仅适用于已知父子关系的场景) - 更通用的做法仍是依赖
visited数组——只要节点已访问过就跳过,但前提是图不含重边且初始化充分 - 这个细节在邻接表实现中极易被忽略,一跑测试用例就无限循环
实际用哪一种,取决于你是否担心栈深度、是否需路径信息、以及图是否有向。递归写起来快,但生产环境面对深层图建议切栈实现;而一旦要返回路径,又得权衡可读性和内存占用——这些不是理论选择,是上线前真会卡住你的点。










