【算法图解】DFS:一条路走到黑

DFS(深度优先搜索)像走迷宫:先走到底,走不通再回头。本文讲清递归/显式栈实现、visited、回溯关系、复杂度、典型题型(岛屿数量、路径搜索)与工程注意点。

16 分钟阅读
小明

【算法图解】DFS:一条路走到黑

DFS(Depth-First Search,深度优先搜索)像走迷宫:

  • 先沿着一条路走到底
  • 走不通就回退(backtrack)
  • 换一条路继续

DFS 是很多题的基础骨架:

  • 图遍历
  • 连通块/岛屿数量
  • 路径搜索
  • 拓扑排序(配合颜色标记)

1. DFS 的两种写法:递归与显式栈

1.1 递归 DFS(最直观)

function dfs(graph, start) {
  const visited = new Set()
  const order = []

  function visit(node) {
    visited.add(node)
    order.push(node)

    for (const next of graph[node] ?? []) {
      if (!visited.has(next)) visit(next)
    }
  }

  visit(start)
  return order
}

优点:代码短、表达清晰。

风险:递归深度过大可能爆栈。

1.2 显式栈 DFS(更工程)

function dfsIter(graph, start) {
  const visited = new Set([start])
  const stack = [start]
  const order = []

  while (stack.length) {
    const node = stack.pop()
    order.push(node)

    const neighbors = graph[node] ?? []
    for (let i = neighbors.length - 1; i >= 0; i--) {
      const next = neighbors[i]
      if (!visited.has(next)) {
        visited.add(next)
        stack.push(next)
      }
    }
  }

  return order
}

这里逆序压栈是为了让遍历顺序更接近递归版(可选)。


2. visited 的必要性:图里没有它会无限走

DFS 如果不标记 visited:

  • 在图中遇到环会无限递归/无限循环

所以图 DFS 基本都要 visited。


3. DFS 与回溯的关系

很多人把 DFS 和回溯混在一起。

你可以这样理解:

  • DFS 是一种遍历策略(先深后广)
  • 回溯是在 DFS 过程中“试错 + 撤销选择”的技巧

当你需要在递归返回时撤销状态(例如路径、选择集),那就是回溯题。


4. 复杂度

  • 时间:$O(V+E)$
  • 空间:$O(V)$(visited + 栈/递归栈)

递归版空间上要注意:

  • 递归深度在最坏情况下可以到 $O(V)$。

5. 典型题型

5.1 岛屿数量(二维网格 DFS)

把网格看成图:

  • 每个格子是节点
  • 上下左右是边

DFS 遇到陆地就淹掉(标记 visited),计数连通块。

5.2 路径搜索

  • 找一条可行路径
  • 或枚举所有路径

后者通常需要回溯(撤销路径)。


6. 面试回答模板

  • DFS 深度优先,沿一条路走到底再回退。
  • 可以用递归或显式栈实现。
  • 图遍历需要 visited 防止环。
  • 复杂度 $O(V+E)$。

总结

  • DFS 是图遍历的基础。
  • 递归直观,但要注意栈深。
  • 回溯是 DFS 的“试错与撤销”增强版。