欢迎光临
我们一直在努力

第18篇-DFS深度优先搜索-从二维网格到路径搜索

概述

学完回溯之后,我们继续看另一个非常重要的搜索思想:DFS,深度优先搜索。

很多初学者会把 DFS 和回溯混在一起。
它们确实很像,因为回溯本身就常常是 DFS 的一种应用。
但 DFS 的关注点更广,通常是:

沿着一条路尽可能往深处走,走不通再回退,换另一条路

DFS 最常见的使用场景包括:

  • 二维网格搜索
  • 连通块统计
  • 岛屿问题
  • 路径是否存在
  • 树的遍历
  • 图的遍历

这类题的共同点是:需要从一个起点出发,把能到达的地方全部走一遍。

如果你已经学过递归和回溯,那么 DFS 会非常自然。
它通常可以直接用递归写,也可以用显式栈写。

这篇文章会围绕 DFS 的基础形态展开:

  • DFS 是什么
  • DFS 的基本写法
  • 二维网格中的 DFS
  • 连通块和岛屿问题
  • 路径搜索问题
  • DFS 的常见坑点
  • 学完这篇,你应该能把 DFS 视为一种标准搜索手段,并能写出网格遍历、连通块统计和路径搜索的基础代码。

    核心概念:DFS 到底是什么

    DFS 的全称是 Depth First Search,中文叫深度优先搜索。

    它的核心思想是:

    先沿着一条路径一直走到尽头,再回头尝试其他路径

    这和广度优先搜索 BFS 正好相对。
    BFS 是一层一层扩展,DFS 是一条路走到底。

    DFS 的典型过程

    假设从点 A 出发,周围可以走到 B、C、D:

    A -> B -> E -> F
    -> C -> …
    -> D -> …

    DFS 会优先沿着 A -> B -> E -> F 这条路走深。
    走不下去之后,再回到 A,尝试 C、D。

    DFS 和递归的关系

    DFS 很适合递归写,因为递归本身就天然适合“先深入,再返回”的过程。

    一个最基础的 DFS 结构是:

    void dfs(当前节点) {
    if (终止条件) {
    return;
    }

    标记当前节点;

    for (下一个节点 : 所有可行邻居) {
    dfs(下一个节点);
    }
    }

    这个结构和回溯非常接近。
    区别在于,DFS 更强调“遍历图或网格”,回溯更强调“枚举方案并恢复现场”。

    DFS 就是沿着一条路径尽可能向深处搜索,直到不能继续再回退。

    DFS 基本模板:先记住这几行

    DFS 题目看起来很多,但基础模板通常是固定的。

    网格 DFS 模板

    void dfs(int x, int y) {
    if (越界 || 已访问 || 不满足条件) {
    return;
    }

    标记访问;

    for (四个方向) {
    dfs(nx, ny);
    }
    }

    图 DFS 模板

    void dfs(int node) {
    if (已访问) {
    return;
    }

    标记访问;

    for (int next : graph[node]) {
    dfs(next);
    }
    }

    树 DFS 模板

    void dfs(TreeNode root) {
    if (root == null) {
    return;
    }

    dfs(root.left);
    dfs(root.right);
    }

    DFS 的核心不是代码形式,而是思路:

    • 从当前点出发
    • 判断是否可以继续
    • 标记当前状态
    • 递归访问下一个位置

    DFS 的代码一般都长得很像,关键是判断条件、访问标记和下一步搜索方向。

    例题一:二维网格中的 DFS 遍历

    二维网格题是 DFS 的经典入门场景。

    题目可以抽象成:

    给定一个二维网格,从某个起点出发,访问所有满足条件的格子。

    例如,网格中有 0 和 1:

    1 1 0 0
    1 0 0 1
    0 0 1 1

    如果我们要从某个 1 出发,把相连的 1 都找到,就需要 DFS。

    四个方向的定义

    二维网格通常有四个方向:

    上、下、左、右

    可以用两个数组表示方向偏移:

    int[] dx = {1, 1, 0, 0};
    int[] dy = {0, 0, 1, 1};

    这样就能统一枚举四个邻居。

    Java 代码实现

    class Solution {
    private int m;
    private int n;
    private int[] dx = {1, 1, 0, 0};
    private int[] dy = {0, 0, 1, 1};

    public void dfs(char[][] grid, int x, int y, boolean[][] visited) {
    if (x < 0 || x >= m || y < 0 || y >= n) {
    return;
    }
    if (visited[x][y] || grid[x][y] == '0') {
    return;
    }

    visited[x][y] = true;

    for (int k = 0; k < 4; k++) {
    int nx = x + dx[k];
    int ny = y + dy[k];
    dfs(grid, nx, ny, visited);
    }
    }
    }

    为什么要先判断越界和访问状态

    网格搜索里最常见的错误就是越界。

    如果没有先判断:

    if (x < 0 || x >= m || y < 0 || y >= n)

    那么后续访问 grid[x][y] 时就会报错。

    如果不判断 visited[x][y],DFS 可能会在相邻格子之间来回走,形成死循环。

    所以判断顺序通常是:

  • 越界
  • 是否访问过
  • 是否满足题目条件
  • 二维网格 DFS 的关键是方向枚举、越界判断和访问标记。

    例题二:岛屿数量

    题目:

    给定一个由 1 和 0 组成的二维网格,1 表示陆地,0 表示海水,求岛屿的数量。

    岛屿定义为上下左右相连的陆地块。

    解题思路

    这道题本质上是:
    统计网格中有多少个连通块。

    做法是:

  • 遍历整个网格
  • 如果遇到一个未访问的 1
  • 岛屿数量加一
  • 从这个点开始 DFS,把整个岛屿都标记掉
  • 这样每个岛屿只会统计一次。

    Java 代码实现

    class Solution {
    private int m;
    private int n;
    private int[] dx = {1, 1, 0, 0};
    private int[] dy = {0, 0, 1, 1};

    public int numIslands(char[][] grid) {
    if (grid == null || grid.length == 0) {
    return 0;
    }

    m = grid.length;
    n = grid[0].length;
    boolean[][] visited = new boolean[m][n];
    int count = 0;

    for (int i = 0; i < m; i++) {
    for (int j = 0; j < n; j++) {
    if (!visited[i][j] && grid[i][j] == '1') {
    count++;
    dfs(grid, i, j, visited);
    }
    }
    }

    return count;
    }

    private void dfs(char[][] grid, int x, int y, boolean[][] visited) {
    if (x < 0 || x >= m || y < 0 || y >= n) {
    return;
    }
    if (visited[x][y] || grid[x][y] == '0') {
    return;
    }

    visited[x][y] = true;

    for (int k = 0; k < 4; k++) {
    dfs(grid, x + dx[k], y + dy[k], visited);
    }
    }
    }

    为什么 DFS 后岛屿只会统计一次

    假设一个岛屿里有多个相连的 1:

    1 1
    1 0

    当我们从左上角开始 DFS 时,会把整个连通块里的所有 1 都标记为已访问。
    之后再遍历到同一个岛屿的其他格子时,它们已经被访问过,所以不会重复计数。

    复杂度分析

    • 时间复杂度:O(m * n),每个格子最多访问一次
    • 空间复杂度:O(m * n),visited 数组和递归栈

    遇到一个新的陆地就开始 DFS,把整个连通块淹没掉,然后岛屿数加一。

    例题三:岛屿面积

    题目:

    计算最大岛屿的面积,即连通陆地格子的最大数量。

    解题思路

    这和岛屿数量很像,只不过不是统计岛屿个数,而是统计当前连通块的大小。

    DFS 的返回值可以表示:

    从当前位置出发,当前连通块的面积

    Java 代码实现

    class Solution {
    private int m;
    private int n;
    private int[] dx = {1, 1, 0, 0};
    private int[] dy = {0, 0, 1, 1};

    public int maxAreaOfIsland(int[][] grid) {
    m = grid.length;
    n = grid[0].length;
    boolean[][] visited = new boolean[m][n];
    int maxArea = 0;

    for (int i = 0; i < m; i++) {
    for (int j = 0; j < n; j++) {
    if (!visited[i][j] && grid[i][j] == 1) {
    maxArea = Math.max(maxArea, dfs(grid, i, j, visited));
    }
    }
    }

    return maxArea;
    }

    private int dfs(int[][] grid, int x, int y, boolean[][] visited) {
    if (x < 0 || x >= m || y < 0 || y >= n) {
    return 0;
    }
    if (visited[x][y] || grid[x][y] == 0) {
    return 0;
    }

    visited[x][y] = true;
    int area = 1;

    for (int k = 0; k < 4; k++) {
    area += dfs(grid, x + dx[k], y + dy[k], visited);
    }

    return area;
    }
    }

    为什么这里用返回值累加

    因为面积本身就是一个可以递归合并的量。

    当前格子面积为 1,四个方向的连通面积再累加上来,就是整个岛屿面积。

    复杂度分析

    • 时间复杂度:O(m * n)
    • 空间复杂度:O(m * n)

    DFS 不仅可以“标记走过”,也可以“返回一片连通区域的统计值”。

    例题四:路径是否存在

    题目:

    从起点出发,判断是否能走到终点。

    这类题常见于迷宫、网格路径、图可达性判断。

    解题思路

    DFS 的返回值可以直接表示:

    从当前点出发,是否能到达目标点

    如果某一条路径能到达目标,就直接返回 true。
    否则继续搜索其他方向。

    Java 代码实现

    class Solution {
    private int m;
    private int n;
    private int[] dx = {1, 1, 0, 0};
    private int[] dy = {0, 0, 1, 1};

    public boolean hasPath(int[][] maze, int sx, int sy, int tx, int ty) {
    m = maze.length;
    n = maze[0].length;
    boolean[][] visited = new boolean[m][n];
    return dfs(maze, sx, sy, tx, ty, visited);
    }

    private boolean dfs(int[][] maze, int x, int y, int tx, int ty, boolean[][] visited) {
    if (x == tx && y == ty) {
    return true;
    }
    if (x < 0 || x >= m || y < 0 || y >= n) {
    return false;
    }
    if (visited[x][y] || maze[x][y] == 1) {
    return false;
    }

    visited[x][y] = true;

    for (int k = 0; k < 4; k++) {
    if (dfs(maze, x + dx[k], y + dy[k], tx, ty, visited)) {
    return true;
    }
    }

    return false;
    }
    }

    为什么找到答案可以直接返回

    因为这类题只关心:

    有没有一条路能到终点

    只要某个方向成功了,就没有必要继续搜索其他方向。

    这也是 DFS 在“可达性判断”问题中的典型用法。

    复杂度分析

    • 时间复杂度:最坏 O(m * n)
    • 空间复杂度:O(m * n)

    如果题目只问“能不能到”,DFS 找到目标后可以立即返回。

    DFS 和 BFS:该怎么区分

    DFS 和 BFS 都是搜索算法,但适合场景不同。

    对比项DFSBFS
    搜索方式 一路深入 一层一层扩展
    常见实现 递归或栈 队列
    适合题型 连通块、路径枚举、树遍历 最短步数、层次遍历
    结果特点 不保证最短 通常适合最短路的无权图场景

    怎么选

    如果题目强调:

    • 遍历所有连通区域
    • 找一种路径是否存在
    • 树的递归遍历
    • 枚举所有可能路径

    通常先考虑 DFS。

    如果题目强调:

    • 最短步数
    • 最少操作次数
    • 按层推进

    通常先考虑 BFS。

    DFS 适合深入搜索和连通性判断,BFS 适合最短步数和层次扩展。

    常见坑点:DFS 最容易错在哪里

    1. 忘记标记访问

    如果不写:

    visited[x][y] = true;

    那么 DFS 可能在相邻格子之间反复横跳,陷入死循环。

    2. 标记访问太晚

    有些题中应该在进入节点后立刻标记。
    如果先递归再标记,可能会重复进入同一个节点。

    正确顺序通常是:

  • 判断合法性
  • 标记访问
  • 继续搜索邻居
  • 3. 越界判断写错

    网格题里最常见的错误就是下标越界。

    一定要先判断:

    if (x < 0 || x >= m || y < 0 || y >= n)

    再访问数组内容。

    4. 四个方向写漏

    二维网格通常要处理四个方向:

    上、下、左、右

    如果少写一个方向,搜索结果就不完整。

    5. 递归太深导致栈溢出

    在极端数据下,DFS 递归深度可能非常大。
    如果题目规模很大,可能需要改成显式栈的迭代写法。

    不过在大多数算法题中,递归 DFS 仍然是最常见、最清晰的写法。

    DFS 迭代写法:用栈模拟递归

    虽然递归 DFS 最常见,但也可以用栈来模拟。

    import java.util.Stack;

    class Solution {
    public void dfsIterative(int[][] grid, int sx, int sy) {
    int m = grid.length;
    int n = grid[0].length;
    int[] dx = {1, 1, 0, 0};
    int[] dy = {0, 0, 1, 1};
    boolean[][] visited = new boolean[m][n];
    Stack<int[]> stack = new Stack<>();

    stack.push(new int[]{sx, sy});

    while (!stack.isEmpty()) {
    int[] cur = stack.pop();
    int x = cur[0];
    int y = cur[1];

    if (x < 0 || x >= m || y < 0 || y >= n) {
    continue;
    }
    if (visited[x][y] || grid[x][y] == 0) {
    continue;
    }

    visited[x][y] = true;

    for (int k = 0; k < 4; k++) {
    stack.push(new int[]{x + dx[k], y + dy[k]});
    }
    }
    }
    }

    这种写法和递归 DFS 的逻辑相同,只是把系统调用栈换成了手动维护的栈。

    模板总结:DFS 题怎么写更稳

    可以按这个顺序写 DFS:

  • 明确当前状态是什么
  • 明确什么时候停止搜索
  • 明确怎么标记访问
  • 明确下一步要搜索哪些方向
  • 明确是否需要返回值
  • 一个通用模板是:

    void dfs(状态) {
    if (不合法 || 已访问) {
    return;
    }

    标记当前状态;

    for (下一个状态) {
    dfs(下一个状态);
    }
    }

    如果题目需要统计值,可以让 DFS 返回一个整数。
    如果题目需要判断可达性,可以让 DFS 返回布尔值。

    总结

    DFS 是搜索题里非常基础的一种思路。
    它的核心不是复杂代码,而是清晰地沿着一条路往深处走。

    你可以重点记住下面几句话:

    • DFS 适合连通块、路径、树和图的遍历
    • DFS 通常用递归实现,也可以用栈模拟
    • 网格 DFS 记住越界、访问标记和四个方向
    • 连通块问题本质是“遇到一个新点就把整片区域标记掉”
    • 路径存在性问题可以找到答案就立即返回
    • DFS 和 BFS 的区别在于搜索策略不同
    • 递归太深时要注意栈溢出风险

    如果你已经掌握了递归和回溯,那么 DFS 基本就只是把“搜索对象”从排列组合换成网格、树和图。

    赞(0)
    未经允许不得转载:171主机测评 » 第18篇-DFS深度优先搜索-从二维网格到路径搜索
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址