This commit is contained in:
krahets
2023-12-02 06:24:05 +08:00
parent a4a23e2488
commit a7f5434009
93 changed files with 1463 additions and 1484 deletions

View File

@@ -12,9 +12,9 @@ comments: true
### 1.   for 循环
`for` 循环是最常见的迭代形式之一,**适合预先知道迭代次数时使用**。
`for` 循环是最常见的迭代形式之一,**适合预先知道迭代次数时使用**。
以下函数基于 `for` 循环实现了求和 $1 + 2 + \dots + n$ ,求和结果使用变量 `res` 记录。需要注意的是Python 中 `range(a, b)` 对应的区间是“左闭右开”的,对应的遍历范围为 $a, a + 1, \dots, b-1$
以下函数基于 `for` 循环实现了求和 $1 + 2 + \dots + n$ ,求和结果使用变量 `res` 记录。需要注意的是Python 中 `range(a, b)` 对应的区间是“左闭右开”的,对应的遍历范围为 $a, a + 1, \dots, b-1$
=== "Python"
@@ -182,7 +182,7 @@ comments: true
}
```
图 2-1 展示了该求和函数的流程框图。
图 2-1 该求和函数的流程框图。
![求和函数的流程框图](iteration_and_recursion.assets/iteration.png){ class="animation-figure" }
@@ -192,9 +192,9 @@ comments: true
### 2.   while 循环
与 `for` 循环类似,`while` 循环也是一种实现迭代的方法。在 `while` 循环中,程序每轮都会先检查条件,如果条件为真则继续执行,否则就结束循环。
与 `for` 循环类似,`while` 循环也是一种实现迭代的方法。在 `while` 循环中,程序每轮都会先检查条件,如果条件为真则继续执行,否则就结束循环。
下面我们用 `while` 循环来实现求和 $1 + 2 + \dots + n$
下面我们用 `while` 循环来实现求和 $1 + 2 + \dots + n$
=== "Python"
@@ -388,9 +388,9 @@ comments: true
}
```
**`while` 循环比 `for` 循环的自由度更高**。在 `while` 循环中,我们可以自由设计条件变量的初始化和更新步骤。
**`while` 循环比 `for` 循环的自由度更高**。在 `while` 循环中,我们可以自由设计条件变量的初始化和更新步骤。
例如在以下代码中,条件变量 $i$ 每轮进行两次更新,这种情况就不太方便用 `for` 循环实现
例如在以下代码中,条件变量 $i$ 每轮进行两次更新,这种情况就不太方便用 `for` 循环实现
=== "Python"
@@ -399,7 +399,7 @@ comments: true
"""while 循环(两次更新)"""
res = 0
i = 1 # 初始化条件变量
# 循环求和 1, 4, ...
# 循环求和 1, 4, 10, ...
while i <= n:
res += i
# 更新条件变量
@@ -415,7 +415,7 @@ comments: true
int whileLoopII(int n) {
int res = 0;
int i = 1; // 初始化条件变量
// 循环求和 1, 4, ...
// 循环求和 1, 4, 10, ...
while (i <= n) {
res += i;
// 更新条件变量
@@ -433,7 +433,7 @@ comments: true
int whileLoopII(int n) {
int res = 0;
int i = 1; // 初始化条件变量
// 循环求和 1, 4, ...
// 循环求和 1, 4, 10, ...
while (i <= n) {
res += i;
// 更新条件变量
@@ -470,7 +470,7 @@ comments: true
res := 0
// 初始化条件变量
i := 1
// 循环求和 1, 4, ...
// 循环求和 1, 4, 10, ...
for i <= n {
res += i
// 更新条件变量
@@ -488,7 +488,7 @@ comments: true
func whileLoopII(n: Int) -> Int {
var res = 0
var i = 1 // 初始化条件变量
// 循环求和 1, 4, ...
// 循环求和 1, 4, 10, ...
while i <= n {
res += i
// 更新条件变量
@@ -506,7 +506,7 @@ comments: true
function whileLoopII(n) {
let res = 0;
let i = 1; // 初始化条件变量
// 循环求和 1, 4, ...
// 循环求和 1, 4, 10, ...
while (i <= n) {
res += i;
// 更新条件变量
@@ -524,7 +524,7 @@ comments: true
function whileLoopII(n: number): number {
let res = 0;
let i = 1; // 初始化条件变量
// 循环求和 1, 4, ...
// 循环求和 1, 4, 10, ...
while (i <= n) {
res += i;
// 更新条件变量
@@ -542,7 +542,7 @@ comments: true
int whileLoopII(int n) {
int res = 0;
int i = 1; // 初始化条件变量
// 循环求和 1, 4, ...
// 循环求和 1, 4, 10, ...
while (i <= n) {
res += i;
// 更新条件变量
@@ -560,7 +560,7 @@ comments: true
fn while_loop_ii(n: i32) -> i32 {
let mut res = 0;
let mut i = 1; // 初始化条件变量
// 循环求和 1, 4, ...
// 循环求和 1, 4, 10, ...
while i <= n {
res += i;
// 更新条件变量
@@ -578,7 +578,7 @@ comments: true
int whileLoopII(int n) {
int res = 0;
int i = 1; // 初始化条件变量
// 循环求和 1, 4, ...
// 循环求和 1, 4, 10, ...
while (i <= n) {
res += i;
// 更新条件变量
@@ -596,7 +596,7 @@ comments: true
fn whileLoopII(n: i32) i32 {
var res: i32 = 0;
var i: i32 = 1; // 初始化条件变量
// 循环求和 1, 4, ...
// 循环求和 1, 4, 10, ...
while (i <= n) {
res += @intCast(i);
// 更新条件变量
@@ -611,7 +611,7 @@ comments: true
### 3. &nbsp; 嵌套循环
我们可以在一个循环结构内嵌套另一个循环结构,以 `for` 循环为例:
我们可以在一个循环结构内嵌套另一个循环结构,下面以 `for` 循环为例:
=== "Python"
@@ -821,7 +821,7 @@ comments: true
}
```
图 2-2 给出了该嵌套循环的流程框图。
图 2-2 该嵌套循环的流程框图。
![嵌套循环的流程框图](iteration_and_recursion.assets/nested_iteration.png){ class="animation-figure" }
@@ -829,7 +829,7 @@ comments: true
在这种情况下,函数的操作数量与 $n^2$ 成正比,或者说算法运行时间和输入数据大小 $n$ 成“平方关系”。
我们可以继续添加嵌套循环,每一次嵌套都是一次“升维”,将会使时间复杂度提高至“立方关系”“四次方关系”以此类推。
我们可以继续添加嵌套循环,每一次嵌套都是一次“升维”,将会使时间复杂度提高至“立方关系”“四次方关系”以此类推。
## 2.2.2 &nbsp; 递归
@@ -1037,7 +1037,7 @@ comments: true
- **迭代**:“自下而上”地解决问题。从最基础的步骤开始,然后不断重复或累加这些步骤,直到任务完成。
- **递归**:“自上而下”地解决问题。将原问题分解为更小的子问题,这些子问题和原问题具有相同的形式。接下来将子问题继续分解为更小的子问题,直到基本情况时停止(基本情况的解是已知的)。
以上述求和函数为例,设问题 $f(n) = 1 + 2 + \dots + n$ 。
以上述求和函数为例,设问题 $f(n) = 1 + 2 + \dots + n$ 。
- **迭代**:在循环中模拟求和过程,从 $1$ 遍历到 $n$ ,每轮执行求和操作,即可求得 $f(n)$ 。
- **递归**:将问题分解为子问题 $f(n) = n + f(n-1)$ ,不断(递归地)分解下去,直至基本情况 $f(1) = 1$ 时终止。
@@ -1055,16 +1055,16 @@ comments: true
<p align="center"> 图 2-4 &nbsp; 递归调用深度 </p>
在实际中,编程语言允许的递归深度通常是有限的,过深的递归可能导致栈溢出错。
在实际中,编程语言允许的递归深度通常是有限的,过深的递归可能导致栈溢出错
### 2. &nbsp; 尾递归
有趣的是,**如果函数在返回前的最后一步才进行递归调用**,则该函数可以被编译器或解释器优化,使其在空间效率上与迭代相当。这种情况被称为「尾递归 tail recursion」。
- **普通递归**:当函数返回到上一层级的函数后,需要继续执行代码,因此系统需要保存上一层调用的上下文。
- **尾递归**:递归调用是函数返回前的最后一个操作,这意味着函数返回到上一层级后,无继续执行其他操作,因此系统无保存上一层函数的上下文。
- **尾递归**:递归调用是函数返回前的最后一个操作,这意味着函数返回到上一层级后,无继续执行其他操作,因此系统无保存上一层函数的上下文。
以计算 $1 + 2 + \dots + n$ 为例,我们可以将结果变量 `res` 设为函数参数,从而实现尾递归
以计算 $1 + 2 + \dots + n$ 为例,我们可以将结果变量 `res` 设为函数参数,从而实现尾递归
=== "Python"
@@ -1222,7 +1222,7 @@ comments: true
}
```
尾递归的执行过程如图 2-5 所示。对比普通递归和尾递归,求和操作的执行点是不同的。
尾递归的执行过程如图 2-5 所示。对比普通递归和尾递归,两者的求和操作的执行点是不同的。
- **普通递归**:求和操作是在“归”的过程中执行的,每层返回后都要再执行一次求和操作。
- **尾递归**:求和操作是在“递”的过程中执行的,“归”的过程只需层层返回。
@@ -1233,7 +1233,7 @@ comments: true
!!! tip
请注意许多编译器或解释器并不支持尾递归优化。例如Python 默认不支持尾递归优化,因此即使函数是尾递归形式,仍然可能会遇到栈溢出问题。
请注意许多编译器或解释器并不支持尾递归优化。例如Python 默认不支持尾递归优化,因此即使函数是尾递归形式,仍然可能会遇到栈溢出问题。
### 3. &nbsp; 递归树
@@ -1248,7 +1248,7 @@ comments: true
- 数列的前两个数字为 $f(1) = 0$ 和 $f(2) = 1$ 。
- 数列中的每个数字是前两个数字的和,即 $f(n) = f(n - 1) + f(n - 2)$ 。
按照递推关系进行递归调用,将前两个数字作为终止条件,便可写出递归代码。调用 `fib(n)` 即可得到斐波那契数列的第 $n$ 个数字
按照递推关系进行递归调用,将前两个数字作为终止条件,便可写出递归代码。调用 `fib(n)` 即可得到斐波那契数列的第 $n$ 个数字
=== "Python"
@@ -1430,15 +1430,15 @@ comments: true
}
```
观察以上代码,我们在函数内递归调用了两个函数,**这意味着从一个调用产生了两个调用分支**。如图 2-6 所示,这样不断递归调用下去,最终将产生一层数为 $n$ 的「递归树 recursion tree」。
观察以上代码,我们在函数内递归调用了两个函数,**这意味着从一个调用产生了两个调用分支**。如图 2-6 所示,这样不断递归调用下去,最终将产生一层数为 $n$ 的「递归树 recursion tree」。
![斐波那契数列的递归树](iteration_and_recursion.assets/recursion_tree.png){ class="animation-figure" }
<p align="center"> 图 2-6 &nbsp; 斐波那契数列的递归树 </p>
本质上看,递归体现“将问题分解为更小子问题”的思维范式,这种分治策略至关重要
本质上看,递归体现“将问题分解为更小子问题”的思维范式,这种分治策略至关重要。
- 从算法角度看,搜索、排序、回溯、分治、动态规划等许多重要算法策略直接或间接地应用这种思维方式。
- 从算法角度看,搜索、排序、回溯、分治、动态规划等许多重要算法策略直接或间接地应用这种思维方式。
- 从数据结构角度看,递归天然适合处理链表、树和图的相关问题,因为它们非常适合用分治思想进行分析。
## 2.2.3 &nbsp; 两者对比