从三道leetcode掌握广度优先搜索(BFS)

简介: 前言BFS和DFS是如影随形的两种搜索方式,我们在上篇文章从三道leetcode掌握深度优先搜索(DFS)学习了递归的概念及DFS。不熟悉递归及DFS的同学可以先看看上篇文章,再阅读本篇比较好。

前言

BFSDFS是如影随形的两种搜索方式,我们在上篇文章从三道leetcode掌握深度优先搜索(DFS)学习了递归的概念及DFS。不熟悉递归及DFS的同学可以先看看上篇文章,再阅读本篇比较好。

BFS

BFSDFS的区别在于

  • DFS一条路走到黑,先搜索到最深层再返回上层进行搜索。
  • BFS则是层层递进,先搜索当前所有数据再进行下一层搜索。77.png

  • 以上图为例,我们来看看使用BFS的遍历顺序吧

  1. 从第一层开始搜索到单节点A

  2. 第二层搜索得到路径B -> C -> D

  3. 第三层搜索得到路径E -> F -> G -> H
  4. 同理继续下层搜索,最好完整路径为 A -> B -> C -> D -> E -> F -> G -> H -> I -> J -> K

1.二叉树的层序遍历


我们先来个简单的二叉树遍历

leetcode-102


给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。

78.png


/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[][]}
 */
var levelOrder = function(root) {
  if (!root) return []
  // 首先定义答案遍历
  const ans = []
  // 我们的BFS函数
  // 可以发现BFS和DFS的变量不同
  // BFS参数为数组,因为我们是整层整层搜索
  const BFS = (nodes) => {
    // BFS出口
    if (!nodes.length) return
    // 我们需要通过额外数组保存下一层的数据传给下一层搜索
    const next = []
    const layser = []
    // 对当前层进行迭代搜索
    for (let i = 0, len = nodes.length; i < len; i++) {
      const node = nodes[i]
      layser.push(node.val)
      // BFS的重点就是在于不会直接递归子节点数据
      // 而是通过数组的方式来保存下层数据
      // 在此层只处理此层数据达到层次分明
      if(node.left) {
        next.push(node.left)
      }
      if (node.right) {
        next.push(node.right)
      }
    }
    ans.push(layser)
    // 递归下层数据
    BFS(next)
  }
  // 数组形式的参数
  BFS([root])
  return ans
};
复制代码


2.岛屿的最大面积


我们在上篇文章通过DFS解答了此题,现在我们在通过BFS来解答此题,大家可以对比下两种方式的实现区别。


leetcode-695


给你一个大小为 m x n 的二进制矩阵 grid 。


