算法之深度优先搜索(DFS)

深度优先搜索(Depth-First Search, DFS)是一种用于遍历或搜索树或图的算法。它沿着树的深度遍历节点,尽可能深地探索每个分支,直到达到叶子节点或无法继续深入为止,然后回溯,探索下一个分支。DFS 在解决诸如图的连通性、拓扑排序、回路检测等问题上展现出其独特优势。本文旨在深入探讨 DFS 的原理、实现、特性及实际应用案例,帮助读者全面掌握这一重要算法。

1. 原理与算法描述

1.1 基本概念

DFS 的核心思想在于“深入到底,再回头”。算法从起始顶点出发,访问一个顶点后,先探索与之相连且未被访问过的顶点,直到到达一个叶子节点或无法继续深入,然后回溯到上一个顶点,继续探索其他未访问的分支。DFS 可以用于解决路径查找、图的连通性、拓扑排序等问题。

1.2 实现方法

DFS 可以通过递归或迭代两种方式实现:

  • 递归:自然适合 DFS 的递归结构,每进入一个新顶点,就调用自身处理该顶点的邻接点,直至到达叶子节点或遇到已访问过的顶点返回。
  • 迭代:使用栈来模拟递归过程,维护一个待访问顶点的栈,每次从栈顶取出顶点访问,然后将未访问的邻接顶点压入栈。

1.3 时间与空间复杂度

DFS 的时间复杂度一般为 O(V+E),其中 V 是顶点数,E 是边数。在最坏情况下,需要访问每个顶点和每条边。空间复杂度取决于递归深度或迭代过程中使用的栈空间,最坏情况下为 O(V)。

2. 特性与应用场景

2.1 特性

  • 深度优先:优先探索一条路径的尽头。
  • 回溯:当一条路径走不通时,回退到上一节点,尝试其他路径。
  • 剪枝:在某些问题中,DFS 可以提前终止不必要的探索,提高效率。
  • 拓扑排序:DFS 可用于有向无环图(DAG)的拓扑排序,确定任务的执行顺序。

2.2 应用场景

  • 图的连通性:判断图是否连通,找到所有连通分量。
  • 回路检测:DFS 可以用来检测图中是否存在环。
  • 迷宫求解:通过标记已访问路径,DFS 能有效地求解迷宫问题。
  • 树的遍历:DFS 是树遍历的基本方法之一,包括前序、中序和后序遍历。
  • 顶点间的最短路径:在特定类型的图中,如无权图,DFS 可以找到两点之间的路径(但不是最短路径)。

3. 实战案例:岛屿数量(LeetCode 200)

问题描述

给定一个二维网格地图,由'1'(陆地)和'0'(水域)组成,计算岛屿的数量。岛屿由水平或垂直相邻的陆地组成。

// 示例1:
const grid1 = [
  ["1", "1", "1", "1", "0"],
  ["1", "1", "0", "1", "0"],
  ["1", "1", "0", "0", "0"],
  ["0", "0", "0", "0", "0"],
];
// 输出:1

// 示例2:
const grid2 = [
  ["1", "1", "0", "0", "0"],
  ["1", "1", "0", "0", "0"],
  ["0", "0", "1", "0", "0"],
  ["0", "0", "0", "1", "1"],
];
// 输出:3

解决方案

算法思路

采用 DFS 遍历每个'1'(陆地),一旦遇到就标记为已访问,并递归探索其所有相连的陆地,直到该岛屿的所有部分都被访问过。使用一个计数器记录岛屿数量。

代码实现

var numIslands = function (grid) {
  let islandCount = 0;
  for (let row = 0; row < grid.length; row++) {
    for (let col = 0; col < grid[0].length; col++) {
      if (grid[row][col] == 1) islandCount += 1;
      dfs(grid, row, col); //从当前陆地开始DFS
    }
  }
  return islandCount;
};

// 深度优先搜索DFS,深入探索一个区域,直到无法继续前进,然后回溯
function dfs(grid, row, col) {
  if (row < 0 || col < 0 || row >= grid.length || col >= grid[0].length || grid[row][col] != 1)
    return;
  grid[row][col] = "2"; // 标记为已访问
  dfs(grid, row - 1, col); //上
  dfs(grid, row + 1, col); //下
  dfs(grid, row, col - 1); //左
  dfs(grid, row, col + 1); //右
}

分析

  • 时间复杂度:O(M*N),M 为行数,N 为列数,每个单元格最多被访问一次。
  • 空间复杂度:O(M*N),在最坏情况下,DFS 的递归深度达到 MN,考虑递归栈的开销。

通过这个案例,我们可以看到 DFS 是如何有效地解决特定问题的,尤其是在探索和遍历具有递归结构的图或网格时表现出色。理解 DFS 的机制和应用,对于算法学习和解决实际问题至关重要。