DFS + BFS(深度优先搜索 & 广度优先搜索)

DFS + BFS(深度优先搜索 & 广度优先搜索)

这两种搜索是遍历 的最基础方法。

DFS 像一个走迷宫的人,一条路走到黑,没路了再回头;

BFS 像水波纹,从起点一层一层向外扩散。

何时用 DFS?何时用 BFS?

场景 DFS BFS
遍历所有节点 / 求所有路径 ✅ 回溯方便 也可以,但较麻烦
求最短路径 / 最小步数 ❌ 需要全搜 ✅ 第一次到达即最短
连通性问题(是否可达)
空间消耗 递归栈 O(深度) 队列 O(宽度)
适用结构 树、图、网格 树、图、网格

PDF 安排了 5 道题:

  • DFS:Number of Islands (200)、Path Sum II (113)、Surrounded Regions (130)
  • BFS:Rotting Oranges (994)、Binary Tree Level Order Traversal (102)、Word Ladder (127)(后者作为高阶题)

1. DFS 核心思想与模板

思想

DFS 会沿一个分支尽可能深地探索,直到无法继续才回溯。在代码上通常用递归实现,递归函数内部会有循环或分支选择。回溯可以看作是一种带有状态恢复的 DFS。

回溯框架(也是 DFS 的通用框架)

python 复制代码
result = []
def backtrack(路径, 选择列表):
    if 满足结束条件:
        result.add(路径)
        return
    for 选择 in 选择列表:
        做选择
        backtrack(路径, 选择列表)
        撤销选择

DFS 网格遍历模板(岛屿问题常用)

对于二维网格的 DFS,通常需要遍历每个格子,遇到 '1' 就进行深度搜索,并把遍历过的格子标记为 '0'(或 visited),避免重复。

java 复制代码
void dfs(char[][] grid, int i, int j) {
    // 越界或不是 '1',直接返回
    if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length || grid[i][j] != '1')
        return;
    grid[i][j] = '0';         // 标记为已访问
    dfs(grid, i - 1, j);      // 上
    dfs(grid, i + 1, j);      // 下
    dfs(grid, i, j - 1);      // 左
    dfs(grid, i, j + 1);      // 右
}

2. DFS 例题

例题一:岛屿数量(200. Number of Islands, Medium)

题目:给你一个由 '1'(陆地)和 '0'(水)组成的二维网格,计算岛屿的数量。岛屿总是被水包围,并且只有水平或垂直方向连接。

思路:遍历每个格子,如果遇到未访问的陆地 '1',岛屿数量 +1,然后用 DFS 把与它相连的所有陆地都淹没(标记为 '0')。

代码

java 复制代码
public int numIslands(char[][] grid) {
    if (grid == null || grid.length == 0) return 0;
    int count = 0;
    for (int i = 0; i < grid.length; i++) {
        for (int j = 0; j < grid[0].length; j++) {
            if (grid[i][j] == '1') {
                count++;
                dfs(grid, i, j);
            }
        }
    }
    return count;
}

private void dfs(char[][] grid, int i, int j) {
    if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length || grid[i][j] != '1')
        return;
    grid[i][j] = '0';
    dfs(grid, i - 1, j);
    dfs(grid, i + 1, j);
    dfs(grid, i, j - 1);
    dfs(grid, i, j + 1);
}

时空复杂度:O(M×N),遍历每个格子一次,递归栈最深可能 O(M×N)。


例题二:路径总和 II(113. Path Sum II, Medium)

题目:给定一个二叉树和一个目标和,找到所有从根节点到叶子节点路径总和等于给定目标和的路径。

思路:典型的回溯 + DFS。用一个临时列表记录当前路径,到达叶子时检查总和是否等于 targetSum,若是则加入结果集。回溯时移除末尾节点。

代码

java 复制代码
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
    List<List<Integer>> res = new ArrayList<>();
    dfs(root, targetSum, new ArrayList<>(), res);
    return res;
}

private void dfs(TreeNode node, int remaining, List<Integer> path, List<List<Integer>> res) {
    if (node == null) return;
    path.add(node.val);                              // 做选择
    if (node.left == null && node.right == null && remaining == node.val) {
        res.add(new ArrayList<>(path));             // 找到一个解
    } else {
        dfs(node.left, remaining - node.val, path, res);
        dfs(node.right, remaining - node.val, path, res);
    }
    path.remove(path.size() - 1);                   // 撤销选择(回溯)
}

