「这是我参与2022首次更文挑战的第9天,活动详情查看:2022首次更文挑战」。
广度优先搜索
广度优先搜索,英文名 Breadth first search,简称 BFS。
为什么会有广度优先搜索
为了提高搜索效率
比如《图解算法》第6章,介绍广度优先搜索时举了一些例子:
- 编写国际象棋AI,计算最少多少步就可以获胜。
- 编写拼写检查器,计算最少编辑多少个地方就可以将错拼的单词改为正确的单词。
- 根据你的人际关系网络找到关系最近的医生。
把这些问题抽象成数据结构和算法,就是在一个很大的集合里去找到我们想要的元素,这个集合可能是状态集、树或者图。
比如下图,从一棵树中找到某个结点:
如果是人来找这个结点,扫一眼,纵观整棵树,就知道这个结点在什么位置了。
但是计算机不行,计算机只能一个结点一个结点去看,也就是去扫荡一遍。
那么怎样在只扫荡一次的情况下,扫完所有结点呢?
于是就有了广度优先算法,从树的根节点开始,一层一层地往下扫荡,如下图:
找关系
再举一个图解算法中的例子,假设你经营着一个芒果农场,需要寻找芒果销售商,以便将芒果卖给他。
从你的人际关系中寻找。首先,创建一个朋友名单,然后依次检查名单中的每个人,看看他是否是芒果销售商。
如果你的朋友里面没有芒果销售商,那么你就必须在你朋友的朋友里查找了。
这样一来,你不仅在朋友中查找,还在朋友的朋友中查找,甚至可以在朋友的朋友的朋友中查找,直到找到你需要的芒果销售商为止。
寻找芒果销售商的过程,我们把它抽象一下:
“在你的人际关系中,有芒果销售商吗”其实就是 从结点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))
跟着前面的思路来写代码,如此轻松地就实现了一个广度优先搜索,图解算法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
}
打开淘宝网,把代码放进去:
遍历成功!
二叉树的层序遍历
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
}
又是一样的套路,有没有!
leetcode 广度优先搜索初体验
题目描述:给你二叉树的根节点
root
,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
这题其实就是二叉树的层序遍历,只是返回格式有点麻烦,是个二维数组。
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 的使用场景:层序遍历、最短路径问题
另外,算法不是玄学,两星期前,我就是一个货真价实的算法初学者。这两星期,通过了解数据结构或算法诞生的前因后果,再结合编码实践,慢慢地熟悉了一些算法。
你一定也可以!
往期算法相关文章