有一套考察算法的小题目。后台返回一个扁平的数据结构,转成树。
我们看下题目:打平的数据内容如下:
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": [
// 结果 ,,,
]
}
]
}
]
我们的要求很简单,可以先不用考虑性能问题。实现功能即可,回头分析了面试的情况,结果使我大吃一惊。
10%的人没思路,没碰到过这种结构
60%的人说用过递归,有思路,给他个笔记本,但就是写不出来
20%的人在引导下,磕磕绊绊能写出来
剩下10%的人能写出来,但性能不是最佳
感觉不是在招聘季节遇到一个合适的人真的很难。
接下来,我们用几种方法来实现这个小算法
什么是好算法,什么是坏算法
判断一个算法的好坏,一般从执行时间
和占用空间
来看,执行时间越短,占用的内存空间越小,那么它就是好的算法。对应的,我们常常用时间复杂度代表执行时间,空间复杂度代表占用的内存空间。
时间复杂度
时间复杂度的计算并不是计算程序具体运行的时间,而是算法执行语句的次数。
随着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)
}
}
不考虑性能实现,递归遍历查找
主要思路是提供一个递getChildren
的方法,该方法递归
去查找子集。就这样,不用考虑性能,无脑去查,大多数人只知道递归,就是写不出来。。。
/**
* 递归查找,获取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;
}
从上面的代码我们分析,该实现的时间复杂度为O(2^n)
。
不用递归,也能搞定
主要思路是先把数据转成Map
去存储,之后遍历的同时借助对象的引用
,直接从Map
找对应的数据做存储
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;
}
从上面的代码我们分析,有两次循环,该实现的时间复杂度为O(2n)
,需要一个Map把数据存储起来,空间复杂度O(n)
最优性能
主要思路也是先把数据转成Map
去存储,之后遍历的同时借助对象的引用
,直接从Map
找对应的数据做存储。不同点在遍历的时候即做Map
存储,有找对应关系。性能会更好。
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;
}
从上面的代码我们分析,一次循环就搞定了,该实现的时间复杂度为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 |
从我们的测试结果来看,随着数量的增大,递归的实现会越来越慢,基本成指数的增长方式。
原文始发于微信公众号(猿来是前端):扁平数据结构转Tree树形结构
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/250181.html