例题三:被围绕的区域(130. Surrounded Regions, Medium)

题目:给你一个 m x n 的矩阵 board,元素为 'X' 或 'O'。将被 'X' 围绕的 'O' 区域填充为 'X'。任何边界上的 'O' 不会被围绕,且与其相连的 'O' 也不会被围绕。

思路:反向思维。从边界上的 'O' 出发,用 DFS 把所有与边界连通的 'O' 标记为特殊字符(如 '#')。然后遍历整个矩阵,将 'O' 变为 'X'(因为没被标记说明是被围绕的),将 '#' 恢复为 'O'。

代码

java 复制代码
public void solve(char[][] board) {
    if (board == null || board.length == 0) return;
    int m = board.length, n = board[0].length;
    
    // 处理第一列和最后一列
    for (int i = 0; i < m; i++) {
        if (board[i][0] == 'O') dfs(board, i, 0);
        if (board[i][n-1] == 'O') dfs(board, i, n-1);
    }
    // 处理第一行和最后一行(跳过角格子,但没关系)
    for (int j = 0; j < n; j++) {
        if (board[0][j] == 'O') dfs(board, 0, j);
        if (board[m-1][j] == 'O') dfs(board, m-1, j);
    }
    
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (board[i][j] == 'O') board[i][j] = 'X';
            else if (board[i][j] == '#') board[i][j] = 'O';
        }
    }
}

private void dfs(char[][] board, int i, int j) {
    if (i < 0 || i >= board.length || j < 0 || j >= board[0].length || board[i][j] != 'O')
        return;
    board[i][j] = '#';
    dfs(board, i-1, j);
    dfs(board, i+1, j);
    dfs(board, i, j-1);
    dfs(board, i, j+1);
}

3. BFS 核心思想与模板

思想

BFS 使用队列,每次把当前节点的邻接节点加入队尾,保证先访问离起点近的节点。常用来求无权图的最短路径。

层序遍历 / 基础 BFS 模板(树)

java 复制代码
public List<List<Integer>> levelOrder(TreeNode root) {
    List<List<Integer>> res = new ArrayList<>();
    Queue<TreeNode> q = new LinkedList<>();
    if (root != null) q.offer(root);
    while (!q.isEmpty()) {
        int size = q.size();              // 当前层的节点个数
        List<Integer> level = new ArrayList<>();
        for (int i = 0; i < size; i++) {
            TreeNode cur = q.poll();
            level.add(cur.val);
            if (cur.left != null) q.offer(cur.left);
            if (cur.right != null) q.offer(cur.right);
        }
        res.add(level);
    }
    return res;
}

对于图,需要额外使用 visited 数组防止重复访问;有时可以直接修改原数据(如岛屿淹没)来节省空间。


4. BFS 例题

例题一:腐烂的橘子(994. Rotting Oranges, Medium)

题目 :网格中,0 表示空单元格,1 表示新鲜橘子,2 表示腐烂的橘子。每分钟,所有与腐烂橘子相邻(上下左右)的新鲜橘子都会腐烂。返回直到没有新鲜橘子为止所经过的最小分钟数。如果不可能,返回 -1。

思路:多源 BFS。初始时将所有腐烂的橘子入队。然后 BFS 每一层向外扩散,每扩散一层,时间 +1。最终检查是否还有新鲜橘子。

代码

java 复制代码
public int orangesRotting(int[][] grid) {
    int m = grid.length, n = grid[0].length;
    Queue<int[]> q = new LinkedList<>();
    int fresh = 0;
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (grid[i][j] == 2) q.offer(new int[]{i, j});
            else if (grid[i][j] == 1) fresh++;
        }
    }
    if (fresh == 0) return 0;
    
    int[][] dirs = {{-1,0},{1,0},{0,-1},{0,1}};
    int minutes = 0;
    while (!q.isEmpty()) {
        int size = q.size();
        boolean infected = false;
        for (int i = 0; i < size; i++) {
            int[] cur = q.poll();
            for (int[] d : dirs) {
                int x = cur[0] + d[0], y = cur[1] + d[1];
                if (x >= 0 && x < m && y >= 0 && y < n && grid[x][y] == 1) {
                    grid[x][y] = 2;
                    q.offer(new int[]{x, y});
                    fresh--;
                    infected = true;
                }
            }
        }
        if (infected) minutes++;
    }
    return fresh == 0 ? minutes : -1;
}

