首先我们要知道为什么需要分析这些复杂度?
任何一个程序最重要是准确性,即要确保程序能正常运行,实现预期功能。
但是,任何一个有价值的程序除了确保能正常运行,还要确保尽量短的运行时间和尽量少的运行空间,使程序正确高效执行得到预期效果。这就涉及时间复杂度分析和(空间复杂度分析),通过分析程序算法的时间复杂度可以找出运行时间尽量短的算法。
对于一些数据处理比较少的简单程序,不同算法使程序运行时间不同,但由于数据处理量少,这种运行时间的差别可以忽略。但是在实际应用中,很多程序往往涉及相当大量的数据处理,这就会导致实现同一个功能的程序,用不同算法,运行时间差别很大。有些算法可能只要几秒,有些算法却要几天才能得到结果。这时候,时间复杂度的分析就显得必要
时间复杂度和空间复杂度分别表示什么?
T(n) = O(f(n)); //时间复杂度
S(n) = O(f(n)); //空间复杂度
它们都叫做大O表示法,
T代表算法执行总时长,
S代表算法占用总空间,
f(n)代表执行的总次数。
时间复杂度:
举个例子:
function total(n) { // 1
var sum = 0; // 2
for (var i = 0; i < n; i++) { // 3
sum += i; // 4
} //5
} //6
上面的代码,在执行第二行的时执行了一次,暂且记做为1,第二行代码的迭代器,由于不知晓n的长度,所以暂且记做为n,第四行代码嵌套在迭代器中,所以也记做为n,所以他们的执行次数为 (2n+1)
再举个例子
function total(n) { // 1
var sum = 0; // 2
for (var i = 0; i < n; i++) { // 3
for (var j = 0; j < n; j++) { // 4
sum = sum + i + j; // 5
}
}
}
按照之前的逻辑,第二行需要执行一次,第三行需要执行n次,第四行呢?大家都知道在迭代器中嵌套是互乘的关系,所以是n²,第五行的常亮嵌套在第四行中,所以它也是n²,总共计算所得(1+n+2n²)
所以由此得出,代码的总执行时间T(n)与每行代码的执行次数成正比
T(n) = O(f(n))
T(n)代表代码的执行时间,n代表数据规模的大小,f(n)代表代码执行次数综合,O代表代码的执行时间与f(n)成正比
所以上述的两个例子的结果(2n+1)和(1+n+2n²)就是大O时间复杂表示法,它并不是代码的真正执行时间,而是标识代码随着数据规模增长的变化趋势,简称为时间复杂度。
并且在n比较大的时候,常亮是不会起到决定性的作用,所以只保留一个最大量级即可,所以对上述两个结果简化所得(n)和(n²)。
如何快速分析一段代码的时间复杂度?
我们只需要关注循环次数最多的那一段代码即可
function total(n) { // 1
var sum = 0; // 2
for (var i = 0; i < n; i++) { // 3
sum += i; // 4
} //5
} //6
很明显,在第三和第四行代码执行次数是最多的,分别执行了n次,忽略常数项,所以这段代码的时间复杂度为O(n)
集中常见的时间复杂度分析:
类型 | 大O表示法 | 术语 | 说明 |
---|---|---|---|
15 | O(1) | 常数阶 | 最低的时间复杂度,无论输入数据增大多少倍,耗时永远不变 |
3logn | O(logn) | 对数阶 | 以2为底数的log,数据增大n倍,耗时更加logn倍,比如数据量增大256倍时,耗时只增大8倍,因为它术语二分查找法,每次取一半来进行排除,所以256个数据就只需要查找八次就可以找到目标 |
3n+5 | O(n) | 线性阶 | 去除常量,只剩下n,数据量增大几倍,耗时也增大几倍 |
5n^2+3n+1 | O(n^2) | 平方阶 | n的平方,计算的复杂度随着数据量的平方根增长 |
6n^3+4n+2 | O(n^3) | 立方阶 | n的立方,计算的复杂度随着数据量的立方根增长 |
2^n+1 | O(2^n) | 指数阶 | 左边为底数,右上角为指数,结果为幂,随着数据量的指数增长 |
n!+3 | O(n!) | 阶乘阶 | n! = n*(n-1),随着数据量的阶乘增长 |
空间复杂度
空间复杂度的推算和时间复杂度类似,唯一不同的是,空间复杂度表示算法的存储空间和数据规模之间的关系
举个例子:
function initArr(n) {
var arr = [];
for (var i = 0; i < n; i++) {
arr[i] = i;
}
}
忽略掉常量,那么它的空间复杂度就是O(n);
如何优化程序,降低时间和空间复杂度?
再举个例子:
function total(n) {
var sum = 0;
for (var i = 1; i < n; i++) {
sum += i;
}
return sum;
}
很明显,它的复杂度为O(n),我们尝试把它降维O(1),常数阶
function total(n) {
var sum = n*(n+1)/2
return sum;
}
但更高层的延伸还可分为低中高的复杂度等级:
function find(n, x, arr) {
let ind = -1;
for (let i = 0; i < n; i++) {
if (arr[i] === x){
ind = i;
break;
}
}
return ind;
}
其中的if判断是一个位置的结果,因为你并不知道x这个值是在第一位还是在最后一位,所以还分为低中高的三个复杂度等级
总结
复杂度也叫渐进复杂度,包含了时间和空间复杂度,一个标识执行的时长,一个表示占用的内存,它们都用来分析算法执行效率与数据规模之前的增长关系,越高阶的复杂度算法,执行效率越低
参考文章:
https://www.jb51.net/article/167003.htm 时间和空间复杂度的浅析
https://zhuanlan.zhihu.com/p/54267154 时间和空间复杂度的等级划分
https://juejin.im/post/5c2a1d9d6fb9a04a0f654581 时间和空间复杂度的讲解