岛屿是由一些相邻的1(代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在 水平或者竖直的四个方向上 相邻。你可以假设grid 的四个边缘都被 0(代表水)包围着。


岛屿的面积是岛上值为 1 的单元格的数目。


计算并返回 grid 中最大的岛屿面积。如果没有岛屿,则返回面积为 0 。

79.png


var maxAreaOfIsland = function(grid) {
  // m/n用于对比边界
  const m = grid.length
  const n = grid[0].length
  // 定义输出变量
  let ans = 0
  // BFS函数
  // 参数同样为数组
  const BFS = (grids) => {
    // BFS出口
    if (!grids.length) return 0
    const nextGrids = []
    let count = 0
    // 处理当前层
    for (let i = 0, len = grids.length; i < len; i++) {
      const [row, col] = grids[i]
      // 剪枝
      if (grid[row][col] === 1) {
        // 将数据累加到当前层
        count++
        grid[row][col] = 0
        // 将下层数据存储待下层函数使用
        if (row > 0) {
          nextGrids.push([row - 1, col])
        }
        if (row + 1 < m) {
          nextGrids.push([row + 1, col])
        }
        if (col > 0) {
          nextGrids.push([row, col - 1])
        }
        if (col + 1 < n) {
          nextGrids.push([row, col + 1])
        }
      }
    }
    // 返回累计值
    return count + BFS(nextGrids)
  }
  // 二维数组没有根节点
  // 所以得遍历每个格子以此来进行上下左右的BFS搜索
  for (let i = 0; i < m; i++) {
    for (let j = 0; j < n; j++) {
      if (grid[i][j] === 1) {
        // 比较更新答案
        ans = Math.max(BFS([[i, j]]), ans)
      }
    }
  }
  return ans
};
复制代码

3. 01矩阵

我们再来看一个专为BFS定制的题

leetcode-542

给定一个由 0 和 1 组成的矩阵 mat,请输出一个大小相同的矩阵,其中每一个格子是 mat 中对应位置元素到最近的 0 的距离。

两个相邻元素间的距离为 1。


80.png

本题初看是个很常规的递归搜索题型,可以使用BFSDFS来搜索节点到最近0的距离。如果使用DFS来搜索的话,我们需要递归寻找直到搜索到0,但是此时的距离并不是简单的子节点(上下左右)距离+1,而是得通过横向对比来取最小值+1,而为了避免重复寻找,我们又需要去做去重处理,所以最后算法会变得非常复杂。


而使用BFS则很适合求解此类题型,我们称之为多源最短路径


我们本要求解10的距离,我们反向思考,通过0来更新周围1的距离。通过同步更新0周围的1距离,比如最先更新0相邻的1,第二步更新与1相邻的1,这样通过层层外溢的搜索就可以确定最短距离了,而且是直接可以确定,不需要对比和更新,我们只需要避免重复更新就行。

var updateMatrix = function(mat) {
  const ans = []
  // 边界m/n
  const m = mat.length
  const n = mat[0].length
  // 多源最短路径的源
  const queue = []
  // 上下左右方向
  const dirs = [
    [1, 0],
    [-1, 0],
    [0, 1],
    [0, -1]
  ]
  // BFS函数
  const BFS = (grids) => {
    // BFS出口
    if (!grids.length) return
    // 下一层数据
    const next = []
    // 本层数据搜索
    for (let i = 0, gridsLen = grids.length; i < gridsLen; i++) {
      const [row, col] = grids[i]
      // 上下左右方向寻找下层数据
      for (let i = 0, len = dirs.length; i < len; i++) {
        const dir = dirs[i]
        const x = row + dir[0]
        const y = col + dir[1]
        if (x >= 0 && y >= 0 && x < m && y < n && ans[x][y] === undefined) {
          ans[x][y] = ans[row][col] + 1
          next.push([x, y])
        }
      }
    }
    // 递归处理下层数据
    BFS(next)
  }
  for (let i = 0; i < m; i++) {
    ans[i] = []
    for (let j = 0; j < n; j++) {
      if (mat[i][j] === 0) {
        queue.push([i, j])
        // 我们仅将源(0)加入ans
        // 这样未更新的就为undefined
        // 我们以此来去重
        ans[i][j] = 0
      }
    }
  }
  BFS(queue)
  return ans
};
复制代码

BFS的非递归实现


前面我们的BFS函数都是通过递归调用自身来实现的,我们在处理完本层数据之后,再调用自身处理下层数据,直到遇到出口为止。相对于DFS来说,递归调用是非常明确的,总是从n -> n +1,所以我们也可以通过队列的方式来实现。


其大致实现原理如下:


我们设置全局队列 queue,然后往队列中加入第一层数据

const quque = []
queue.push(1)
复制代码


然后再不断地遍历 queue,将得到的下层数据再加入队列

queue.push(2) // [1, 2]
queue.push(2) // [1, 2, 2]
复制代码


可以发现,下一层的数据总是在本层后面,以此达到层次遍历。


我们以上面的第三题为例

var updateMatrix = function(mat) {
  const ans = []
  const m = mat.length
  const n = mat[0].length
  const queue = []
  const dirs = [
      [1, 0],
      [-1, 0],
      [0, 1],
      [0, -1]
  ]
  for (let i = 0; i < m; i++) {
    ans[i] = []
    for (let j = 0; j < n; j++) {
      if (mat[i][j] === 0) {
        queue.push([i, j])
        ans[i][j] = 0
      }
    }
  }
  // 我们通过全局队列来保存搜索节点
  // 通过不断地前进搜索来实现BFS
  while(queue.length) {
    // 通过队列操作弹出第一个数据
    const grid = queue.shift()
    const [row, col] = grid
    for (let i = 0, len = dirs.length; i < len; i++) {
      const dir = dirs[i]
      const x = row + dir[0]
      const y = col + dir[1]
      if (x >= 0 && y >= 0 && x < m && y < n && ans[x][y] === undefined) {
        ans[x][y] = ans[row][col] + 1
        // 我们下层数据加入队列
        queue.push([x, y])
      }
    }
  }
  return ans
};
复制代码

我们可以仔细对比其中两种实现方式的差异,以后使用哪种方式都可以,全凭个人喜好。


BFS模板


通过上面的练习,我们来总结下BFS模板


  1. 递归模式
const BFS = (list) => {
  // 出口
  if (!list.length) return
  // 下层数据数组
  const next = []
  for (let i = 0; i < list.length; i++) {
    // 处理当前节点
    // 收集下层数据
    next.push(node.child)
  }
  // 递归调用下层数据
  BFS(next)
}
复制代码

  1. 队列模式
const fn = () => {
  // 设置全局队列
  const quque = []
  // 加入第一层节点
  queue.push(...)
  // 遍历数据
  while(queue.length) {
    // 不断弹出首节点来达到清空队列得到出口
    const node = quque.shift()
    // 处理当前节点
    // 添加下层节点
    queue.push(node.child)
  }
}
复制代码


总结


我们来总结下本篇文章的知识点


  1. BFSDFS是两种常见的搜索方式,一种深度优先不撞南墙不回头,另一种广度优先层层递进

  2. BFS有两种实现方式,队列方式及递归方式

  3. BFS的一样需要设置函数出口防止无限循环或无限递归

  4. BFSDFS更加适用于多源最短路径的题型

  5. BFS的常见模板



相关文章
|
6月前
|
算法 测试技术 C++
【动态规划】【广度优先搜索】LeetCode:2617 网格图中最少访问的格子数
【动态规划】【广度优先搜索】LeetCode:2617 网格图中最少访问的格子数
|
5月前
|
存储 算法 数据可视化
LeetCode 133题详解:广度优先搜索和深度优先搜索实现克隆图
LeetCode 133题详解:广度优先搜索和深度优先搜索实现克隆图
|
5月前
|
算法
【经典LeetCode算法题目专栏分类】【第4期】BFS广度优先算法:单词接龙、最小基因变化、二进制矩阵中的最短路径
【经典LeetCode算法题目专栏分类】【第4期】BFS广度优先算法:单词接龙、最小基因变化、二进制矩阵中的最短路径
|
5月前
|
存储 算法 数据可视化
LeetCode 130题详解:深度优先搜索与广度优先搜索解决被围绕的区域
LeetCode 130题详解:深度优先搜索与广度优先搜索解决被围绕的区域
|
5月前
|
存储 算法 数据可视化
广度优先搜索(BFS)+回溯解决LeetCode 126题:单词接龙 II
广度优先搜索(BFS)+回溯解决LeetCode 126题:单词接龙 II
|
5月前
|
存储 算法 数据可视化
LeetCode 题目 97:动态规划、递归到广度优先搜索BFS 实现交错字符串
LeetCode 题目 97:动态规划、递归到广度优先搜索BFS 实现交错字符串
|
5月前
|
搜索推荐 Java
单源最短路(只有一个起点)bfs,多源BFS,目录力扣675.为高尔夫比赛砍树,多源最短路问题:力扣542.01矩阵力扣1020.飞地的数量
单源最短路(只有一个起点)bfs,多源BFS,目录力扣675.为高尔夫比赛砍树,多源最短路问题:力扣542.01矩阵力扣1020.飞地的数量
|
6月前
【Leetcode 2583】二叉树中的第K大层和 —— 优先队列 + BFS
解题思路: - 使用队列保存节点,按层序依次保存该层节点 - 使用优先队列保存每层节点值的总和,最后剔除前k个大数即可得到
|
6月前
|
算法 测试技术 C#
【动态规划】【广度优先搜索】LeetCode:2617 网格图中最少访问的格子数
【动态规划】【广度优先搜索】LeetCode:2617 网格图中最少访问的格子数
|
6月前
|
算法
leetcode-675:为高尔夫比赛砍树 (最短路径算法bfs,dijkstra,A*)
leetcode-675:为高尔夫比赛砍树 (最短路径算法bfs,dijkstra,A*)
56 0