算法:扁平数据结构转tree(JavaScript版)

        前两天看了一篇文章,提到了一道算法,扁平数据结构转成tree,然后尝试写了一下。

// 源数据
let arr = [
    {id: 1, name: '部门1', pid: 0},
    {id: 2, name: '部门2', pid: 1},
    {id: 3, name: '部门3', pid: 1},
    {id: 4, name: '部门4', pid: 3},
    {id: 5, name: '部门5', pid: 4},
]

// 输出结果
[
    {
        "id":1,
        "name":"部门1",
        "pid":0,
        "chilrden":[
            {
                "id":2,
                "name":"部门2",
                "pid":1,
                "chilrden":[]
            },
            {
                "id":3,
                "name":"部门3",
                "pid":1,
                "chilrden":[
                    {
                        "id":4,
                        "name":"部门4",
                        "pid":3,
                        "chilrden":[
                            {
                                "id":5,
                                "name":"部门5",
                                "pid":4,
                                "chilrden":[]
                            }
                        ]
                    }
                ]
            }
        ]
    }
]

        首先看到题目的第一反应是使用双层循环,不过发现对于这道题目,双层循环只能解决转成深度为2的树,不满足层级比较深的场景。

        然后我们就会想到使用递归能不能解决问题呢,但是如何使用递归呢?

使用递归

        刚看到这道题的时候,可能一头雾水,我们得找个下手的地方。根据题目分析,数组对象中的pid是父节点的id,pid为0时,则为根节点。

        所以我们第一步就是遍历一遍数组,找到数组中pid为0的对象然后添加到结果集中,并且一旦找到我们就要趁热打铁,看一下当前对象有没有子节点。

        也就是重新遍历一遍数组,找一下数组中有没有pid为当前对象的id,有的话添加到当前对象的chilrden中,按这个规律,继续往下找,如果已经遍历一遍了,则退回上一步,从上个节点继续找。直到最外成的循环遍历完,算法结束。如果还不太明白,我们看一下这道题目的执行流程图。

按照这个思路我们来看一下算法:

function test(list) {    // 这是个启动方法,需要这个方法发动技能
    let result = [];    //    结果集
    arrayToTree(list, result, 0);    // 递归启动!!!
    return result;
}

/**
 * @param {*} data 源数组
 * @param {*} result 将结果添加到这个数组
 * @param {*} pid 父id
 */
function arrayToTree(data, result, pid) {
    for (let item of data) {    // 每次调用此方法都重新遍历一遍源数组
        if (item.pid === pid) {    // 如果当前的pid等于传进来的父id,则
            let newItem = {...item, chilrden : []};    // 将当前对象所有属性和新增属性chilrden合并成一个新对象
            result.push(newItem);    // 将新对象添加的传进来的数组中
            arrayToTree(data, newItem.chilrden, item.id);    // 这里注意,调用递归方法时,将新对象的chilrden作为结果数组传入,下一层的结果则会添加到这一层对象的chilrden中
        }
    }
}

        采用递归的方法能够快速简单的实现问题,并且不需要考虑源数组的顺序,但是有个弊端就是随着数据量越来越大的话,性能会越来越差,那么能不能不使用递归呢,当然可以。

使用map

        根据上面递归方法我们能发现,算法主要是在很多重复查找的过程中耗费了很多的时间,那么我们如果能将他们每个对象都先存下来,在需要的时候直接读取就会节省很多时间。这时候我们就得益于js的引用类型了。

        我们首先定义一个新对象,将源数组中每一个对象的id作为key,将当前对象所有属性和新增属性chilrden作为value。

        然后我们只需要遍历一遍,将key为当前id的对象,添加到key等于pid的的对象的chilrden中。在内存中就相当于链表一样串联起来了。上代码。

function arrayToTree(list) {
    let result = [];    // 结果集
    let map = {};

    for(let item of list) {    // 遍历一遍源数组
        map[item.id] = {...item, chilrden: []};    // 将源数组中每一个对象的id作为key,将当前对象所有属性和新增属性chilrden作为value。
    }

    for (let item of list) {
        if (item.pid === 0) {    // 当pid为0时,添加到结果集
            let newItem = map[item.id];    // 注意!这里一定要将map[item.id] 赋值给新变量,这样newItem就和map[item.id]指向同一个内存地址了,达到数据共享
            result.push(newItem);
        } else {
            map[item.pid].chilrden.push(map[item.id]);    // 将key为当前id的对象,添加到key等于pid的对象的chilrden中
        }
    }
    return result;
}

内存中数据图例如下(看懂掌声),当输出result时,就会将后续节点都输出。

map优化 

       写到这里已经差不多了,不过能发现算法在执行过程中循环了两次,一次是存储数据,一次是查找赋值,能不能放一个循环里呢,当然可以,不过需要加点小改动。直接看代码,看不懂的地方,注意看注释

function arrayToTree(list) {
    let result = [];    // 结果集
    let map = {};
    for (let item of list) {
    
        if (!map[item.id]) {    // 如果map[item.id]已经有数据了,那么就不需要这一步了,否则会覆盖原有的数据。
            map[item.id] = { chilrden:[] };    // 先预先给map每一项定义一个chilrden属性
        }
        map[item.id] = {    
            ...item,
            chilrden: map[item.id]['chilrden']    // 注意!这里要使用已经定义的chilrden或原来的数据,否则引用不到。
        };

        if (item.pid === 0) {
            let newItem = map[item.id];
            result.push(newItem);
        } else {
            if (!map[item.pid]) {    // 如果源数组没按id排序的话,这里的map[item.pid]很有可能还没定义,所以我们需要先给这个位置预定义一个chilrden属性。这里可能消除了你的疑问,为什么第6行代码需要先判断一下了。
                map[item.pid] = { chilrden:[] };
            }
            map[item.pid].chilrden.push(map[item.id]);
        }
    }
    return result;
}

        有可能你有疑问了,我不想像上面这样改,我直接将6行代码移动到11行,然后将5到7行代码删掉的话不就行了?这样也是可以的。不过这种方法需要先将源数据按id排好序,否则下面第17行代码就会找不到位置添加,如果排好序再调用这个方法也算是一种思路,如下面代码所示。

function arrayToTree(list) {
    let result = [];
    let map = {};

    // for(let item of list) {    // 遍历一遍源数组
    //     map[item.id] = {...item, chilrden: []};    // 将源数组中每一个对象的id作为key,将当前对象所有属性和新增属性chilrden作为value。
    // }

    for (let item of list) {

        map[item.id] = {...item, chilrden: []};    // 将源数组中每一个对象的id作为key,将当前对象所有属性和新增属性chilrden作为value。

        if (item.pid === 0) {    // 当pid为0时,添加到结果集
            let newItem = map[item.id];    // 注意!这里一定要将map[item.id] 赋值给新变量,这样newItem就和map[item.id]指向同一个内存地址了,达到数据共享
            result.push(newItem);
        } else {
            map[item.pid].chilrden.push(map[item.id]);    // 将key为当前id的对象,添加到key等于pid的对象的chilrden中
        }
    }
    return result;
}

        如果代码注释有看不懂的,评论区留下你的疑问。哪里有问题的,欢迎指正。

算法参考文章:

https://juejin.cn/post/6983904373508145189#heading-0

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值