背景
假如后台返回一个扁平的数据结构,转成树,应该怎么做呢?
打平的数据内容如下:
1 | let arr = [ |
期望输出的结果:
1 | [ |
假如先不用考虑性能问题,实现功能即可。
可能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)。
1 | let x = 1; |
- 有多个循环语句时候,算法的时间复杂度是由嵌套层数最多的循环语句中最内层语句的方法决定的。举例如下:在下面for循环当中,外层循环每执行一次,内层循环要执行n次,执行次数是根据n所决定的,时间复杂度是O(n^2)。
1 | for (i = 0; i < n; i++){ |
- 循环不仅与n有关,还与执行循环判断条件有关。举例如下:在代码中,如果arr[i]不等于1的话,时间复杂度是O(n)。如果arr[i]等于1的话,循环不执行,时间复杂度是O(0)。
1 | for(var i = 0; i<n && arr[i] !=1; i++) { |
空间复杂度
空间复杂度是对一个算法在运行过程中临时占用存储空间的大小。
计算方法
- 忽略常数,用O(1)表示
- 递归算法的空间复杂度=(递归深度n)*(每次递归所要的辅助空间)
计算空间复杂度的简单几点
- 仅仅只复制单个变量,空间复杂度为O(1)。举例如下:空间复杂度为O(n) = O(1)。
1 | let a = 1; |
- 递归实现,调用fun函数,每次都创建1个变量k。调用n次,空间复杂度O(n*1) = O(n)。
1 | function fun(n) { |
不考虑性能实现,递归遍历查找
主要思路是提供一个递getChildren的方法,该方法递归去查找子集。 就这样,不用考虑性能,无脑去查,大多数人只知道递归,就是写不出来。。。
1 | /** |
从上面的代码我们分析,该实现的时间复杂度为O(2^n)。
不用递归,也能搞定
主要思路是先把数据转成Map去存储,之后遍历的同时借助对象的引用,直接从Map找对应的数据做存储
1 | function arrayToTree(items) { |
从上面的代码我们分析,有两次循环,该实现的时间复杂度为O(2n),需要一个Map把数据存储起来,空间复杂度O(n)
最优性能
主要思路也是先把数据转成Map去存储,之后遍历的同时借助对象的引用,直接从Map找对应的数据做存储。不同点在遍历的时候即做Map存储,有找对应关系。性能会更好。
1 | function arrayToTree(items) { |
从上面的代码我们分析,一次循环就搞定了,该实现的时间复杂度为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
从我们的测试结果来看,随着数量的增大,递归的实现会越来越慢,基本成指数的增长方式。
总结
实践出真理,大家共勉进步。