前言
最近亲眼见证了算法的魅力。
在一场集成测试中,产品通过excel导入800条数据,用时8s之久,体验很不友好。
之所以用时这么久,除了校验逻辑比较多之外,更多的是3个sheet联动校验造成多层嵌套循环,形成了笛卡尔积
。
后来后端重新找了3个sheet的对应关系,避免了笛卡尔积的形成,优化之后,同样导入800条数据,时间缩短至2-3秒。
也许有人会说,你这上面举的是后端的例子,跟前端有什么关系。
其实不然,随着大前端的发展,前端不只局限于界面与交互,越来越多的计算也交于前端处理。
我本人在进行excel导出功能开发时,就被要求前端进行求和运算、数据拆分、数据排序等,而后端只负责返回原始数据。
所以,我开始了js数据结构与算法
的学习,以期能够适应日趋复杂的前端计算。
开始
在正式学习js数据结构与算法之前,我们需要了解两个非常重要的概念: 时间复杂度
、空间复杂度
。
他们是学好算法的基石,也是衡量我们代码好坏的两个非常重要的标准。
我们平时表示算法复杂度主要就是用 O()
,读作大欧表示法。它的主要计算原则如下:
-
如果只是常数直接估算为1,即
O(1)
,不是说只执行了1次,而是对常量级时间复杂度的一种表示法。 -
对于 O(3n+4) 里常数4对于总执行次数的几乎没有影响,直接忽略不计,系数3影响也不大,因为3n和n都是一个量级的,所以作为系数的常数3也估算为1或者可以理解为去掉系数,所以 O(3n+4) 的时间复杂度为
O(n)
-
如果是多项式,只需要保留n的最高次项,
O( 666n³ + 666n² + n )
,这个复杂度里面的最高次项是n的3次方。因为随着n的增大,后面的项的增长远远不及n的最高次项大,所以低于这个次项的直接忽略不计,常数也忽略不计,简化后的时间复杂度O(n³)
时间复杂度
时间复杂度对应代码的运行时间
。
常见的时间复杂度如下:
- 常数型复杂度:O(1)
- 对数型复杂度:O(logn)
- 线性型复杂度:O(n)
- 线性对数型复杂度:O(nlogn)
- k次型复杂度:O(nᵏ)
- 指数型复杂度:O(kⁿ)
- 阶乘型复杂度:O(n!)
它们的排序如下:
O(1) < O(logn) < O(n) < O(nlogn) < O(n²) < O(n³) < O(2ⁿ) < O(n!)
下面我们具体介绍一下:
O(1)
- 算法里没有循环和递归,只有一些赋值和运算等简单操作
- 算法消耗不随变量增长而增长,性能最佳
- 无论代码执行多少行,即使有几千几万行,时间复杂度都为O(1)
function fun {
const n = 1;
const m = n * 10000;
...
};
O(logn)
- 循环的次数呈现对数级别的增长
- 性能较好
function fun() {
let i = 1;
const n = 100;
while (i < n) {
i *= 2;
}
}
O(n)
- 只有一层循环
- 算法消耗随n的增长而增长,性能一般
- 无论n值有多大,即使是Inifinity,时间复杂度都为O(n)
function fun() {
for (let i = 0; i < n; i++) {
console.log(i);
};
};
O(nlogn)
- 常用于一个对时间复杂度为O(logn)的代码执行一个n次循环
- 算法消耗随n的增长而增长,性能较差
function fun() {
for (let i = 0; i < n; i++) {
while (i < n) {
i *= 10;
};
};
};
O(n²)
- 最常见的算法时间复杂度,可用于快速开发业务逻辑
- 常见于2次循环,或者3次循环,以及k次循环
- 算法消耗随n的增长而增长,性能糟糕
- 实际开发过程中,不建议使用K值过大的循环,否则代码将非常难以维护
function fun() {
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
};
};
};
O(2ⁿ)
- 常见于2次递归、3次递归以及k次递归的情况
- 算法消耗随n的增长而增长,性能糟糕
- 实际开发过程中,k为1时,一次递归的时间复杂度为O(1)。因为O(1^n)无论n为多少都为O(1)。
function fun(n) {
if {n <= 2}{
return 1
}
return fun(n - 1) + fun(n - 2)
};
O(n!)
- 极其不常见
- 算法消耗随n的增长而增长,性能糟糕
function fun(n) {
let count = 0;
for(let i=0; i<n; i++) {
count++;
fun(n-1);
};
};
空间复杂度
空间复杂度对应代码的占用内存
。
常用的空间复杂度有:O(1)
、 O(n)
、O(n²)
,下面我们具体介绍一下:
O(1)
无论代码多少行,只要它不会因为算法的执行而导致额外的空间增长,那么它的空间复杂度就是 O(1)
,例如:
function fun {
const n = 1;
const m = n * 10000;
...
};
O(n)
如果n的数值越大,算法需要分配存储的空间就越多,那么它的空间复杂度就是 O(n)
。例如:
function fun {
let arr = [];
for( let i = 1; i < n; i++ ) {
arr[i] = i;
};
};
O(n²)
O(n²) 这种空间复杂度一般出现在比如二维数组,或是矩阵的情况下,例如:
const arr = [
[1,2,3,],
[1,2,3,],
[1,2,3,],
];