• BFS(广度优先搜索):层序遍历和最短路径


     

     

    作者:fuxuemingzhu
    链接:https://leetcode-cn.com/problems/binary-tree-level-order-traversal/solution/tao-mo-ban-bfs-he-dfs-du-ke-yi-jie-jue-by-fuxuemin/
    来源:力扣(LeetCode)
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    DFS(深度优先搜索)和 BFS(广度优先搜索)就像孪生兄弟,提到一个总是想起另一个。然而在实际使用中,我们用 DFS 的时候远远多于 BFS。那么,是不是 BFS 就没有什么用呢?

    如果我们使用 DFS/BFS 只是为了遍历一棵树、一张图上的所有结点的话,那么 DFS 和 BFS 的能力没什么差别,我们当然更倾向于更方便写、空间复杂度更低的 DFS 遍历。不过,某些使用场景是 DFS 做不到的,只能使用 BFS 遍历。这就是本文要介绍的两个场景:「层序遍历」、「最短路径」。

    • DFS 与 BFS 的特点比较
    • BFS 的适用场景
    • 如何用 BFS 进行层序遍历
    • 如何用 BFS 求解最短路径问题

    DFS 与 BFS

    让我们先看看在二叉树上进行 DFS 遍历和 BFS 遍历的代码比较。

    DFS 遍历使用递归

     BFS 遍历使用队列数据结构

    比较两段代码的话,最直观的感受就是:DFS 遍历的代码比 BFS 简洁太多了!这是因为递归的方式隐含地使用了系统的 栈,我们不需要自己维护一个数据结构。如果只是简单地将二叉树遍历一遍,那么 DFS 显然是更方便的选择

    虽然 DFS 与 BFS 都是将二叉树的所有结点遍历了一遍,但它们遍历结点的顺序不同。

    这个遍历顺序也是 BFS 能够用来解「层序遍历」、「最短路径」问题的根本原因

    BFS 的应用一:层序遍历
    BFS 的层序遍历应用就是本题了:

    LeetCode 102. Binary Tree Level Order Traversal 二叉树的层序遍历(Medium)

    给定一个二叉树,返回其按层序遍历得到的节点值。 层序遍历即逐层地、从左到右访问所有结点。

    什么是层序遍历呢?简单来说,层序遍历就是把二叉树分层,然后每一层从左到右遍历:

    乍一看来,这个遍历顺序和 BFS 是一样的,我们可以直接用 BFS 得出层序遍历结果。然而,层序遍历要求的输入结果和 BFS 是不同的。层序遍历要求我们区分每一层,也就是返回一个二维数组。而 BFS 的遍历结果是一个一维数组,无法区分每一层。

    那么,怎么给 BFS 遍历的结果分层呢?我们首先来观察一下 BFS 遍历的过程中,结点进队列和出队列的过程:

     

    可以看到,此时队列中的结点是 3、4、5,分别来自第 1 层和第 2 层。这个时候,第 1 层的结点还没出完,第 2 层的结点就进来了,而且两层的结点在队列中紧挨在一起,我们无法区分队列中的结点来自哪一层。

    因此,我们需要稍微修改一下代码,在每一层遍历开始前,先记录队列中的结点数量 nn(也就是这一层的结点数量),然后一口气处理完这一层的 nn 个结点。

     1 // 二叉树的层序遍历
     2 void bfs(TreeNode root) {
     3 Queue<TreeNode> queue = new ArrayDeque<>();
     4 queue.add(root);
     5 while (!queue.isEmpty()) {
     6 int n = queue.size();
     7 for (int i = 0; i < n; i++) { 
     8 // 变量 i 无实际意义,只是为了循环 n 次
     9   TreeNode node = queue.poll();
    10   if (node.left != null) {
    11     queue.add(node.left);
    12     }
    13   if (node.right != null) {
    14     queue.add(node.right);
    15     }
    16   }
    17   }
    18 }

    可以看到,在 while 循环的每一轮中,都是将当前层的所有结点出队列,再将下一层的所有结点入队列,这样就实现了层序遍历。

    最终我们得到的题解代码为:

    Java

    public List<List<Integer>> levelOrder(TreeNode root) {
    List<List<Integer>> res = new ArrayList<>();

    Queue<TreeNode> queue = new ArrayDeque<>();
    if (root != null) {
    queue.add(root);
    }
    while (!queue.isEmpty()) {
    int n = queue.size();
    List<Integer> level = new ArrayList<>();
    for (int i = 0; i < n; i++) {
    TreeNode node = queue.poll();
    level.add(node.val);
    if (node.left != null) {
    queue.add(node.left);
    }
    if (node.right != null) {
    queue.add(node.right);
    }
    }
    res.add(level);
    }

    return res;
    }

    BFS 的应用二:最短路径
    在一棵树中,一个结点到另一个结点的路径是唯一的但在图中,结点之间可能有多条路径,其中哪条路最近呢?这一类问题称为最短路径问题。最短路径问题也是 BFS 的典型应用,而且其方法与层序遍历关系密切。

    在二叉树中,BFS 可以实现一层一层的遍历。在图中同样如此。从源点出发,BFS 首先遍历到第一层结点,到源点的距离为 1,然后遍历到第二层结点,到源点的距离为 2…… 可以看到,用 BFS 的话,距离源点更近的点会先被遍历到,这样就能找到到某个点的最短路径了。

     

     

    在上一篇文章中,我们介绍了网格结构的基本概念,以及网格结构中的 DFS 遍历。其中一些概念和技巧也可以用在 BFS 遍历中:

    • 格子 (r, c) 的相邻四个格子为:(r-1, c)、(r+1, c)、(r, c-1) 和 (r, c+1);
    • 使用函数 inArea 判断当前格子的坐标是否在网格范围内;
    • 将遍历过的格子标记为 2,避免重复遍历。

    对于网格结构的性质、网格结构的 DFS 遍历技巧不是很了解的同学,可以复习一下上一篇文章:LeetCode 例题精讲 | 12 岛屿问题:网格结构中的 DFS。

    上一篇文章讲过了网格结构 DFS 遍历,这篇文章正好讲解一下网格结构的 BFS 遍历。要解最短路径问题,我们首先要写出层序遍历的代码,仿照上面的二叉树层序遍历代码,类似地可以写出网格层序遍历:

     1 // 网格结构的层序遍历
     2 // 从格子 (i, j) 开始遍历
     3 void bfs(int[][] grid, int i, int j) {
     4     Queue<int[]> queue = new ArrayDeque<>();
     5     queue.add(new int[]{r, c});
     6     while (!queue.isEmpty()) {
     7         int n = queue.size();
     8         for (int i = 0; i < n; i++) { 
     9             int[] node = queue.poll();
    10             int r = node[0];
    11             int c = node[1];
    12             if (r-1 >= 0 && grid[r-1][c] == 0) {
    13                 grid[r-1][c] = 2;
    14                 queue.add(new int[]{r-1, c});
    15             }
    16             if (r+1 < N && grid[r+1][c] == 0) {
    17                 grid[r+1][c] = 2;
    18                 queue.add(new int[]{r+1, c});
    19             }
    20             if (c-1 >= 0 && grid[r][c-1] == 0) {
    21                 grid[r][c-1] = 2;
    22                 queue.add(new int[]{r, c-1});
    23             }
    24             if (c+1 < N && grid[r][c+1] == 0) {
    25                 grid[r][c+1] = 2;
    26                 queue.add(new int[]{r, c+1});
    27             }
    28         }
    29     }
    30 }

    以上的层序遍历代码有几个注意点:

    • 队列中的元素类型是 int[] 数组,每个数组的长度为 2,包含格子的行坐标和列坐标。
    • 为了避免重复遍历,这里使用到了和 DFS 遍历一样的技巧:把已遍历的格子标记为 2。注意:我们在将格子放入队列之前就将其标记为 2。想一想,这是为什么?
    • 在将格子放入队列之前就检查其坐标是否在网格范围内,避免将「不存在」的格子放入队列。

    这段网格遍历代码还有一些可以优化的地方。由于一个格子有四个相邻的格子,代码中判断了四遍格子坐标的合法性,代码稍微有点啰嗦。我们可以用一个 moves 数组存储相邻格子的四个方向:

    写好了层序遍历的代码,接下来我们看看如何来解决本题中的最短路径问题。

    这道题要找的是距离陆地最远的海洋格子。假设网格中只有一个陆地格子,我们可以从这个陆地格子出发做层序遍历,直到所有格子都遍历完。最终遍历了几层,海洋格子的最远距离就是几。

    那么有多个陆地格子的时候怎么办呢?一种方法是将每个陆地格子都作为起点做一次层序遍历,但是这样的时间开销太大。

    BFS 完全可以以多个格子同时作为起点。我们可以把所有的陆地格子同时放入初始队列,然后开始层序遍历,这样遍历的效果如下图所示:

    这种遍历方法实际上叫做「多源 BFS」。多源 BFS 的定义不是今天讨论的重点,你只需要记住多源 BFS 很方便,只需要把多个源点同时放入初始队列即可。

    需要注意的是,虽然上面的图示用 1、2、3、4 表示层序遍历的层数,但是在代码中,我们不需要给每个遍历到的格子标记层数,只需要用一个 distance 变量记录当前的遍历的层数(也就是到陆地格子的距离)即可。

    最终,我们得到的题解代码为:

     1 int[][] moves = {
     2         {-1, 0}, {1, 0}, {0, -1}, {0, 1},
     3     };
     4 
     5     int distance = -1; // 记录当前遍历的层数(距离)
     6     while (!queue.isEmpty()) {
     7         distance++;
     8         int n = queue.size();
     9         for (int i = 0; i < n; i++) { 
    10             int[] node = queue.poll();
    11             int r = node[0];
    12             int c = node[1];
    13             for (int[] move : moves) {
    14                 int r2 = r + move[0];
    15                 int c2 = c + move[1];
    16                 if (inArea(grid, r2, c2) && grid[r2][c2] == 0) {
    17                     grid[r2][c2] = 2;
    18                     queue.add(new int[]{r2, c2});
    19                 }
    20             }
    21         }
    22     }
    23 
    24     return distance;
    25 }
    26 
    27 // 判断坐标 (r, c) 是否在网格中
    28 boolean inArea(int[][] grid, int r, int c) {
    29     return 0 <= r && r < grid.length 
    30         && 0 <= c && c < grid[0].length;
    31 }

    总结
    可以看到,「BFS 遍历」、「层序遍历」、「最短路径」实际上是递进的关系。在 BFS 遍历的基础上区分遍历的每一层,就得到了层序遍历。在层序遍历的基础上记录层数,就得到了最短路径。

    作者:nettee
    链接:https://leetcode-cn.com/problems/binary-tree-level-order-traversal/solution/bfs-de-shi-yong-chang-jing-zong-jie-ceng-xu-bian-l/
    来源:力扣(LeetCode)
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 相关阅读:
    my sql Group_concat函数
    [Effective JavaScript 笔记]第43条:使用Object的直接实例构造轻量级的字典
    [Effective JavaScript 笔记]第4章:对象和原型--个人总结
    [Effective JavaScript 笔记]第42条:避免使用轻率的猴子补丁
    [Effective JavaScript 笔记]第41条:将原型视为实现细节
    [Effective JavaScript 笔记]第40条:避免继承标准类
    [Effective JavaScript 笔记]第39条:不要重用父类的属性名
    [Effective JavaScript 笔记]第38条:在子类的构造函数中调用父类的构造函数
    [Effective JavaScript 笔记]第37条:认识到this变量的隐式绑定问题
    [Effective JavaScript 笔记]第36条:只将实例状态存储在实例对象中
  • 原文地址:https://www.cnblogs.com/sbb-first-blog/p/13259728.html
Copyright © 2020-2023  润新知