前两天看了一篇文章,提到了一道算法,扁平数据结构转成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;
}
如果代码注释有看不懂的,评论区留下你的疑问。哪里有问题的,欢迎指正。
算法参考文章: