树的操作都离不开遍历,我们对于二叉树的遍历有前序遍历,中序遍历、后序遍历,层次遍历,我们常说的深度遍历和广度遍历其实就是前序遍历和层次遍历。
对二叉树更详细的介绍可以看一下大佬的文章,我这里做一些简要复习和我工作中常用到的一些关于树的操作,给自己留做工作记录也分享给大家,遇到一些比较特别的操作也会持续更新。
[算法总结] 20 道题搞定 BAT 面试------二叉树: juejin.cn/post/684490...
二叉树就是这么简单:juejin.cn/post/684490...
二叉树遍历规则
所谓前序,中序,后续遍历命名的由来是我们访问二叉树根节点的顺序 。前序遍历就是优先访问根节点,中序遍历是第二个访问根节点,后续遍历就是访问完左右节点之后,最后访问根节点。注意访问二字。访问和获取是两个不同的概念,我们可以获取一个节点,但是不访问他。对应在计算机里的概念是,获取一个节点表示将他入栈,访问一个节点是他处于栈顶,我们即将让他出栈。
前序遍历
根结点 ---> 左子树 ---> 右子树
遍历结果:4-2-1-3-3-5-7
中序遍历
左子树---> 根结点 ---> 右子树
遍历结果:1-2-3-4-5-6-7
后序遍历
左子树 ---> 右子树 ---> 根结点
遍历结果:1-3-2-5-7-6-4
层次遍历
只需按层次遍历。
遍历结果:4-2-6-1-3-5-7
树结构
js
let data = [
{
id: 1,
value: 1,
parent: null,
children: [
{
id: 4,
value: 4,
parent: 1,
children: [
{
id: 6,
value: 6,
parent: 4,
children: [],
},
],
},
{
id: 5,
value: 5,
parent: 1,
children: [],
},
],
},
{
id: 2,
value: 2,
parent: null,
children: [],
},
{
id: 3,
value: 3,
parent: null,
children: [],
}
]
树转数组
树转数组的思想是,把树都遍历一遍,每当遇到一个节点,就把节点存储起来放到一个数组里,这个数组就是转化后的数组,常用到的遍历办法就是使用广度遍历和深度遍历。
深度优先遍历
Depth First Search(dfs):它会一直向下访问树的左子树,直到到达叶子节点,然后再回溯到上一层的右子树。
广度优先遍历
Breadth First Search(bfs):通过使用队列来实现,它会按照层级顺序逐层访问树的节点。
js
let treeToArray = (data) => {
let result = []
//深度遍历(递归):递归过程中,每次遇到一个节点有children,就进入递归
let dfs = tree => {
tree.forEach(item => {
let { children, ...res } = item //可有可无,看你是否还需要chileren属性
result.push(res)
if (item.children && item.children.length > 0) {
dfs(item.children)
}
});
}
//深度遍历(迭代):采用栈是思想,后进先出,每当有节点出栈后都需要把节点push到result
let dfs1 = tree => {
tree.forEach(node => {
let stack = []
stack.push(node)
while (stack.length) { //循环结束条件:栈里一个节点也没有了
let item = stack.pop()//把栈顶弹出去执行,然后把当前节点放到result中
let { children, ...res } = item //可有可无,看你是否还需要chileren属性
result.push(res)
while (item.children.length > 0) {
let i = item.children.pop()
stack.push(i)
}
}
})
}
//广度遍历:采用队列的思想,先进先出,每当有节点出队列后都需要把节点push到result
let bfs = tree => {
tree.forEach(node => {
let queue = []
queue.push(node)
while (queue.length) {//迭代结束的条件:队列里一个节点都没了
let item = queue.shift() //队列的第一个
let { children, ...res } = item //可有可无,看你是否还需要chileren属性
result.push(res)
for (let i of item.children) {
queue.push(i)
}
}
})
}
dfs(data) //[1,4,6,5,2,3]
// dfs1(data) //[1,4,6,5,2,3]
// bfs(data) //[1,4,5,6,2,3]
return result
}
console.log('treeToArray', treeToArray(data))
深度、广度方法可以任选一个去做操作,但是也会有所差异:
相同点:
时间复杂度都是O(n)
不同点:
1、如果树的结构是高度不平衡的,即某一条路径上的节点数量远大于其他路径,那么深度优先遍历可能会更快,因为它会更快地到达叶子节点。
2、bfs的空间复杂度是O(w),dfs的空间复杂度是O(h),w是树的最大宽度,h是树的最大高度。
3、如果考虑性能问题,深度优先遍历建议使用迭代的方式实现,可以避免递归带来的性能消耗。
数组转树
这里其实是数组操作,但是写了树转数组就多写一个数组转树吧。 数组结构:
js
let arr = [
{
id: 1, value: 1, parent: null
},
{
id: 2, value: 2, parent: null
},
{
id: 3, value: 3, parent: null
},
{
id: 4, value: 4, parent: 1
},
{
id: 5, value: 5, parent: 1
},
{
id: 6, value: 6, parent: 4
},
]
实现代码:
js
let arrToTree = (arr, idProp = 'id', parentProp = 'parent') => {
let tree = []
let map = {}
arr.forEach(item => {
map[item[idProp]] = item
})
arr.forEach(item => {
let parent = map[item[parentProp]]
if (item[parentProp]) {
(parent.children || (parent.children = [])).push(item)
} else {
tree.push(item)
}
})
return tree
}
根据id查找对应的树节点
这里的办法跟上面是很类似的,核心思想还是利用遍历,边查询边对比,遇到查找目标就return。
js
//使用的是递归版深度遍历
//prop是可替换的,可能你要查找的属性是componyId,那这个prop就是componyId
let findNodeById = (tree, id, prop = "id") => {
for (let item of tree) {
//判断节点是否符合
if (item[prop] === id) {
return item
}
if (item.children && item.children.length > 0) {
//递归调用时,如果符合条件的就退出递归
const result = findNodeById(item.children, id, 'id')
if (result) {
return result
}
}
}
}
console.log('result', findNodeById(data, 5, 'id'))
查找某个节点的路径
实现思想:遍历+回溯
由于路径的是纵深的,所以采用深度遍历的方式; 由于不知道目标节点是在哪棵树,所以需要有回溯的思想,每次遍历的时候,都记录节点,如果查到叶子节点都没找到目标节点,就清除当前添加的path,如果找到就需要把整个路径返回。
js
let findPathById = (tree, id, prop = "id", path = []) => {
for (let node of tree) {
path.push(node[prop])
if (node[prop] === id) return path
if (node.children && node.children.length) {
let result = findPathById(node.children, id, prop, path)
if (result) return result //找到匹配的节点了
}
//比如先找到[1,4,6],已经是叶子节点了,都没找到,6不是目标,pop出去
//回溯上一层是4,也不是目标,pop出去,进入for的第二层,此时node是5
//把节点5添加进path,匹配成功,return path
path.pop() //找不到匹配的节点,将其从路径中删除
}
return null //遍历完整棵树的了,都找不到匹配的节点,返回null
}
let path = []
findPathById(data, 5, 'id', path)
console.log('path', path) //[1,5]
查找所有叶子节点
实现思想:遍历树,然后把children = [] 的节点添加到一个新数组里。
js
let findLeaves = (tree, list = []) => {
tree.forEach(node => {
if (node.children && node.children.length) {
findLeaves(node.children, list)
} else {
console.log('node', node)
list.push(node)
}
})
return list
}
console.log('leaves', findLeaves(data)) //[6,5,2,3]
查找树的所有叶子节点路径
结合上面查找路径和查找叶子节点的方法,找到叶子节点时,把该叶子节点的路径(path)加入到结果集(list)中。
js
let findAllPath = (tree, path = [], list = []) => {
tree.forEach(node => {
path.push(node)
if (node.children && node.children.length) {
findAllPath(node.children, path, list)
} else {
list.push([...path])
}
path.pop()
})
return list
}
console.log('path--', findAllPath(data)) //[[1,4,6],[1,5],[2],[3]]
最后
以上代码可能需要根据实际数据列表情况做调整,仅供参考!