Move docs/* to docs/zh/*

This commit is contained in:
krahets
2023-10-08 01:33:09 +08:00
parent 400b3914f6
commit 6f7e768cb7
591 changed files with 0 additions and 0 deletions

View File

@ -1,13 +0,0 @@
# 复杂度分析
<div class="center-table" markdown>
![复杂度分析](../assets/covers/chapter_complexity_analysis.jpg){ width="600" }
</div>
!!! abstract
复杂度分析犹如浩瀚的算法宇宙中的时空向导。
它带领我们在时间与空间这两个维度上深入探索,寻找更优雅的解决方案。

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

View File

@ -1,738 +0,0 @@
# 迭代与递归
在数据结构与算法中,重复执行某个任务是很常见的,其与算法的复杂度密切相关。而要重复执行某个任务,我们通常会选用两种基本的程序结构:迭代和递归。
## 迭代
「迭代 iteration」是一种重复执行某个任务的控制结构。在迭代中程序会在满足一定的条件下重复执行某段代码直到这个条件不再满足。
### for 循环
`for` 循环是最常见的迭代形式之一,**适合预先知道迭代次数时使用**。
以下函数基于 `for` 循环实现了求和 $1 + 2 + \dots + n$ ,求和结果使用变量 `res` 记录。需要注意的是Python 中 `range(a, b)` 对应的区间是“左闭右开”的,对应的遍历范围为 $a, a + 1, \dots, b-1$ 。
=== "Python"
```python title="iteration.py"
[class]{}-[func]{for_loop}
```
=== "C++"
```cpp title="iteration.cpp"
[class]{}-[func]{forLoop}
```
=== "Java"
```java title="iteration.java"
[class]{iteration}-[func]{forLoop}
```
=== "C#"
```csharp title="iteration.cs"
[class]{iteration}-[func]{forLoop}
```
=== "Go"
```go title="iteration.go"
[class]{}-[func]{forLoop}
```
=== "Swift"
```swift title="iteration.swift"
[class]{}-[func]{forLoop}
```
=== "JS"
```javascript title="iteration.js"
[class]{}-[func]{forLoop}
```
=== "TS"
```typescript title="iteration.ts"
[class]{}-[func]{forLoop}
```
=== "Dart"
```dart title="iteration.dart"
[class]{}-[func]{forLoop}
```
=== "Rust"
```rust title="iteration.rs"
[class]{}-[func]{for_loop}
```
=== "C"
```c title="iteration.c"
[class]{}-[func]{forLoop}
```
=== "Zig"
```zig title="iteration.zig"
[class]{}-[func]{forLoop}
```
下图展示了该求和函数的流程框图。
![求和函数的流程框图](iteration_and_recursion.assets/iteration.png)
此求和函数的操作数量与输入数据大小 $n$ 成正比,或者说成“线性关系”。实际上,**时间复杂度描述的就是这个“线性关系”**。相关内容将会在下一节中详细介绍。
### while 循环
与 `for` 循环类似,`while` 循环也是一种实现迭代的方法。在 `while` 循环中,程序每轮都会先检查条件,如果条件为真则继续执行,否则就结束循环。
下面,我们用 `while` 循环来实现求和 $1 + 2 + \dots + n$ 。
=== "Python"
```python title="iteration.py"
[class]{}-[func]{while_loop}
```
=== "C++"
```cpp title="iteration.cpp"
[class]{}-[func]{whileLoop}
```
=== "Java"
```java title="iteration.java"
[class]{iteration}-[func]{whileLoop}
```
=== "C#"
```csharp title="iteration.cs"
[class]{iteration}-[func]{whileLoop}
```
=== "Go"
```go title="iteration.go"
[class]{}-[func]{whileLoop}
```
=== "Swift"
```swift title="iteration.swift"
[class]{}-[func]{whileLoop}
```
=== "JS"
```javascript title="iteration.js"
[class]{}-[func]{whileLoop}
```
=== "TS"
```typescript title="iteration.ts"
[class]{}-[func]{whileLoop}
```
=== "Dart"
```dart title="iteration.dart"
[class]{}-[func]{whileLoop}
```
=== "Rust"
```rust title="iteration.rs"
[class]{}-[func]{while_loop}
```
=== "C"
```c title="iteration.c"
[class]{}-[func]{whileLoop}
```
=== "Zig"
```zig title="iteration.zig"
[class]{}-[func]{whileLoop}
```
在 `while` 循环中,由于初始化和更新条件变量的步骤是独立在循环结构之外的,**因此它比 `for` 循环的自由度更高**。
例如在以下代码中,条件变量 $i$ 每轮进行了两次更新,这种情况就不太方便用 `for` 循环实现。
=== "Python"
```python title="iteration.py"
[class]{}-[func]{while_loop_ii}
```
=== "C++"
```cpp title="iteration.cpp"
[class]{}-[func]{whileLoopII}
```
=== "Java"
```java title="iteration.java"
[class]{iteration}-[func]{whileLoopII}
```
=== "C#"
```csharp title="iteration.cs"
[class]{iteration}-[func]{whileLoopII}
```
=== "Go"
```go title="iteration.go"
[class]{}-[func]{whileLoopII}
```
=== "Swift"
```swift title="iteration.swift"
[class]{}-[func]{whileLoopII}
```
=== "JS"
```javascript title="iteration.js"
[class]{}-[func]{whileLoopII}
```
=== "TS"
```typescript title="iteration.ts"
[class]{}-[func]{whileLoopII}
```
=== "Dart"
```dart title="iteration.dart"
[class]{}-[func]{whileLoopII}
```
=== "Rust"
```rust title="iteration.rs"
[class]{}-[func]{while_loop_ii}
```
=== "C"
```c title="iteration.c"
[class]{}-[func]{whileLoopII}
```
=== "Zig"
```zig title="iteration.zig"
[class]{}-[func]{whileLoopII}
```
总的来说,**`for` 循环的代码更加紧凑,`while` 循环更加灵活**,两者都可以实现迭代结构。选择使用哪一个应该根据特定问题的需求来决定。
### 嵌套循环
我们可以在一个循环结构内嵌套另一个循环结构,以 `for` 循环为例:
=== "Python"
```python title="iteration.py"
[class]{}-[func]{nested_for_loop}
```
=== "C++"
```cpp title="iteration.cpp"
[class]{}-[func]{nestedForLoop}
```
=== "Java"
```java title="iteration.java"
[class]{iteration}-[func]{nestedForLoop}
```
=== "C#"
```csharp title="iteration.cs"
[class]{iteration}-[func]{nestedForLoop}
```
=== "Go"
```go title="iteration.go"
[class]{}-[func]{nestedForLoop}
```
=== "Swift"
```swift title="iteration.swift"
[class]{}-[func]{nestedForLoop}
```
=== "JS"
```javascript title="iteration.js"
[class]{}-[func]{nestedForLoop}
```
=== "TS"
```typescript title="iteration.ts"
[class]{}-[func]{nestedForLoop}
```
=== "Dart"
```dart title="iteration.dart"
[class]{}-[func]{nestedForLoop}
```
=== "Rust"
```rust title="iteration.rs"
[class]{}-[func]{nested_for_loop}
```
=== "C"
```c title="iteration.c"
[class]{}-[func]{nestedForLoop}
```
=== "Zig"
```zig title="iteration.zig"
[class]{}-[func]{nestedForLoop}
```
下图给出了该嵌套循环的流程框图。
![嵌套循环的流程框图](iteration_and_recursion.assets/nested_iteration.png)
在这种情况下,函数的操作数量与 $n^2$ 成正比,或者说算法运行时间和输入数据大小 $n$ 成“平方关系”。
我们可以继续添加嵌套循环,每一次嵌套都是一次“升维”,将会使时间复杂度提高至“立方关系”、“四次方关系”、以此类推。
## 递归
「递归 recursion」是一种算法策略通过函数调用自身来解决问题。它主要包含两个阶段。
1. **递**:程序不断深入地调用自身,通常传入更小或更简化的参数,直到达到“终止条件”。
2. **归**:触发“终止条件”后,程序从最深层的递归函数开始逐层返回,汇聚每一层的结果。
而从实现的角度看,递归代码主要包含三个要素。
1. **终止条件**:用于决定什么时候由“递”转“归”。
2. **递归调用**:对应“递”,函数调用自身,通常输入更小或更简化的参数。
3. **返回结果**:对应“归”,将当前递归层级的结果返回至上一层。
观察以下代码,我们只需调用函数 `recur(n)` ,就可以完成 $1 + 2 + \dots + n$ 的计算:
=== "Python"
```python title="recursion.py"
[class]{}-[func]{recur}
```
=== "C++"
```cpp title="recursion.cpp"
[class]{}-[func]{recur}
```
=== "Java"
```java title="recursion.java"
[class]{recursion}-[func]{recur}
```
=== "C#"
```csharp title="recursion.cs"
[class]{recursion}-[func]{recur}
```
=== "Go"
```go title="recursion.go"
[class]{}-[func]{recur}
```
=== "Swift"
```swift title="recursion.swift"
[class]{}-[func]{recur}
```
=== "JS"
```javascript title="recursion.js"
[class]{}-[func]{recur}
```
=== "TS"
```typescript title="recursion.ts"
[class]{}-[func]{recur}
```
=== "Dart"
```dart title="recursion.dart"
[class]{}-[func]{recur}
```
=== "Rust"
```rust title="recursion.rs"
[class]{}-[func]{recur}
```
=== "C"
```c title="recursion.c"
[class]{}-[func]{recur}
```
=== "Zig"
```zig title="recursion.zig"
[class]{}-[func]{recur}
```
下图展示了该函数的递归过程。
![求和函数的递归过程](iteration_and_recursion.assets/recursion_sum.png)
虽然从计算角度看,迭代与递归可以得到相同的结果,**但它们代表了两种完全不同的思考和解决问题的范式**。
- **迭代**:“自下而上”地解决问题。从最基础的步骤开始,然后不断重复或累加这些步骤,直到任务完成。
- **递归**:“自上而下”地解决问题。将原问题分解为更小的子问题,这些子问题和原问题具有相同的形式。接下来将子问题继续分解为更小的子问题,直到基本情况时停止(基本情况的解是已知的)。
以上述的求和函数为例,设问题 $f(n) = 1 + 2 + \dots + n$ 。
- **迭代**:在循环中模拟求和过程,从 $1$ 遍历到 $n$ ,每轮执行求和操作,即可求得 $f(n)$ 。
- **递归**:将问题分解为子问题 $f(n) = n + f(n-1)$ ,不断(递归地)分解下去,直至基本情况 $f(1) = 1$ 时终止。
### 调用栈
递归函数每次调用自身时,系统都会为新开启的函数分配内存,以存储局部变量、调用地址和其他信息等。这将导致两方面的结果。
- 函数的上下文数据都存储在称为“栈帧空间”的内存区域中,直至函数返回后才会被释放。因此,**递归通常比迭代更加耗费内存空间**。
- 递归调用函数会产生额外的开销。**因此递归通常比循环的时间效率更低**。
如下图所示,在触发终止条件前,同时存在 $n$ 个未返回的递归函数,**递归深度为 $n$** 。
![递归调用深度](iteration_and_recursion.assets/recursion_sum_depth.png)
在实际中,编程语言允许的递归深度通常是有限的,过深的递归可能导致栈溢出报错。
### 尾递归
有趣的是,**如果函数在返回前的最后一步才进行递归调用**,则该函数可以被编译器或解释器优化,使其在空间效率上与迭代相当。这种情况被称为「尾递归 tail recursion」。
- **普通递归**:当函数返回到上一层级的函数后,需要继续执行代码,因此系统需要保存上一层调用的上下文。
- **尾递归**:递归调用是函数返回前的最后一个操作,这意味着函数返回到上一层级后,无需继续执行其他操作,因此系统无需保存上一层函数的上下文。
以计算 $1 + 2 + \dots + n$ 为例,我们可以将结果变量 `res` 设为函数参数,从而实现尾递归。
=== "Python"
```python title="recursion.py"
[class]{}-[func]{tail_recur}
```
=== "C++"
```cpp title="recursion.cpp"
[class]{}-[func]{tailRecur}
```
=== "Java"
```java title="recursion.java"
[class]{recursion}-[func]{tailRecur}
```
=== "C#"
```csharp title="recursion.cs"
[class]{recursion}-[func]{tailRecur}
```
=== "Go"
```go title="recursion.go"
[class]{}-[func]{tailRecur}
```
=== "Swift"
```swift title="recursion.swift"
[class]{}-[func]{tailRecur}
```
=== "JS"
```javascript title="recursion.js"
[class]{}-[func]{tailRecur}
```
=== "TS"
```typescript title="recursion.ts"
[class]{}-[func]{tailRecur}
```
=== "Dart"
```dart title="recursion.dart"
[class]{}-[func]{tailRecur}
```
=== "Rust"
```rust title="recursion.rs"
[class]{}-[func]{tail_recur}
```
=== "C"
```c title="recursion.c"
[class]{}-[func]{tailRecur}
```
=== "Zig"
```zig title="recursion.zig"
[class]{}-[func]{tailRecur}
```
尾递归的执行过程如下图所示。对比普通递归和尾递归,求和操作的执行点是不同的。
- **普通递归**:求和操作是在“归”的过程中执行的,每层返回后都要再执行一次求和操作。
- **尾递归**:求和操作是在“递”的过程中执行的,“归”的过程只需层层返回。
![尾递归过程](iteration_and_recursion.assets/tail_recursion_sum.png)
!!! tip
请注意许多编译器或解释器并不支持尾递归优化。例如Python 默认不支持尾递归优化,因此即使函数是尾递归形式,但仍然可能会遇到栈溢出问题。
### 递归树
当处理与“分治”相关的算法问题时,递归往往比迭代的思路更加直观、代码更加易读。以“斐波那契数列”为例。
!!! question
给定一个斐波那契数列 $0, 1, 1, 2, 3, 5, 8, 13, \dots$ ,求该数列的第 $n$ 个数字。
设斐波那契数列的第 $n$ 个数字为 $f(n)$ ,易得两个结论。
- 数列的前两个数字为 $f(1) = 0$ 和 $f(2) = 1$ 。
- 数列中的每个数字是前两个数字的和,即 $f(n) = f(n - 1) + f(n - 2)$ 。
按照递推关系进行递归调用,将前两个数字作为终止条件,便可写出递归代码。调用 `fib(n)` 即可得到斐波那契数列的第 $n$ 个数字。
=== "Python"
```python title="recursion.py"
[class]{}-[func]{fib}
```
=== "C++"
```cpp title="recursion.cpp"
[class]{}-[func]{fib}
```
=== "Java"
```java title="recursion.java"
[class]{recursion}-[func]{fib}
```
=== "C#"
```csharp title="recursion.cs"
[class]{recursion}-[func]{fib}
```
=== "Go"
```go title="recursion.go"
[class]{}-[func]{fib}
```
=== "Swift"
```swift title="recursion.swift"
[class]{}-[func]{fib}
```
=== "JS"
```javascript title="recursion.js"
[class]{}-[func]{fib}
```
=== "TS"
```typescript title="recursion.ts"
[class]{}-[func]{fib}
```
=== "Dart"
```dart title="recursion.dart"
[class]{}-[func]{fib}
```
=== "Rust"
```rust title="recursion.rs"
[class]{}-[func]{fib}
```
=== "C"
```c title="recursion.c"
[class]{}-[func]{fib}
```
=== "Zig"
```zig title="recursion.zig"
[class]{}-[func]{fib}
```
观察以上代码,我们在函数内递归调用了两个函数,**这意味着从一个调用产生了两个调用分支**。如下图所示,这样不断递归调用下去,最终将产生一个层数为 $n$ 的「递归树 recursion tree」。
![斐波那契数列的递归树](iteration_and_recursion.assets/recursion_tree.png)
本质上看,递归体现“将问题分解为更小子问题”的思维范式,这种分治策略是至关重要的。
- 从算法角度看,搜索、排序、回溯、分治、动态规划等许多重要算法策略都直接或间接地应用这种思维方式。
- 从数据结构角度看,递归天然适合处理链表、树和图的相关问题,因为它们非常适合用分治思想进行分析。
## 两者对比
总结以上内容,如下表所示,迭代和递归在实现、性能和适用性上有所不同。
<p align="center"> 表 <id> &nbsp; 迭代与递归特点对比 </p>
| | 迭代 | 递归 |
| -------- | -------------------------------------- | ------------------------------------------------------------ |
| 实现方式 | 循环结构 | 函数调用自身 |
| 时间效率 | 效率通常较高,无函数调用开销 | 每次函数调用都会产生开销 |
| 内存使用 | 通常使用固定大小的内存空间 | 累积函数调用可能使用大量的栈帧空间 |
| 适用问题 | 适用于简单循环任务,代码直观、可读性好 | 适用于子问题分解,如树、图、分治、回溯等,代码结构简洁、清晰 |
!!! tip
如果感觉以下内容理解困难,可以在读完“栈”章节后再来复习。
那么,迭代和递归具有什么内在联系呢?以上述的递归函数为例,求和操作在递归的“归”阶段进行。这意味着最初被调用的函数实际上是最后完成其求和操作的,**这种工作机制与栈的“先入后出”原则是异曲同工的**。
事实上,“调用栈”和“栈帧空间”这类递归术语已经暗示了递归与栈之间的密切关系。
1. **递**:当函数被调用时,系统会在“调用栈”上为该函数分配新的栈帧,用于存储函数的局部变量、参数、返回地址等数据。
2. **归**:当函数完成执行并返回时,对应的栈帧会从“调用栈”上被移除,恢复之前函数的执行环境。
因此,**我们可以使用一个显式的栈来模拟调用栈的行为**,从而将递归转化为迭代形式:
=== "Python"
```python title="recursion.py"
[class]{}-[func]{for_loop_recur}
```
=== "C++"
```cpp title="recursion.cpp"
[class]{}-[func]{forLoopRecur}
```
=== "Java"
```java title="recursion.java"
[class]{recursion}-[func]{forLoopRecur}
```
=== "C#"
```csharp title="recursion.cs"
[class]{recursion}-[func]{forLoopRecur}
```
=== "Go"
```go title="recursion.go"
[class]{}-[func]{forLoopRecur}
```
=== "Swift"
```swift title="recursion.swift"
[class]{}-[func]{forLoopRecur}
```
=== "JS"
```javascript title="recursion.js"
[class]{}-[func]{forLoopRecur}
```
=== "TS"
```typescript title="recursion.ts"
[class]{}-[func]{forLoopRecur}
```
=== "Dart"
```dart title="recursion.dart"
[class]{}-[func]{forLoopRecur}
```
=== "Rust"
```rust title="recursion.rs"
[class]{}-[func]{for_loop_recur}
```
=== "C"
```c title="recursion.c"
[class]{}-[func]{forLoopRecur}
```
=== "Zig"
```zig title="recursion.zig"
[class]{}-[func]{forLoopRecur}
```
观察以上代码,当递归被转换为迭代后,代码变得更加复杂了。尽管迭代和递归在很多情况下可以互相转换,但也不一定值得这样做,有以下两点原因。
- 转化后的代码可能更加难以理解,可读性更差。
- 对于某些复杂问题,模拟系统调用栈的行为可能非常困难。
总之,**选择迭代还是递归取决于特定问题的性质**。在编程实践中,权衡两者的优劣并根据情境选择合适的方法是至关重要的。

View File

@ -1,48 +0,0 @@
# 算法效率评估
在算法设计中,我们先后追求以下两个层面的目标。
1. **找到问题解法**:算法需要在规定的输入范围内,可靠地求得问题的正确解。
2. **寻求最优解法**:同一个问题可能存在多种解法,我们希望找到尽可能高效的算法。
也就是说,在能够解决问题的前提下,算法效率已成为衡量算法优劣的主要评价指标,它包括以下两个维度。
- **时间效率**:算法运行速度的快慢。
- **空间效率**:算法占用内存空间的大小。
简而言之,**我们的目标是设计“既快又省”的数据结构与算法**。而有效地评估算法效率至关重要,因为只有这样我们才能将各种算法进行对比,从而指导算法设计与优化过程。
效率评估方法主要分为两种:实际测试、理论估算。
## 实际测试
假设我们现在有算法 `A` 和算法 `B` ,它们都能解决同一问题,现在需要对比这两个算法的效率。最直接的方法是找一台计算机,运行这两个算法,并监控记录它们的运行时间和内存占用情况。这种评估方式能够反映真实情况,但也存在较大局限性。
一方面,**难以排除测试环境的干扰因素**。硬件配置会影响算法的性能表现。比如在某台计算机中,算法 `A` 的运行时间比算法 `B` 短;但在另一台配置不同的计算机中,我们可能得到相反的测试结果。这意味着我们需要在各种机器上进行测试,统计平均效率,而这是不现实的。
另一方面,**展开完整测试非常耗费资源**。随着输入数据量的变化,算法会表现出不同的效率。例如,在输入数据量较小时,算法 `A` 的运行时间比算法 `B` 更少;而输入数据量较大时,测试结果可能恰恰相反。因此,为了得到有说服力的结论,我们需要测试各种规模的输入数据,而这需要耗费大量的计算资源。
## 理论估算
由于实际测试具有较大的局限性,我们可以考虑仅通过一些计算来评估算法的效率。这种估算方法被称为「渐近复杂度分析 asymptotic complexity analysis」简称「复杂度分析」。
复杂度分析体现算法运行所需的时间(空间)资源与输入数据大小之间的关系。**它描述了随着输入数据大小的增加,算法执行所需时间和空间的增长趋势**。这个定义有些拗口,我们可以将其分为三个重点来理解。
- “时间和空间资源”分别对应「时间复杂度 time complexity」和「空间复杂度 space complexity」。
- “随着输入数据大小的增加”意味着复杂度反映了算法运行效率与输入数据体量之间的关系。
- “时间和空间的增长趋势”表示复杂度分析关注的不是运行时间或占用空间的具体值,而是时间或空间增长的“快慢”。
**复杂度分析克服了实际测试方法的弊端**,体现在以下两个方面。
- 它独立于测试环境,分析结果适用于所有运行平台。
- 它可以体现不同数据量下的算法效率,尤其是在大数据量下的算法性能。
!!! tip
如果你仍对复杂度的概念感到困惑,无须担心,我们会在后续章节中详细介绍。
复杂度分析为我们提供了一把评估算法效率的“标尺”,使我们可以衡量执行某个算法所需的时间和空间资源,对比不同算法之间的效率。
复杂度是个数学概念,对于初学者可能比较抽象,学习难度相对较高。从这个角度看,复杂度分析可能不太适合作为最先介绍的内容。然而,当我们讨论某个数据结构或算法的特点时,难以避免要分析其运行速度和空间使用情况。
综上所述,建议你在深入学习数据结构与算法之前,**先对复杂度分析建立初步的了解,以便能够完成简单算法的复杂度分析**。

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

File diff suppressed because it is too large Load Diff

View File

@ -1,49 +0,0 @@
# 小结
### 重点回顾
**算法效率评估**
- 时间效率和空间效率是衡量算法优劣的两个主要评价指标。
- 我们可以通过实际测试来评估算法效率,但难以消除测试环境的影响,且会耗费大量计算资源。
- 复杂度分析可以克服实际测试的弊端,分析结果适用于所有运行平台,并且能够揭示算法在不同数据规模下的效率。
**时间复杂度**
- 时间复杂度用于衡量算法运行时间随数据量增长的趋势,可以有效评估算法效率,但在某些情况下可能失效,如在输入的数据量较小或时间复杂度相同时,无法精确对比算法效率的优劣。
- 最差时间复杂度使用大 $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^2)$ 和 $O(2^n)$ 等。
### Q & A
!!! question "尾递归的空间复杂度是 $O(1)$ 吗?"
理论上,尾递归函数的空间复杂度可以被优化至 $O(1)$ 。不过绝大多数编程语言(例如 Java、Python、C++、Go、C# 等)都不支持自动优化尾递归,因此通常认为空间复杂度是 $O(n)$ 。
!!! question "函数和方法这两个术语的区别是什么?"
函数function可以被独立执行所有参数都以显式传递。方法method与一个对象关联被隐式传递给调用它的对象能够对类的实例中包含的数据进行操作。
下面以几个常见的编程语言来说明。
- C 语言是过程式编程语言没有面向对象的概念所以只有函数。但我们可以通过创建结构体struct来模拟面向对象编程与结构体相关联的函数就相当于其他语言中的方法。
- Java 和 C# 是面向对象的编程语言,代码块(方法)通常都是作为某个类的一部分。静态方法的行为类似于函数,因为它被绑定在类上,不能访问特定的实例变量。
- C++ 和 Python 既支持过程式编程(函数),也支持面向对象编程(方法)。
!!! question "图“常见的空间复杂度类型”反映的是否是占用空间的绝对大小?"
不是,该图片展示的是空间复杂度,其反映的是增长趋势,而不是占用空间的绝对大小。
假设取 $n = 8$ ,你可能会发现每条曲线的值与函数对应不上。这是因为每条曲线都包含一个常数项,用于将取值范围压缩到一个视觉舒适的范围内。
在实际中,因为我们通常不知道每个方法的“常数项”复杂度是多少,所以一般无法仅凭复杂度来选择 $n = 8$ 之下的最优解法。但对于 $n = 8^5$ 就很好选了,这时增长趋势已经占主导了。

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

File diff suppressed because it is too large Load Diff