|
|
@ -613,18 +613,11 @@ $T(n)$ 是一次函数,说明其运行时间的增长趋势是线性的,因
|
|
|
|
|
|
|
|
|
|
|
|
我们将线性阶的时间复杂度记为 $O(n)$ ,这个数学符号称为「大 $O$ 记号 big-$O$ notation」,表示函数 $T(n)$ 的「渐近上界 asymptotic upper bound」。
|
|
|
|
我们将线性阶的时间复杂度记为 $O(n)$ ,这个数学符号称为「大 $O$ 记号 big-$O$ notation」,表示函数 $T(n)$ 的「渐近上界 asymptotic upper bound」。
|
|
|
|
|
|
|
|
|
|
|
|
时间复杂度分析本质上是计算“操作数量函数 $T(n)$”的渐近上界。接下来,我们来看函数渐近上界的数学定义。
|
|
|
|
时间复杂度分析本质上是计算“操作数量函数 $T(n)$”的渐近上界,其具有明确的数学定义。
|
|
|
|
|
|
|
|
|
|
|
|
!!! abstract "函数渐近上界"
|
|
|
|
!!! abstract "函数渐近上界"
|
|
|
|
|
|
|
|
|
|
|
|
若存在正实数 $c$ 和实数 $n_0$ ,使得对于所有的 $n > n_0$ ,均有
|
|
|
|
若存在正实数 $c$ 和实数 $n_0$ ,使得对于所有的 $n > n_0$ ,均有 $T(n) \leq c \cdot f(n)$ ,则可认为 $f(n)$ 给出了 $T(n)$ 的一个渐近上界,记为 $T(n) = O(f(n))$ 。
|
|
|
|
$$
|
|
|
|
|
|
|
|
T(n) \leq c \cdot f(n)
|
|
|
|
|
|
|
|
$$
|
|
|
|
|
|
|
|
则可认为 $f(n)$ 给出了 $T(n)$ 的一个渐近上界,记为
|
|
|
|
|
|
|
|
$$
|
|
|
|
|
|
|
|
T(n) = O(f(n))
|
|
|
|
|
|
|
|
$$
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
如下图所示,计算渐近上界就是寻找一个函数 $f(n)$ ,使得当 $n$ 趋向于无穷大时,$T(n)$ 和 $f(n)$ 处于相同的增长级别,仅相差一个常数项 $c$ 的倍数。
|
|
|
|
如下图所示,计算渐近上界就是寻找一个函数 $f(n)$ ,使得当 $n$ 趋向于无穷大时,$T(n)$ 和 $f(n)$ 处于相同的增长级别,仅相差一个常数项 $c$ 的倍数。
|
|
|
|
|
|
|
|
|
|
|
@ -642,17 +635,9 @@ $T(n)$ 是一次函数,说明其运行时间的增长趋势是线性的,因
|
|
|
|
|
|
|
|
|
|
|
|
1. **忽略 $T(n)$ 中的常数项**。因为它们都与 $n$ 无关,所以对时间复杂度不产生影响。
|
|
|
|
1. **忽略 $T(n)$ 中的常数项**。因为它们都与 $n$ 无关,所以对时间复杂度不产生影响。
|
|
|
|
2. **省略所有系数**。例如,循环 $2n$ 次、$5n + 1$ 次等,都可以简化记为 $n$ 次,因为 $n$ 前面的系数对时间复杂度没有影响。
|
|
|
|
2. **省略所有系数**。例如,循环 $2n$ 次、$5n + 1$ 次等,都可以简化记为 $n$ 次,因为 $n$ 前面的系数对时间复杂度没有影响。
|
|
|
|
3. **循环嵌套时使用乘法**。总操作数量等于外层循环和内层循环操作数量之积,每一层循环依然可以分别套用上述 `1.` 和 `2.` 技巧。
|
|
|
|
3. **循环嵌套时使用乘法**。总操作数量等于外层循环和内层循环操作数量之积,每一层循环依然可以分别套用第 `1.` 点和第 `2.` 点的技巧。、
|
|
|
|
|
|
|
|
|
|
|
|
以下代码与公式分别展示了使用上述技巧前后的统计结果。两者推出的时间复杂度相同,都为 $O(n^2)$ 。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$$
|
|
|
|
给定一个函数,我们可以用上述技巧来统计操作数量。
|
|
|
|
\begin{aligned}
|
|
|
|
|
|
|
|
T(n) & = 2n(n + 1) + (5n + 1) + 2 & \text{完整统计 (-.-|||)} \newline
|
|
|
|
|
|
|
|
& = 2n^2 + 7n + 3 \newline
|
|
|
|
|
|
|
|
T(n) & = n^2 + n & \text{偷懒统计 (o.O)}
|
|
|
|
|
|
|
|
\end{aligned}
|
|
|
|
|
|
|
|
$$
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
=== "Java"
|
|
|
|
=== "Java"
|
|
|
|
|
|
|
|
|
|
|
@ -867,6 +852,16 @@ $$
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
以下公式展示了使用上述技巧前后的统计结果,两者推出的时间复杂度都为 $O(n^2)$ 。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$$
|
|
|
|
|
|
|
|
\begin{aligned}
|
|
|
|
|
|
|
|
T(n) & = 2n(n + 1) + (5n + 1) + 2 & \text{完整统计 (-.-|||)} \newline
|
|
|
|
|
|
|
|
& = 2n^2 + 7n + 3 \newline
|
|
|
|
|
|
|
|
T(n) & = n^2 + n & \text{偷懒统计 (o.O)}
|
|
|
|
|
|
|
|
\end{aligned}
|
|
|
|
|
|
|
|
$$
|
|
|
|
|
|
|
|
|
|
|
|
### 第二步:判断渐近上界
|
|
|
|
### 第二步:判断渐近上界
|
|
|
|
|
|
|
|
|
|
|
|
**时间复杂度由多项式 $T(n)$ 中最高阶的项来决定**。这是因为在 $n$ 趋于无穷大时,最高阶的项将发挥主导作用,其他项的影响都可以被忽略。
|
|
|
|
**时间复杂度由多项式 $T(n)$ 中最高阶的项来决定**。这是因为在 $n$ 趋于无穷大时,最高阶的项将发挥主导作用,其他项的影响都可以被忽略。
|
|
|
@ -896,15 +891,11 @@ $$
|
|
|
|
|
|
|
|
|
|
|
|
![常见的时间复杂度类型](time_complexity.assets/time_complexity_common_types.png)
|
|
|
|
![常见的时间复杂度类型](time_complexity.assets/time_complexity_common_types.png)
|
|
|
|
|
|
|
|
|
|
|
|
!!! tip
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
部分示例代码需要一些预备知识,包括数组、递归等。如果你遇到不理解的部分,可以在学完后面章节后再回顾。现阶段,请先专注于理解时间复杂度的含义和推算方法。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 常数阶 $O(1)$
|
|
|
|
### 常数阶 $O(1)$
|
|
|
|
|
|
|
|
|
|
|
|
常数阶的操作数量与输入数据大小 $n$ 无关,即不随着 $n$ 的变化而变化。
|
|
|
|
常数阶的操作数量与输入数据大小 $n$ 无关,即不随着 $n$ 的变化而变化。
|
|
|
|
|
|
|
|
|
|
|
|
对于以下算法,尽管操作数量 `size` 可能很大,但由于其与输入数据大小 $n$ 无关,因此时间复杂度仍为 $O(1)$ :
|
|
|
|
在以下函数中,尽管操作数量 `size` 可能很大,但由于其与输入数据大小 $n$ 无关,因此时间复杂度仍为 $O(1)$ :
|
|
|
|
|
|
|
|
|
|
|
|
=== "Java"
|
|
|
|
=== "Java"
|
|
|
|
|
|
|
|
|
|
|
@ -1132,7 +1123,7 @@ $$
|
|
|
|
|
|
|
|
|
|
|
|
### 平方阶 $O(n^2)$
|
|
|
|
### 平方阶 $O(n^2)$
|
|
|
|
|
|
|
|
|
|
|
|
平方阶的操作数量相对于输入数据大小以平方级别增长。平方阶通常出现在嵌套循环中,外层循环和内层循环都为 $O(n)$ ,因此总体为 $O(n^2)$ :
|
|
|
|
平方阶的操作数量相对于输入数据大小 $n$ 以平方级别增长。平方阶通常出现在嵌套循环中,外层循环和内层循环都为 $O(n)$ ,因此总体为 $O(n^2)$ :
|
|
|
|
|
|
|
|
|
|
|
|
=== "Java"
|
|
|
|
=== "Java"
|
|
|
|
|
|
|
|
|
|
|
@ -1776,7 +1767,7 @@ $$
|
|
|
|
|
|
|
|
|
|
|
|
## 最差、最佳、平均时间复杂度
|
|
|
|
## 最差、最佳、平均时间复杂度
|
|
|
|
|
|
|
|
|
|
|
|
**算法的时间效率往往不是固定的,而是与输入数据的分布有关**。假设输入一个长度为 $n$ 的数组 `nums` ,其中 `nums` 由从 $1$ 至 $n$ 的数字组成,每个数字只出现一次,但元素顺序是随机打乱的,任务目标是返回元素 $1$ 的索引。我们可以得出以下结论。
|
|
|
|
**算法的时间效率往往不是固定的,而是与输入数据的分布有关**。假设输入一个长度为 $n$ 的数组 `nums` ,其中 `nums` 由从 $1$ 至 $n$ 的数字组成,每个数字只出现一次;但元素顺序是随机打乱的,任务目标是返回元素 $1$ 的索引。我们可以得出以下结论。
|
|
|
|
|
|
|
|
|
|
|
|
- 当 `nums = [?, ?, ..., 1]` ,即当末尾元素是 $1$ 时,需要完整遍历数组,**达到最差时间复杂度 $O(n)$** 。
|
|
|
|
- 当 `nums = [?, ?, ..., 1]` ,即当末尾元素是 $1$ 时,需要完整遍历数组,**达到最差时间复杂度 $O(n)$** 。
|
|
|
|
- 当 `nums = [1, ?, ?, ...]` ,即当首个元素为 $1$ 时,无论数组多长都不需要继续遍历,**达到最佳时间复杂度 $\Omega(1)$** 。
|
|
|
|
- 当 `nums = [1, ?, ?, ...]` ,即当首个元素为 $1$ 时,无论数组多长都不需要继续遍历,**达到最佳时间复杂度 $\Omega(1)$** 。
|
|
|
|