如何衡量程序运行的效率?
复杂度是什么 #
复杂度是衡量代码运行效率的重要度量因素。
对于同一个计算任务,不同的计算方法得到结果的过程复杂度是不一样的。根据资源消耗情况,我们可以从两个维度衡量复杂度。
1. 首先,这段代码消耗的资源是什么
一般而言,代码执行过程中会消耗 计算时间 和 计算空间,那么需要衡量的就是时间复杂度
和空间复杂度
。
2. 其次,这段代码对于资源的消耗是多少
我们通常不会关注这段代码对于资源消耗的绝对量,因为不管是时间还是空间,它们的消耗程度都和输入的数据量高度相关。为了更客观地衡量复杂度,我们通常会关注时间或者空间消耗量与输入数据量之间的关系。
复杂度是一个关于输入数据量n
的函数。假设你的代码复杂度是f(n)
,那么就用个大写字母O
把f(n)
括起来就可以,即O(f(n))
。通常,复杂度的计算方法遵循以下几个原则:
1. 复杂度与具体的常系数无关
O(n)
和O(2n)
表示的是同样的复杂度。即 O(2n) = O(n + n) = O(n) + O(n)
2. 多项式级的复杂度相加的时候,选择高者作为结果
O(n²) + O(n)
和O(n²)
表示的是同样的复杂度。即 O(n²) = O(n²) + O(n) = O(n² + n)
注意
O(1)
也是表示一个特殊复杂度。含义为某个任务通过有限可数的资源即可完成。此处有限可数的是指,与输入数据量n
无关。
时间复杂度 #
注意
时间复杂度与代码的结构有着非常紧密的关系
时间复杂度是程序执行所耗费的时间(执行次数)。
常见的时间复杂度衡量级别有:
常数阶O(1)
#
无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是O(1)
。如:
let i = 1;
let j = 2;
++i;
j++;
let m = i + j;
2
3
4
线性阶O(n)
#
代码共执行n
次,则f(n) = n
。时间复杂度为O(n)
。
for (let i = 1; i < n; i++) {
// 语句执行 n 次
}
2
对数阶O(logn)
#
通过不断的缩小查找范围,每次都将i * 2
,假设循环x
次之后i > 2
,也就是说2ˣ = n
,那么 x = log2ⁿ
,则f(n) = log2ⁿ
。时间复杂度为O(logn)
。
let i = 1;
while (i < n) {
i = i * 2;
}
2
3
线性对数阶O(nlogn)
#
将时间复杂度为O(logn)
的代码循环n
遍的话,那么它的时间复杂度就是n * O(logn)
,则f(n) = nlogn
。时间复杂度为O(nlogn)
。
for (let i = 1; i < n; i++) {
let j = 1;
while (j < n) {
j = j * 2;
}
}
2
3
4
5
平方阶O(n²)
#
代码共执行n² + n
次,则f(n) = n²
。时间复杂度为O(n²)
。
for (let i = 0; i < n; i++) {
// 语句执行 n 次
for (let j = 0; j < n; j++) {
// 语句执行 n² 次
}
}
2
3
4
5
立方阶O(n³)
#
代码共执行n³ + n² + n
,则f(n) = n³
。时间复杂度为O(n³)
。
for (let i = 1; i < n; i++) {
// 语句执行 n 次
for (let j = 1; j < n; j++) {
// 语句执行 n² 次
for (let k = 1; k < n; k++) {
// 语句执行 n³ 次
}
}
}
2
3
4
5
6
7
8
K 次方阶O(nᵏ)
#
代码共执行nᵏ + ... + n³ + n² + n
次,则f(n) = nᵏ
。时间复杂度为O(nᵏ)
。
for (let i = 1; i < n; i++) {
// 语句执行 n 次
for (let j = 1; j < n; j++) {
// 语句执行 n² 次
for (let k = 1; k < n; k++) {
// 语句执行 n³ 次
// ...
}
}
}
2
3
4
5
6
7
8
9
指数阶O(2ⁿ)
#
代码共执行2ⁿ⁺¹ - 1
次,则f(n) = 2ⁿ
。时间复杂度为O(2ⁿ)
。
function fn(n) {
if (n === 0) return 1;
return fn(n - 1) + fn(n - 1);
}
2
3
阶乘阶O(n!)
#
待举例。这类算法的时间复杂度为O(n!)
。
空间复杂度 #
注意
空间复杂度与数据结构的设计有关
空间复杂度是程序运行所耗费的空间。
空间复杂度比较常用的有:
常数阶O(1)
#
如果算法执行所需要的临时空间不随着某个变量n
的大小而变化,即此算法空间复杂度为一个常量,可表示为O(1)
let i = 1;
let j = 2;
++i;
j++;
let m = i + j;
2
3
4
线性阶O(n)
#
这里声明了一个大小为n
的数组,所以复杂度应该是O(n)
const arr = [];
for (let i = 1; i < n; i++) {
arr[i] = i;
}
2
3
平方阶O(n²)
#
这里声明了一个大小为n
的数组,但是里面又开辟了一个新的大小为n
的数组,即f(n) = n²
,所以复杂度应该是O(n²)
const arr = [];
for (let i = 1; i < n; i++) {
arr[i] = [];
for (let j = 1; j < n; j++) {
arr[i][j] = i + j;
}
}
2
3
4
5
6
结论 #
- 一个顺序结构的代码,时间复杂度是
O(1)
- 二分查找,或者更通用地说是采用分而治之的二分策略,时间复杂度都是
O(logn)
- 一个简单的 for 循环,时间复杂度是
O(n)
- 两个顺序执行的 for 循环,时间复杂度是
O(n) + O(n) = O(2n)
,其实也是O(n)
- 两个嵌套的 for 循环,时间复杂度是
O(n²)
总结 #
复杂度通常包括时间复杂度
和空间复杂度
,其中时间复杂度与代码的结构设计高度相关,空间复杂度与代码中数据结构的选择高度相关。在具体计算复杂度时需要注意以下几点:
- 复杂度与具体的常系数无关,
O(n)
和O(2n)
表示的是同样的复杂度。 - 复杂度相加的时候,选择高者作为结果,也就是说
O(n²) + O(n)
和O(n²)
表示的是同样的复杂度。 O(1)
也是表示一个特殊复杂度,即任务与算例个数n
无关。
练习 #
评估以下代码片段的时间复杂度是多少
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
for (let k = 0; k < n; k++) {
// ...
}
for (let m = 0; m < n; m++) {
// ...
}
}
}
2
3
4
5
6
7
8
9
答案解析
代码共执行n + n² + n³ + n³
次,则f(n) = n³
。时间复杂度为O(n³)
for (let i = 0; i < n; i++) {
// 语句执行 n 次
for (let j = 0; j < n; j++) {
// 语句执行 n² 次
for (let k = 0; k < n; k++) {
// 语句执行 n³ 次
// ...
}
for (let m = 0; m < n; m++) {
// 语句执行 n³ 次
// ...
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13