vlambda博客
学习文章列表

[超详解] 广度和深度优先搜索

要问我感觉什么算法用得最多,那我一定回答标题写的那两个算法。这里图文并茂得为大家讲解这两个算法,超详细!


“广深算法” 多用于图,图就是多个连接的点。一般情况下,树算是图的一个特殊情况吧。


看下边这棵树,每个数字是一个节点:

一颗二叉树,如果我们要遍历这颗树,那么主要有两种遍历方式:

    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