JS基础——常见的树的操作(持续更新)

树的操作都离不开遍历,我们对于二叉树的遍历有前序遍历,中序遍历、后序遍历,层次遍历,我们常说的深度遍历和广度遍历其实就是前序遍历和层次遍历。

对二叉树更详细的介绍可以看一下大佬的文章,我这里做一些简要复习和我工作中常用到的一些关于树的操作,给自己留做工作记录也分享给大家,遇到一些比较特别的操作也会持续更新。

算法总结\] 20 道题搞定 BAT 面试------二叉树: [juejin.cn/post/684490...](https://juejin.cn/post/6844903669326954504?searchId=20240116100835BB35173794B329655E96#heading-0 "https://juejin.cn/post/6844903669326954504?searchId=20240116100835BB35173794B329655E96#heading-0") 二叉树就是这么简单:[juejin.cn/post/684490...](https://juejin.cn/post/6844903582202855438?searchId=20240116101146B0D4AE6FBD841D65669C "https://juejin.cn/post/6844903582202855438?searchId=20240116101146B0D4AE6FBD841D65669C") # 二叉树遍历规则 所谓前序,中序,后续遍历命名的由来是我们访问**二叉树根节点的顺序** 。前序遍历就是优先访问根节点,中序遍历是第二个访问根节点,后续遍历就是访问完左右节点之后,最后访问根节点。注意访问二字。访问和获取是两个不同的概念,我们可以获取一个节点,但是不访问他。对应在计算机里的概念是,获取一个节点表示将他入栈,访问一个节点是他处于栈顶,我们即将让他出栈。 ![image.png](https://file.jishuzhan.net/article/1747179964618248194/bb9afd79035c3d84b162d781e0503431.webp) ## 前序遍历 根结点 ---\> 左子树 ---\> 右子树 遍历结果: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]] ``` # 最后 以上代码可能需要根据实际数据列表情况做调整,仅供参考!

相关推荐
冠位观测者2 分钟前
【Leetcode 每日一题 - 补卡】1534. 统计好三元组
数据结构·算法·leetcode
明月看潮生7 分钟前
青少年编程与数学 02-016 Python数据结构与算法 25课题、量子算法
python·算法·青少年编程·量子计算·编程与数学
JNU freshman42 分钟前
C. Robin Hood in Town思考与理解
算法
Lysun0011 小时前
dispaly: inline-flex 和 display: flex 的区别
前端·javascript·css
山禾女鬼0011 小时前
Vue 3 自定义指令
前端·javascript·vue.js
_x_w1 小时前
【17】数据结构之图及图的存储篇章
数据结构·python·算法·链表·排序算法·图论
啊卡无敌1 小时前
Vue 3 reactive 和 ref 区别及 失去响应性问题
前端·javascript·vue.js
anscos1 小时前
Actran声源识别方法连载(二):薄膜模态表面振动识别
人工智能·算法·仿真软件·actran
涵信2 小时前
第九节:React HooksReact 18+新特性-React 19的use钩子如何简化异步操作?
前端·javascript·react.js
-优势在我2 小时前
LeetCode之两数之和
算法·leetcode