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²).