例题二:二叉树的层序遍历(102. Binary Tree Level Order Traversal, Medium)

前面已经给出完整模板,这是 BFS 在树上的标准应用。不加赘述。


例题三:单词接龙(127. Word Ladder, Hard)

题目 :给定两个单词 beginWordendWord,以及一个单词列表 wordList,每次只能改变一个字母,且每一步得到的单词必须在列表中。求从 beginWordendWord 的最短转换序列的长度。如果不存在,返回 0。

思路 :这是一道经典的"无权图最短路径 "问题。单词为节点,如果两个单词只差一个字母,则它们之间有一条边。用 BFS 从 beginWord 开始,每层代表一步,当遇到 endWord 时,当前层数 +1 就是答案。

代码(标准 BFS):

java 复制代码
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
    Set<String> wordSet = new HashSet<>(wordList);
    if (!wordSet.contains(endWord)) return 0;
    
    Queue<String> q = new LinkedList<>();
    q.offer(beginWord);
    Set<String> visited = new HashSet<>();
    visited.add(beginWord);
    
    int steps = 1;
    while (!q.isEmpty()) {
        int size = q.size();
        for (int i = 0; i < size; i++) {
            String cur = q.poll();
            if (cur.equals(endWord)) return steps;
            for (String neighbor : getNeighbors(cur, wordSet)) {
                if (!visited.contains(neighbor)) {
                    visited.add(neighbor);
                    q.offer(neighbor);
                }
            }
        }
        steps++;
    }
    return 0;
}

// 生成当前单词所有合法邻居(只变一个字母且在 wordSet 中)
private List<String> getNeighbors(String word, Set<String> wordSet) {
    List<String> res = new ArrayList<>();
    char[] chars = word.toCharArray();
    for (int i = 0; i < chars.length; i++) {
        char old = chars[i];
        for (char c = 'a'; c <= 'z'; c++) {
            if (c == old) continue;
            chars[i] = c;
            String newWord = new String(chars);
            if (wordSet.contains(newWord)) res.add(newWord);
        }
        chars[i] = old;
    }
    return res;
}

优化:还可以用双向 BFS 进一步提高效率。


5. BFS / DFS 选择速查

问题类型 推荐方法 原因
岛屿数量 DFS / BFS 均可 遍历所有连通组件
路径总和 II DFS + 回溯 需要记录所有路径
被围绕的区域 DFS / BFS 从边界开始标记连通区域
腐烂的橘子 BFS 求最小分钟数,层数即时间
二叉树的层序遍历 BFS 需要按层输出
单词接龙 BFS 最短转换序列,无权图最短路径

通用口诀

  • 要想找最短、最少步数BFS
  • 要想找所有可能、回溯遍历DFS / 回溯
相关推荐
一行代码一行诗++1 小时前
转义字符和语句
c语言·开发语言·算法
算法鑫探1 小时前
算法与数据结构 以及算法复杂度
c语言·数据结构·算法·新人首发
数据牧羊人的成长笔记2 小时前
SVM与朴素贝叶斯算法+Kaggle竞赛+智能推荐系统+关联规则分析与Apriori算法+Gensim与LDA主题模型
算法·机器学习·支持向量机
拳里剑气2 小时前
C++算法:前缀和
开发语言·c++·算法·前缀和
隔壁大炮2 小时前
Day07-词嵌入层解释
人工智能·深度学习·算法·计算机视觉·cnn
啊我不会诶2 小时前
Codeforces Round 1091 (Div. 2) and CodeCraft 26
c++·算法
凌波粒2 小时前
LeetCode--二叉树前中后序遍历的递归与迭代实现(二叉树/DFS)
算法·leetcode·深度优先
啊哦呃咦唔鱼2 小时前
Leetcodehot100-215. 数组中的第K个最大元素
数据结构·算法·leetcode
老赵聊算法、大模型备案2 小时前
从剪映、即梦 AI 被罚,读懂 AI 生成内容标识硬性合规要求
人工智能·算法·安全·aigc