[超详解] 广度和深度优先搜索
要问我感觉什么算法用得最多,那我一定回答标题写的那两个算法。这里图文并茂得为大家讲解这两个算法,超详细!
“广深算法” 多用于图,图就是多个连接的点。一般情况下,树算是图的一个特殊情况吧。
看下边这棵树,每个数字是一个节点:
一颗二叉树,如果我们要遍历这颗树,那么主要有两种遍历方式:
1)深度优先
2)广度优先
那么怎么理解这两种种不同的遍历方式呢?你可以这样理解,假设我们要消费每个节点,那么不同的遍历方式具有不同的消费方式,他们分为:
1)深度优先:叶子节点总是先被消费,根节点最后被消费
2)广度优先:根节点最先被消费,层层扩散到叶子节点
二叉树可能大家都比较熟悉,这里扩展为多叉树,看下面动图,表示各个节点被消费的路径(遍历路径):
深度优先,红色表示被消费,绿色表示算法走的路径:
广度优先,红色表示被消费,绿色表示算法走的路径:
从图直观地可以感受到两种不同的遍历方式有两种不同的感受,对于深度优先,任意路径最深的节点先被消费,然后依次返回,直到所有路径消费完。对于广度优先,从树中的表现是:一层一层往下消费。
⚠️ 千万不要被前面的动图局限了你的思维,接下来让我们将算法扩展到图:
深度优先,黄色节点作为起点:
广度优先,黄色节点作为起点,这里做了简化,不可能同时消费多个节点哈:
相信看了这几张图,你应该已经有了清晰的认识了吧。现在就算是把这个算法扩展到空间也不是什么难事了对吧。
“广深”算法的核心在于 队列和堆栈 这两个数据结构。
假设有一堆人买冰淇淋:
1)队列,就像排队一样前面的人先买到冰淇淋。先进先出
2)堆栈,就像插队一样,明明你排第一个,但是不停地有人插队到第一个,结果最后一个插队的人反而先买到冰淇淋。先进后出
堆栈的特性实现了深度算法,而队列的特性实现了广度算法。
你仔细看上面的图,对于深度算法,任意一条路径中,是不是总是最后被绿的点先红。而对于广度算法,总是先绿的点先红。所表现的特性和 队列与堆栈 的特性完全相同。
说到这里,一个宏观的概念已经成型了,话不多说代码奉上(我们以遍历多叉树为例):
测试数据的结构如下图所示:
javaScript 版本,在浏览器中按 f12 ,然后选择下图中的 console 将代码复制进去,敲 enter 就能执行了,好神奇:
/**
* [parent, value] 用元组表示一个节点,其中 第一个元素为父节点的值,第二个元素为该节点的值
*/
let treeNodes = [
[undefined, 0],
[0, 1], [0, 2], [0, 3],
[1, 4], [3, 5], [3, 6], [3, 7],
[4, 8], [4, 9], [4, 10], [4, 11],
[6, 12], [6, 13]
]
/** 这个map的 key 表示节点的值,value 表示节点对象 */
let map = [];
for (let tuple of treeNodes) {
map[tuple[1]] = {
parent: undefined,
value: tuple[1],
children: []
}
}
/** 构建多叉树 */
let root;
for (let tuple of treeNodes) {
let parent = map[tuple[0]]
if (!parent) {
root = map[tuple[1]]; // 这个节点没有父节点,所以就是根节点了
continue;
}
let child = map[tuple[1]]
parent.children.push(child);
}
/** 深度优先搜索 */
let deep_first_search = function (root) {
let childIndexMap = []; // 这个map用来记录遍历到每个节点的那个子节点了
let stack = [root];
while (stack.length > 0) {
let node = stack[stack.length - 1]; // peek 一下
let childIndex = childIndexMap[node.value] || 0;
if (childIndex < node.children.length) {
stack.push(node.children[childIndex]);
childIndexMap[node.value] = childIndex + 1;
} else {
console.log(stack.pop().value); // 消费这个节点
}
}
}
/** 广度优先搜索 */
let broad_first_search = function (root) {
let queue = [root];
while (queue.length > 0) {
let node = queue.shift(); // 弹出队首的元素
for (let child of node.children) {
queue.push(child); // 入队
}
console.log(node.value);
}
}
console.log('test dfs');
deep_first_search(root);
console.log('-------------华丽的分割线-----------------')
console.log('test bfs');
broad_first_search(root);
我执行的结果:
和前面动图遍历多叉树的顺序是不是一致?的确一致,这说明我们说了那么多,其实实现起来也没多难。
这两个算法的用处非常多,比如 ‘走迷宫’,‘找最短路径’,‘拓扑排序’ 等。
现实生活中这两个算法运用更是普遍,比如企业结构中的部门层级,每天用的文本编辑器等。
反正只要与 ‘从点集中找解’ 相关或类似的问题,这两个算法都能派上一点用途。
写作不易,点个关注支持下吧😄
Apelife
No one should be hurt,especially an angel, a doctor save our life