写给前端开发的广度优先搜索

1,493 阅读5分钟

「这是我参与2022首次更文挑战的第9天,活动详情查看:2022首次更文挑战」。

广度优先搜索

广度优先搜索,英文名 Breadth first search,简称 BFS。

为什么会有广度优先搜索

为了提高搜索效率

比如《图解算法》第6章,介绍广度优先搜索时举了一些例子:

  • 编写国际象棋AI,计算最少多少步就可以获胜。
  • 编写拼写检查器,计算最少编辑多少个地方就可以将错拼的单词改为正确的单词。
  • 根据你的人际关系网络找到关系最近的医生。

把这些问题抽象成数据结构和算法,就是在一个很大的集合里去找到我们想要的元素,这个集合可能是状态集、树或者图。

比如下图,从一棵树中找到某个结点:

image.png

如果是人来找这个结点,扫一眼,纵观整棵树,就知道这个结点在什么位置了。

但是计算机不行,计算机只能一个结点一个结点去看,也就是去扫荡一遍。

那么怎样在只扫荡一次的情况下,扫完所有结点呢?

于是就有了广度优先算法,从树的根节点开始,一层一层地往下扫荡,如下图:

image.png

找关系

再举一个图解算法中的例子,假设你经营着一个芒果农场,需要寻找芒果销售商,以便将芒果卖给他。

从你的人际关系中寻找。首先,创建一个朋友名单,然后依次检查名单中的每个人,看看他是否是芒果销售商。

image.png

如果你的朋友里面没有芒果销售商,那么你就必须在你朋友的朋友里查找了。

image.png

这样一来,你不仅在朋友中查找,还在朋友的朋友中查找,甚至可以在朋友的朋友的朋友中查找,直到找到你需要的芒果销售商为止。

寻找芒果销售商的过程,我们把它抽象一下:

image.png

“在你的人际关系中,有芒果销售商吗”其实就是 从结点A出发,有前往结点B的路径吗

从中延伸一下,如果我问:哪个芒果销售商与你关系最近?

这便是最短路径问题,也可以用广度优先搜索查找到,看需要几层朋友关系才能找到芒果销售商。

“哪个芒果销售商与你关系最近”其实就是 从结点A出发,前往结点B的哪条路径最短

广度优先搜索的实现

要实现广度优先搜索,需要借助一个队列,要检索的元素就入队,发现没找到,就把元素出队。

以上面的寻找芒果销售商为例,首先,把朋友关系抽象成下面这样的数据结构,假设朋友6就是芒果销售商:

const friedsRelations = {
  name: '我',
  friedsList: [
    {
      name: '朋友1',
      friedsList: [
        {
          name: '朋友4'
        },
        {
          name: '朋友5'
        }
      ]
    },
    {
      name: '朋友2',
      friedsList: [
        {
          name: '朋友6',
          isTarget: true
        }
      ]
    },
    {
      name: '朋友3'
    }
  ]
}

然后使用广度优先算法去查找这个芒果销售商,代码如下:

function breadthFirstSearch (root) {
  const queue = []                               // 定义一个队列
  queue.unshift(root)                            // 把第一个结点入队

  while (queue.length) {                         // 循环这个队列,如果队列里有元素,说明还在继续查找 
    const top = queue.shift()                    // 每次出队第一个元素
    if (top.isTarget) {                          // 如果找到了,直接返回
      return top.name
    }

    const friedsList = top.friedsList || []      // 如果朋友里找不到,就需要去找朋友的朋友了。

    for (let i = 0, len = friedsList.length; i < len; i++) {
      queue.push(friedsList[i])                  // 把朋友的朋友放到队列最后
    }
  }
}

运行测试一下:

console.log('芒果经销商 :>> ', breadthFirstSearch(friedsRelations))

image.png

跟着前面的思路来写代码,如此轻松地就实现了一个广度优先搜索,图解算法yyds!

时间复杂度:O(n),因为每个结点进队出队各一次。

空间复杂度:O(n),因为队列中元素的个数不超过 n 个。

广度优先搜索算法遍历 Dom 树

一道经典面试题,跟上面的寻找芒果销售商问题,可以说几乎是一模一样。

function breadthFirstSearch (root) {
  const res = []
  if (root) {
    const queue = []
    queue.unshift(root)
    while (queue.length) {
      const top = queue.shift()
      res.push(top)
      const children = top.children
      for (let i = 0; i < children.length; i++) {
        queue.push(children[i])
      }
    }
  }
  return res
}

打开淘宝网,把代码放进去:

image.png

遍历成功!

二叉树的层序遍历

image.png

const levelOrder = function (root) {
  const queue = []

  queue.unshift(root)

  while (queue.length) {
    const top = queue.shift()

    res.push(top.val)

    if (top.left) {
      queue.push(top.left)
    }

    if (top.right) {
      queue.push(top.right)
    }
  }

  return res
}

又是一样的套路,有没有!

image.png

leetcode 广度优先搜索初体验

leetcode 102

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

image.png

这题其实就是二叉树的层序遍历,只是返回格式有点麻烦,是个二维数组。

const levelOrder = function (root) {
  const queue = []
  const res = []

  queue.unshift(root)

  while (queue.length) {
    res.push([])

    for (let i = 0, len = queue.length; i < len; i++) {
      const top = queue.shift()
      res[res.length - 1].push(top.val)
      
      if (top.left) {
        queue.push(top.left)
      }
      if (top.right) {
        queue.push(top.right)
      }
    }
  }
  return res
}

代码中稍微有些变化,但万变不离其宗,还是一个套路。

小结

如果有人问你,什么是广度优先搜索?

你就反问他,你平时在朋友中找关系的时候怎么找的就行。

而一共通过了几层朋友关系找到的,就是最短路径问题。

当然,通过代码来实现广度优先搜索需要借助队列,需要稍微理解一下进队和出队的逻辑,但相信我,自己动手去写下代码体会下,就能掌握。

这里贴一个我看到的通过动图的方式讲解 BFS 的文章,写得非常详细,可以拓展阅读下。

LeetCode 例题精讲 | 13 BFS 的使用场景:层序遍历、最短路径问题

另外,算法不是玄学,两星期前,我就是一个货真价实的算法初学者。这两星期,通过了解数据结构或算法诞生的前因后果,再结合编码实践,慢慢地熟悉了一些算法。

你一定也可以!

往期算法相关文章

林学算法-链表初识(js)

写给前端开发的算法简介

林学算法-树初识(js)

写给算法初学者的分治法和快速排序(js)

写给前端开发的散列表介绍