mirror of
https://github.com/krahets/hello-algo.git
synced 2024-12-26 01:26:28 +08:00
build
This commit is contained in:
parent
df0f7d3be1
commit
42fff151ff
8 changed files with 97 additions and 47 deletions
|
@ -3,11 +3,11 @@ comments: true
|
|||
icon: material/timer-sand
|
||||
---
|
||||
|
||||
# 第 2 章 时空复杂度
|
||||
# 第 2 章 复杂度分析
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
![时空复杂度](../assets/covers/chapter_complexity_analysis.jpg){ width="600" }
|
||||
![复杂度分析](../assets/covers/chapter_complexity_analysis.jpg){ width="600" }
|
||||
|
||||
</div>
|
||||
|
||||
|
|
|
@ -102,7 +102,15 @@ status: new
|
|||
=== "Dart"
|
||||
|
||||
```dart title="iteration.dart"
|
||||
[class]{}-[func]{forLoop}
|
||||
/* for 循环 */
|
||||
int forLoop(int n) {
|
||||
int res = 0;
|
||||
// 循环求和 1, 2, ..., n-1, n
|
||||
for (int i = 1; i <= n; i++) {
|
||||
res += i;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
@ -216,7 +224,17 @@ status: new
|
|||
=== "Dart"
|
||||
|
||||
```dart title="iteration.dart"
|
||||
[class]{}-[func]{whileLoop}
|
||||
/* while 循环 */
|
||||
int whileLoop(int n) {
|
||||
int res = 0;
|
||||
int i = 1; // 初始化条件变量
|
||||
// 循环求和 1, 2, ..., n-1, n
|
||||
while (i <= n) {
|
||||
res += i;
|
||||
i++; // 更新条件变量
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
@ -326,7 +344,19 @@ status: new
|
|||
=== "Dart"
|
||||
|
||||
```dart title="iteration.dart"
|
||||
[class]{}-[func]{whileLoopII}
|
||||
/* while 循环(两次更新) */
|
||||
int whileLoopII(int n) {
|
||||
int res = 0;
|
||||
int i = 1; // 初始化条件变量
|
||||
// 循环求和 1, 4, ...
|
||||
while (i <= n) {
|
||||
res += i;
|
||||
// 更新条件变量
|
||||
i++;
|
||||
i *= 2;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
@ -434,7 +464,18 @@ status: new
|
|||
=== "Dart"
|
||||
|
||||
```dart title="iteration.dart"
|
||||
[class]{}-[func]{nestedForLoop}
|
||||
/* 双层 for 循环 */
|
||||
String nestedForLoop(int n) {
|
||||
String res = "";
|
||||
// 循环 i = 1, 2, ..., n-1, n
|
||||
for (int i = 1; i <= n; i++) {
|
||||
// 循环 j = 1, 2, ..., n-1, n
|
||||
for (int j = 1; j <= n; j++) {
|
||||
res += "($i, $j), ";
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
@ -557,7 +598,15 @@ status: new
|
|||
=== "Dart"
|
||||
|
||||
```dart title="recursion.dart"
|
||||
[class]{}-[func]{recur}
|
||||
/* 递归 */
|
||||
int recur(int n) {
|
||||
// 终止条件
|
||||
if (n == 1) return 1;
|
||||
// 递:递归调用
|
||||
int res = recur(n - 1);
|
||||
// 归:返回结果
|
||||
return n + res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
@ -689,7 +738,13 @@ status: new
|
|||
=== "Dart"
|
||||
|
||||
```dart title="recursion.dart"
|
||||
[class]{}-[func]{tailRecur}
|
||||
/* 尾递归 */
|
||||
int tailRecur(int n, int res) {
|
||||
// 终止条件
|
||||
if (n == 0) return res;
|
||||
// 尾递归调用
|
||||
return tailRecur(n - 1, res + n);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
@ -813,7 +868,15 @@ status: new
|
|||
=== "Dart"
|
||||
|
||||
```dart title="recursion.dart"
|
||||
[class]{}-[func]{fib}
|
||||
/* 斐波那契数列:递归 */
|
||||
int fib(int n) {
|
||||
// 终止条件 f(1) = 0, f(2) = 1
|
||||
if (n == 1 || n == 2) return n - 1;
|
||||
// 递归调用 f(n) = f(n-1) + f(n-2)
|
||||
int res = fib(n - 1) + fib(n - 2);
|
||||
// 返回结果 f(n)
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
|
|
@ -9,7 +9,7 @@ comments: true
|
|||
1. **找到问题解法**:算法需要在规定的输入范围内,可靠地求得问题的正确解。
|
||||
2. **寻求最优解法**:同一个问题可能存在多种解法,我们希望找到尽可能高效的算法。
|
||||
|
||||
也就是说,在能够解决问题的前提下,算法效率是衡量算法优劣的主要评价指标,它包括以下两个维度。
|
||||
也就是说,在能够解决问题的前提下,算法效率已成为衡量算法优劣的主要评价指标,它包括以下两个维度。
|
||||
|
||||
- **时间效率**:算法运行速度的快慢。
|
||||
- **空间效率**:算法占用内存空间的大小。
|
||||
|
@ -24,7 +24,7 @@ comments: true
|
|||
|
||||
一方面,**难以排除测试环境的干扰因素**。硬件配置会影响算法的性能表现。比如在某台计算机中,算法 `A` 的运行时间比算法 `B` 短;但在另一台配置不同的计算机中,我们可能得到相反的测试结果。这意味着我们需要在各种机器上进行测试,统计平均效率,而这是不现实的。
|
||||
|
||||
另一方面,**展开完整测试非常耗费资源**。随着输入数据量的变化,算法会表现出不同的效率。例如,在输入数据量较小时,算法 `A` 的运行时间比算法 `B` 更少;而输入数据量较大时,测试结果可能恰恰相反。因此,为了得到有说服力的结论,我们需要测试各种规模的输入数据,而这样需要耗费大量的计算资源。
|
||||
另一方面,**展开完整测试非常耗费资源**。随着输入数据量的变化,算法会表现出不同的效率。例如,在输入数据量较小时,算法 `A` 的运行时间比算法 `B` 更少;而输入数据量较大时,测试结果可能恰恰相反。因此,为了得到有说服力的结论,我们需要测试各种规模的输入数据,而这需要耗费大量的计算资源。
|
||||
|
||||
## 2.1.2 理论估算
|
||||
|
||||
|
|
|
@ -671,10 +671,6 @@ $$
|
|||
|
||||
<p align="center"> 图 2-16 常见的空间复杂度类型 </p>
|
||||
|
||||
!!! tip
|
||||
|
||||
部分示例代码需要一些前置知识,包括数组、链表、二叉树、递归算法等。如果你遇到看不懂的地方,可以在学完后面章节后再来复习。
|
||||
|
||||
### 1. 常数阶 $O(1)$
|
||||
|
||||
常数阶常见于数量与输入数据大小 $n$ 无关的常量、变量、对象。
|
||||
|
|
|
@ -15,7 +15,7 @@ comments: true
|
|||
- 时间复杂度用于衡量算法运行时间随数据量增长的趋势,可以有效评估算法效率,但在某些情况下可能失效,如在输入的数据量较小或时间复杂度相同时,无法精确对比算法效率的优劣。
|
||||
- 最差时间复杂度使用大 $O$ 符号表示,对应函数渐近上界,反映当 $n$ 趋向正无穷时,操作数量 $T(n)$ 的增长级别。
|
||||
- 推算时间复杂度分为两步,首先统计操作数量,然后判断渐近上界。
|
||||
- 常见时间复杂度从小到大排列有 $O(1)$、$O(\log n)$、$O(n)$、$O(n \log n)$、$O(n^2)$、$O(2^n)$、$O(n!)$ 等。
|
||||
- 常见时间复杂度从小到大排列有 $O(1)$、$O(\log n)$、$O(n)$、$O(n \log n)$、$O(n^2)$、$O(2^n)$ 和 $O(n!)$ 等。
|
||||
- 某些算法的时间复杂度非固定,而是与输入数据的分布有关。时间复杂度分为最差、最佳、平均时间复杂度,最佳时间复杂度几乎不用,因为输入数据一般需要满足严格条件才能达到最佳情况。
|
||||
- 平均时间复杂度反映算法在随机数据输入下的运行效率,最接近实际应用中的算法性能。计算平均时间复杂度需要统计输入数据分布以及综合后的数学期望。
|
||||
|
||||
|
@ -24,7 +24,7 @@ comments: true
|
|||
- 空间复杂度的作用类似于时间复杂度,用于衡量算法占用空间随数据量增长的趋势。
|
||||
- 算法运行过程中的相关内存空间可分为输入空间、暂存空间、输出空间。通常情况下,输入空间不计入空间复杂度计算。暂存空间可分为指令空间、数据空间、栈帧空间,其中栈帧空间通常仅在递归函数中影响空间复杂度。
|
||||
- 我们通常只关注最差空间复杂度,即统计算法在最差输入数据和最差运行时间点下的空间复杂度。
|
||||
- 常见空间复杂度从小到大排列有 $O(1)$、$O(\log n)$、$O(n)$、$O(n^2)$、$O(2^n)$ 等。
|
||||
- 常见空间复杂度从小到大排列有 $O(1)$、$O(\log n)$、$O(n)$、$O(n^2)$ 和 $O(2^n)$ 等。
|
||||
|
||||
## 2.5.1 Q & A
|
||||
|
||||
|
@ -42,7 +42,7 @@ comments: true
|
|||
- Java 和 C# 是面向对象的编程语言,代码块(方法)通常都是作为某个类的一部分。静态方法的行为类似于函数,因为它被绑定在类上,不能访问特定的实例变量。
|
||||
- C++ 和 Python 既支持过程式编程(函数),也支持面向对象编程(方法)。
|
||||
|
||||
!!! question "图“空间复杂度的常见类型”反映的是否是占用空间的绝对大小?"
|
||||
!!! question "图“常见的空间复杂度类型”反映的是否是占用空间的绝对大小?"
|
||||
|
||||
不是,该图片展示的是空间复杂度,其反映的是增长趋势,而不是占用空间的绝对大小。
|
||||
|
||||
|
|
|
@ -619,18 +619,11 @@ $T(n)$ 是一次函数,说明其运行时间的增长趋势是线性的,因
|
|||
|
||||
我们将线性阶的时间复杂度记为 $O(n)$ ,这个数学符号称为「大 $O$ 记号 big-$O$ notation」,表示函数 $T(n)$ 的「渐近上界 asymptotic upper bound」。
|
||||
|
||||
时间复杂度分析本质上是计算“操作数量函数 $T(n)$”的渐近上界。接下来,我们来看函数渐近上界的数学定义。
|
||||
时间复杂度分析本质上是计算“操作数量函数 $T(n)$”的渐近上界,其具有明确的数学定义。
|
||||
|
||||
!!! abstract "函数渐近上界"
|
||||
|
||||
若存在正实数 $c$ 和实数 $n_0$ ,使得对于所有的 $n > n_0$ ,均有
|
||||
$$
|
||||
T(n) \leq c \cdot f(n)
|
||||
$$
|
||||
则可认为 $f(n)$ 给出了 $T(n)$ 的一个渐近上界,记为
|
||||
$$
|
||||
T(n) = O(f(n))
|
||||
$$
|
||||
若存在正实数 $c$ 和实数 $n_0$ ,使得对于所有的 $n > n_0$ ,均有 $T(n) \leq c \cdot f(n)$ ,则可认为 $f(n)$ 给出了 $T(n)$ 的一个渐近上界,记为 $T(n) = O(f(n))$ 。
|
||||
|
||||
如图 2-8 所示,计算渐近上界就是寻找一个函数 $f(n)$ ,使得当 $n$ 趋向于无穷大时,$T(n)$ 和 $f(n)$ 处于相同的增长级别,仅相差一个常数项 $c$ 的倍数。
|
||||
|
||||
|
@ -650,17 +643,9 @@ $T(n)$ 是一次函数,说明其运行时间的增长趋势是线性的,因
|
|||
|
||||
1. **忽略 $T(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"
|
||||
|
||||
|
@ -875,6 +860,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}
|
||||
$$
|
||||
|
||||
### 2. 第二步:判断渐近上界
|
||||
|
||||
**时间复杂度由多项式 $T(n)$ 中最高阶的项来决定**。这是因为在 $n$ 趋于无穷大时,最高阶的项将发挥主导作用,其他项的影响都可以被忽略。
|
||||
|
@ -910,15 +905,11 @@ $$
|
|||
|
||||
<p align="center"> 图 2-9 常见的时间复杂度类型 </p>
|
||||
|
||||
!!! tip
|
||||
|
||||
部分示例代码需要一些预备知识,包括数组、递归等。如果你遇到不理解的部分,可以在学完后面章节后再回顾。现阶段,请先专注于理解时间复杂度的含义和推算方法。
|
||||
|
||||
### 1. 常数阶 $O(1)$
|
||||
|
||||
常数阶的操作数量与输入数据大小 $n$ 无关,即不随着 $n$ 的变化而变化。
|
||||
|
||||
对于以下算法,尽管操作数量 `size` 可能很大,但由于其与输入数据大小 $n$ 无关,因此时间复杂度仍为 $O(1)$ :
|
||||
在以下函数中,尽管操作数量 `size` 可能很大,但由于其与输入数据大小 $n$ 无关,因此时间复杂度仍为 $O(1)$ :
|
||||
|
||||
=== "Java"
|
||||
|
||||
|
@ -1407,7 +1398,7 @@ $$
|
|||
|
||||
### 3. 平方阶 $O(n^2)$
|
||||
|
||||
平方阶的操作数量相对于输入数据大小以平方级别增长。平方阶通常出现在嵌套循环中,外层循环和内层循环都为 $O(n)$ ,因此总体为 $O(n^2)$ :
|
||||
平方阶的操作数量相对于输入数据大小 $n$ 以平方级别增长。平方阶通常出现在嵌套循环中,外层循环和内层循环都为 $O(n)$ ,因此总体为 $O(n^2)$ :
|
||||
|
||||
=== "Java"
|
||||
|
||||
|
@ -2967,7 +2958,7 @@ $$
|
|||
|
||||
## 2.3.5 最差、最佳、平均时间复杂度
|
||||
|
||||
**算法的时间效率往往不是固定的,而是与输入数据的分布有关**。假设输入一个长度为 $n$ 的数组 `nums` ,其中 `nums` 由从 $1$ 至 $n$ 的数字组成,每个数字只出现一次,但元素顺序是随机打乱的,任务目标是返回元素 $1$ 的索引。我们可以得出以下结论。
|
||||
**算法的时间效率往往不是固定的,而是与输入数据的分布有关**。假设输入一个长度为 $n$ 的数组 `nums` ,其中 `nums` 由从 $1$ 至 $n$ 的数字组成,每个数字只出现一次;但元素顺序是随机打乱的,任务目标是返回元素 $1$ 的索引。我们可以得出以下结论。
|
||||
|
||||
- 当 `nums = [?, ?, ..., 1]` ,即当末尾元素是 $1$ 时,需要完整遍历数组,**达到最差时间复杂度 $O(n)$** 。
|
||||
- 当 `nums = [1, ?, ?, ...]` ,即当首个元素为 $1$ 时,无论数组多长都不需要继续遍历,**达到最佳时间复杂度 $\Omega(1)$** 。
|
||||
|
|
|
@ -45,7 +45,7 @@ comments: true
|
|||
|
||||
上述整理扑克牌的方法本质上是“插入排序”算法,它在处理小型数据集时非常高效。许多编程语言的排序库函数中都存在插入排序的身影。
|
||||
|
||||
**例三:货币找零**。假设我们在超市购买了 $69$ 元的商品,给收银员付了 $100$ 元,则收银员需要找我们 $31$ 元。他会很自然地完成图 1-3 所示的思考。
|
||||
**例三:货币找零**。假设我们在超市购买了 $69$ 元的商品,给了收银员 $100$ 元,则收银员需要找我们 $31$ 元。他会很自然地完成如图 1-3 所示的思考。
|
||||
|
||||
1. 可选项是比 $31$ 元面值更小的货币,包括 $1$ 元、$5$ 元、$10$ 元、$20$ 元。
|
||||
2. 从可选项中拿出最大的 $20$ 元,剩余 $31 - 20 = 11$ 元。
|
||||
|
@ -63,4 +63,4 @@ comments: true
|
|||
|
||||
!!! tip
|
||||
|
||||
如果你对数据结构、算法、数组和二分查找等概念仍感到一知半解,请不要担心,继续往下阅读,这本书将引导你迈入数据结构与算法的知识殿堂。
|
||||
如果你对数据结构、算法、数组和二分查找等概念仍感到一知半解,请继续往下阅读,这本书将引导你迈入数据结构与算法的知识殿堂。
|
||||
|
|
|
@ -27,7 +27,7 @@ comments: true
|
|||
|
||||
## 1.2.3 数据结构与算法的关系
|
||||
|
||||
数据结构与算法高度相关、紧密结合,具体表现在图 1-4 所示的几个方面。
|
||||
如图 1-4 所示,数据结构与算法高度相关、紧密结合,具体表现以下三个方面。
|
||||
|
||||
- 数据结构是算法的基石。数据结构为算法提供了结构化存储的数据,以及用于操作数据的方法。
|
||||
- 算法是数据结构发挥作用的舞台。数据结构本身仅存储数据信息,结合算法才能解决特定问题。
|
||||
|
|
Loading…
Reference in a new issue