大家好,我是考100分的小小码 ,祝大家学习进步,加薪顺利呀。今天说一说同事想用递归,被我一个深度遍历打断施法「终于解决」,希望您对编程的造诣更进一步.
前言
某一天,我正在愉快的敲着代码,听到隔壁同事在嘀咕一个功能的实现方案,疑似树形结构数据的查询。抱着好奇好学的心态,于是我凑了过去。
我: 你是在搞多层题组的查询吗?
同事: 是啊,题组里还有题组,最底层的题组里放着题目id,想封装个函数找出一个题组下的所有题目id。
我: 那你有什么方案实现吗
同事: 看这种树形的数据结构,打算用递归一层一层找了。
我: 那倒不至于,可以借鉴下栈形式的深度遍历思想,一个循环就搞定了,复杂度低,代码易于理解,还不会爆栈。
同事: 还有这种操作?讲讲怎么实现?
随后,我拿起本子和笔跟同事讲解了起来…
深度遍历
深度遍历的思想主要概括为两点:
- 以一个未被访问过的顶点作为起始点,沿着顶点的边一直向下查找未被访问过的点
- 当查找不到可以访问的点,则返回上一个顶点,继续向下查找,直到所有的点都被访问过
简单来说就是“一条路走到黑,不撞南墙不回头”,也突出了“深”的特点。
实现深度遍历有递归和非递归版本的,本文主要讲解非递归版本的,使用栈数据结构来实现,其特点是“先入后出”。在深度遍历中,通常会选择最新的数据最为候补顶点,在候补顶点的管理上就可以使用栈。
图解过程
俗话说的好,千言万语不如一张图。下面将通过图解树和栈的变化来解析深度遍历的过程。在此之前先拟定好一份数据结构。
{
value: 'A',
children: [
{
value: 'B',
children: [
{
value: 'D',
children: [
{
value: 'H',
children: []
}
]
},
{
value: 'E',
children: []
}
]
},
{
value: 'C',
children: [
{
value: 'F',
children: []
},
{
value: 'G',
children: []
}
]
}
]
}
把数据转为二叉树结构,像这样:
1. 此时从顶点A开始遍历
A进栈,栈顶代表当前顶点
2. 向下查找到B
A出栈,B、C进栈
A遍历后,代表已访问,栈中不需要存放,出栈。
可能你会好奇为什么是B、C一起进栈,而不是B进栈,还记得我们上面说的“当查找不到可以访问的点,则返回上一个顶点”,所以我们需要在栈中先存放好之后会遍历的候补顶点,以便于后续返回查找顶点,而此时的C就是候补顶点。如果不理解,也没关系,跟着流程走下去就会慢慢明白了。现在只需要记住栈顶的点就是当前顶点即可。
3. 向下查找到D
B出栈,D、E进栈
4. 向下查找到H
D出栈,H进栈
5. 向下无结点,返回上一个顶点E
H出栈
6. 向下无结点,返回上一个顶点C
E出栈
7. 向下查找到F结点
C出栈,F、G进栈
8. 向下无结点,返回上一个顶点G
F出栈
9. 遍历结束
G出栈,栈空,向下查找无结点,整个遍历过程结束。
JS实现
/** * 深度遍历查找 * @param {*} tree 树形数据 * @param {*} target 想要查找的目标 */
function DFS (tree, target) {
// 模拟栈,管理结点
let stack = [tree]
while (stack.length) {
// 栈顶节点出栈
let node = stack.pop()
// 查找到目标,退出
if (node.value === target) {
return target
}
if (node.children && node.children.length) {
// 将候选顶点入栈,进行下一次循环
stack.push(...node.children.reverse())
}
}
}
reverse
是为了遵循深度遍历的思想,沿着边向下遍历。回想上面的流程图,A遍历完后,下一个节点是B,如果这里不进行反转,直接push
,栈内就会变成[B, C]
,从栈顶取出的结点将会是C。
解决问题
回顾下需要解决的问题,查找多层题组下的题目,每一层题组下可能有题组列表,可能有题目,不管层级多深,需要的是找到题组下所有题目并存储起来。
深度遍历是为了查找某一个结点,而参照这个思想去解决上面所说的场景时,需要做些变式。
- 不需要查找结点,而是全部遍历找出题目子级,遍历过程不设置目标终止条件
- 不需要强行遵循沿边遍历,即不用
reverse
- 栈会将已访问的数据出栈,需要另建一个数组存储所需的数据
得出方案:
/** * 查找题组下的所有题目 * @param {*} tree 树形数据 */
function findQuestions (tree) {
// 模拟栈,管理结点
let stack = [tree]
let res = []
while (stack.length) {
// 栈顶结点出栈
let node = stack.pop()
// 题组有题目
if (node.question_list && node.question_list.length) {
// 存储需要的数据
res.push(...node.question_list)
}
// 题组下还有组
if (node.sub_group_list && node.sub_group_list.length) {
// 将候选顶点入栈,进行下一次循环
stack.push(...node.sub_group_list)
}
}
return res
}
总结
本文通过图解析深度遍历的过程,并参考其思想做了些变式应用在实践中。个人觉得这种使用栈管理树形数据遍历的思想非常不错,相比于递归实现,代码复杂度和空间复杂度要低而且易于理解,最重要的是递归在每往下进行一次都会占据内存,数据层级一旦深,就会引发栈溢出的错误。在以后的业务场景中,如果遇到树形结构遍历时不妨试试这种思想。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
转载请注明出处: https://daima100.com/13009.html