接口返回一份数组型树结构数据(扁平化树)
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,
"children": [
{
"id": 2,
"name": "部门2",
"pid": 1,
"children": []
},
{
"id": 3,
"name": "部门3",
"pid": 1,
"children": [
// 结果 ,,,
]
}
]
}
]
什么是好算法,什么是坏算法
判断一个算法的好坏,一般从执行时间
和占用空间
来看,执行时间越短,占用的内存空间越小,那么它就是好的算法。对应的,我们常常用时间复杂度代表执行时间,空间复杂度代表占用的内存空间。
时间复杂度
时间复杂度的计算并不是计算程序具体运行的时间,而是算法执行语句的次数。
随着n
的不断增大
,时间复杂度不断增大
,算法花费时间
越多。 常见的时间复杂度有
- 常数阶
O(1)
- 对数阶
O(log2 n)
- 线性阶
O(n)
- 线性对数阶
O(n log2 n)
- 平方阶
O(n^2)
- 立方阶
O(n^3)
- k次方阶
O(n^K)
- 指数阶
O(2^n)
计算方法
- 选取相对增长最高的项
- 最高项系数是都化为1
- 若是常数的话用O(1)表示
举个例子:如f(n)=3*n^4+3n+300 则 O(n)=n^4
通常我们计算时间复杂度都是计算最坏情况。计算时间复杂度的要注意的几个点
- 如果算法的执行时间
不随n
的增加
而增长
,假如算法中有上千条
语句,执行时间也不过是一个较大的常数
。此类算法的时间复杂度是O(1)
。 举例如下:代码执行100次,是一个常数,复杂度也是O(1)
。
let x = 1;
while (x <100) {
x++;
}
复制代码
- 有
多个循环语
句时候,算法的时间复杂度是由嵌套层数最多
的循环语句中最内层
语句的方法决定的。举例如下:在下面for循环当中,外层循环
每执行一次
,内层循环
要执行n
次,执行次数是根据n所决定的,时间复杂度是O(n^2)
。
for (i = 0; i < n; i++){
for (j = 0; j < n; j++) {
// ...code
}
}
复制代码
- 循环不仅与
n
有关,还与执行循环判断条件
有关。举例如下:在代码中,如果arr[i]
不等于1
的话,时间复杂度是O(n)。如果arr[i]
等于1
的话,循环不执行,时间复杂度是O(0)
。
for(var i = 0; i<n && arr[i] !=1; i++) {
// ...code
}
复制代码
空间复杂度
空间复杂度是对一个算法在运行过程中临时占用存储空间的大小。
计算方法:
- 忽略常数,用O(1)表示
- 递归算法的空间复杂度=(递归深度n)*(每次递归所要的辅助空间)
计算空间复杂度的简单几点
- 仅仅只复制单个变量,空间复杂度为O(1)。举例如下:空间复杂度为O(n) = O(1)。
let a = 1;
let b = 2;
let c = 3;
console.log('输出a,b,c', a, b, c);
复制代码
- 递归实现,调用fun函数,每次都创建1个变量k。调用n次,空间复杂度O(n*1) = O(n)。
function fun(n) {
let k = 10;
if (n == k) {
return n;
} else {
return fun(++n)
}
解决办法有多种,提供3种方案供参考,注意:数据量必须够大,才能真正看出效果,因为Map本身是有性能消耗的
方案一、不考虑性能实现,递归遍历查找
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},
];
/**
* 递归查找,获取children
*/
const getChildren = (data, result, pid) => {
for (const item of data) {
if (item.pid === pid) {
const newItem = {...item, children: []};
result.push(newItem);
getChildren(data, newItem.children, item.id);
}
}
}
/**
* 转换方法
*/
const arrayToTree = (data, pid) => {
const result = [];
getChildren(data, result, pid)
return result;
}
console.time();
let newArr = arrayToTree(arr, 0);
console.log(newArr);
console.timeEnd();
从上面的代码我们分析,该实现的时间复杂度为O(2^n)
。
方案二、主要思路是先把数据转成Map
去存储,之后遍历的同时借助对象的引用
,直接从Map
找对应的数据做存储
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},
];
function arrayToTree(items) {
const result = []; // 存放结果集
const itemMap = {}; //
// 先转成map存储
for (const item of items) {
itemMap[item.id] = {...item, children: []}
}
for (const item of items) {
const id = item.id;
const pid = item.pid;
const treeItem = itemMap[id];
if (pid === 0) {
result.push(treeItem);
} else {
if (!itemMap[pid]) {
itemMap[pid] = {
children: [],
}
}
itemMap[pid].children.push(treeItem)
}
}
return result;
}
console.time();
let newArr = arrayToTree(arr);
console.log(newArr);
console.timeEnd();
从上面的代码我们分析,有两次循环,该实现的时间复杂度为O(2n)
,需要一个Map把数据存储起来,空间复杂度O(n)
三、更进一步优化Map算法(最优性能)
主要思路也是先把数据转成Map
去存储,之后遍历的同时借助对象的引用
,直接从Map
找对应的数据做存储。不同点在遍历的时候即做Map
存储,有找对应关系。性能会更好。
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},
];
function arrayToTree(items) {
const result = []; // 存放结果集
const itemMap = {}; //
for (const item of items) {
const id = item.id;
const pid = item.pid;
if (!itemMap[id]) {
itemMap[id] = {
children: [],
}
}
itemMap[id] = {
...item,
children: itemMap[id]['children']
}
const treeItem = itemMap[id];
if (pid === 0) {
result.push(treeItem);
} else {
if (!itemMap[pid]) {
itemMap[pid] = {
children: [],
}
}
itemMap[pid].children.push(treeItem)
}
}
return result;
}
console.time();
let newArr = arrayToTree(arr);
console.log(newArr);
console.timeEnd();
从上面的代码我们分析,一次循环就搞定了,该实现的时间复杂度为O(n)
,需要一个Map把数据存储起来,空间复杂度O(n)
小试牛刀
方法 | 1000(条) | 10000(条) | 20000(条) | 50000(条) |
---|---|---|---|---|
递归实现 | 154.596ms | 1.678s | 7.152s | 75.412s |
不用递归,两次遍历 | 0.793ms | 16.499ms | 45.581ms | 97.373ms |
不用递归,一次遍历 | 0.639ms | 6.397ms | 25.436ms | 44.719ms |
从我们的测试结果来看,随着数量的增大,递归的实现会越来越慢,基本成指数的增长方式。
原文链接:https://juejin.cn/post/6983904373508145189