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

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

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

[算法总结] 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]]

最后

以上代码可能需要根据实际数据列表情况做调整,仅供参考!

相关推荐
香菜大丸5 分钟前
链表的归并排序
数据结构·算法·链表
jrrz08285 分钟前
LeetCode 热题100(七)【链表】(1)
数据结构·c++·算法·leetcode·链表
oliveira-time17 分钟前
golang学习2
算法
清灵xmf20 分钟前
TypeScript 类型进阶指南
javascript·typescript·泛型·t·infer
小白学大数据26 分钟前
JavaScript重定向对网络爬虫的影响及处理
开发语言·javascript·数据库·爬虫
qq_3901617735 分钟前
防抖函数--应用场景及示例
前端·javascript
334554321 小时前
element动态表头合并表格
开发语言·javascript·ecmascript
John.liu_Test1 小时前
js下载excel示例demo
前端·javascript·excel
南宫生1 小时前
贪心算法习题其四【力扣】【算法学习day.21】
学习·算法·leetcode·链表·贪心算法
PleaSure乐事1 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro