1. 前言
一方面,算法的好坏主要是从执行时所占的 时间 和 空间 两个方面去考量。但它真正耗费的时间和空间只有上机测试才能获得,所以将具体的时间空间抽成两个概念:时间/空间复杂度,用它们进行理论上的衡量;
另一方面,面试时经常会遇到算法题,从而也会经常被问到 时间/空间复杂度,而leetcode的初级算法第一题就涉及了这个概念。。
所以在看算法之前,时间/空间复杂度 是我们必须要知道的。
2. 时间复杂度T(n)
2.1. 概念
维基百科的解释是这样的:
在计算机科学中,算法的时间复杂度(Time complexity)是一个函数,它定性描述该算法的运行时间。这是一个代表算法输入值的字符串的长度的函数。时间复杂度常用大O符号表述,不包括这个函数的低阶项和首项系数。使用这种方式时,时间复杂度可被称为是渐近的,亦即考察输入值大小趋近无穷时的情况。
所以我们总结一下:
-
大O符号表示法:
T(n) = O(f(n))
,这个公式的全称是算法的渐进时间复杂度;- 时间复杂度记作
T(n)
,f(n)
表示每行代码执行次数之和,O
表示正比例关系; - 大O符号表示法并不是用于来真实代表算法的执行时间的,它是用来表示代码执行时间的增长变化趋势的;
- 时间复杂度记作
-
计算时间复杂度
T(n)
注意点:- 我们通常认为变量
n
是无穷大的; - 计算时通常以某一行代码运行的时间为一个单位时间;
- 忽略
f(n)
的低阶项、首项系数、对数中的底数;
- 我们通常认为变量
2.2. 常见时间复杂度量级
时间复杂度的大小比较:
常数阶O(1) < 对数阶O(logN) < 线性阶O(n) < 线性对数阶O(nlogN) < 平方阶O(n²) < 立方阶O(n³) < K次方阶O(n^k) < 指数阶(2^n)
时间复杂度越大,执行效率越低。
2.2.1. 常数阶 - O(1)
一个算法的执行时间总量是一个常量,或者说它的执行时间不随着n的变化而变化,那它的时间复杂度就是O(1);
案例1:
let i = 1;
let j = 2;
++i;
j++;
let m = i + j;
解析:上述代码有5行,一共消耗5个单位时间,用大O可以表示为Tn = O(5)
。忽略常数项,所以它最终的时间复杂度为T(n) = O(1)
.
案例2:
const arr = [1, 2, 3];
for(let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
解析:上述代码虽然有一个for循环,但arr.length是一个常量,第一行消耗一个单位时间,for循环消耗1 * 3个单位时间,所以用大O可以表示为Tn = O(1 + 1 * 3)
。忽略常数项,所以它最终的时间复杂度为T(n) = O(1)
.
2.2.2. 线性阶 - O(n)
如果一个算法的时间复杂度为O(n),则称这个算法具有线性时间,或O(n)时间。非正式地说,这意味着对于足够大的输入,运行时间增加的大小与输入成线性关系。 比如:O(5n + 3n + 1)
、O(5n + 3n)
、O(100n)
等的时间复杂度都是T(n) = O(n)
。
for(let i = 0; i <= n; i++) {
console.log(i);
console.log(n);
}
解析:上述代码虽然与常数阶的案例2类似,但它的for循环会消耗2 * n
个单位时间,用大O可以表示为Tn = O(2n)
。忽略系数,所以它最终的时间复杂度为T(n) = O(n)
.
2.2.3. 平方阶 - O(n²)
如果把O(n)
的代码再嵌套循环一遍,它的时间复杂度就是O(n²)
了。
比如:
for(let i = 0; i < n; i++) {
for(let j = 0; j < n; j++) {
console.log(i);
console.log(j);
}
}
解析:上述代码用大O可以表示为Tn = O(n * (n * 2))
。忽略首相系数,所以它最终的时间复杂度为T(n) = O(n²)
.
2.2.4. 立方阶 - O(n³)、K次方阶 - O(n^k)
参考平方阶去理解就好,这里就不赘述了。
2.2.5. 对数阶 - O(logN)
由于计算机使用二进制的记数系统,对数常常以2为底(即,有时写作)。然而,由对数的换底公式,和只有底数不同,在大O记法忽略底数,记作O(logn)
。
let i = 1;
while(i < n) {
i = i * 2;
}
从上面代码可以看到,在while循环里面,每次都将i乘以2,乘完之后,i距离n就越来越近了。我们试着求解一下,假设循环x次之后,i就大于2了,此时这个循环就退出了,也就是说2的x次方等于n,那么。用大O可以表示为 Tn = O()。忽略系数,所以它最终的时间复杂度为T(n) = O(logn)
.
2.2.6. 线性对数阶 - O(nlogN)
线性对数阶O(nlogN)
其实非常容易理解,将时间复杂度为O(logn)
的代码循环N遍的话,那么它的时间复杂度就是n * O(logN)
,也就是O(nlogN)
。
比如:
for(let j = 1; j < n; m++){
let i = 1;
while(i < n) {
i = i * 2;
}
}
解析:上述代码用大O可以表示为。忽略底数,所以它最终的时间复杂度为T(n) = O(nlogn)
.
2.2.7. 其他
除此之外,其实还有 平均时间复杂度、均摊时间复杂度、最坏时间复杂度、最好时间复杂度 的分析方法,有点复杂,这里就不展开了。
3. 空间复杂度S(n)
3.1. 概念
空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度,记做S(n)=O(f(n))
。
空间复杂度的计算与时间复杂度一致。
3.2. 常见的空间复杂度
3.2.1. 空间复杂度O(1)
const arr = [1, 2, 3];
for(let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
解析:上述代码虽然有一个for循环,但arr.length是一个常量,第一行消耗一个单位空间,for循环消耗1 * 3个单位空间,所以用大O可以表示为Sn = O(1 + 1 * 3)
。忽略常数项,所以它最终的时间复杂度为S(n) = O(1)
.
3.2.2. 空间复杂度O(n)
for(let i = 0; i < n; i++) {
console.log(i);
}
解析:上述代码用大O可以表示为Sn = O(n * 1)
。所以它最终的时间复杂度为S(n) = O(n)
.
3.2.3. 空间复杂度O(n²)
for(let i = 0; i < n; i++) {
for(let j = 0; j < n; j++) {
console.log(i);
console.log(j);
}
}
解析:上述代码用大O可以表示为Sn = O(n * (n * 2))
。忽略首相系数,所以它最终的时间复杂度为S(n) = O(n²)
.