214 lines
7.4 KiB
Markdown
214 lines
7.4 KiB
Markdown
---
|
||
tags:
|
||
- 算法设计与分析
|
||
- 复习
|
||
- 复杂度分析
|
||
create time: 2026-06-10 11:11
|
||
---
|
||
|
||
# 算法基础与复杂度分析
|
||
|
||
## 概述
|
||
本文系统梳理算法的基本概念、四个特性、算法三要素、时间复杂度的计算与排序,以及递归与迭代的关系。这是算法课程的基石,掌握这些内容才能顺利理解后续的分治法、贪心算法、动态规划等高级策略。
|
||
|
||
## 正文
|
||
|
||
### 一、算法的定义与四个特性
|
||
|
||
算法(Algorithm)是对特定问题求解步骤的一种描述,它是指令的有限序列。每个指令表示一个或多个操作。
|
||
|
||
一个合法的算法必须满足以下**四个特性**:
|
||
|
||
| 特性 | 含义 | 反例 |
|
||
|------|------|------|
|
||
| **有穷性** | 算法必须在执行有限步之后终止 | 死循环 |
|
||
| **确定性** | 每条指令的含义明确、无二义性 | 模糊的自然语言描述 |
|
||
| **可行性** | 每条指令都是可执行的基本操作 | 无法实现的操作 |
|
||
| **输入/输出** | 有零个或多个输入,有一个或多个输出 | 无输出的程序 |
|
||
|
||
> [!question] 为什么算法可以没有输入,但不能没有输出?
|
||
> 因为算法的目的是解决问题并给出结果。没有输入意味着算法不依赖外部数据(如计算 π 的某一位),但无论如何必须有输出才能体现其价值。
|
||
|
||
### 二、算法三要素
|
||
|
||
算法的三个核心要素是:**运算**、**控制结构**和**数据结构**。
|
||
|
||
> [!warning] 常见误区
|
||
> 很多同学把"输入/输出"当作算法三要素之一,这是错误的。算法三要素是运算、控制结构和数据结构。
|
||
|
||
- **运算**:算法中执行的每一种操作
|
||
- **控制结构**:三种基本结构——**顺序**、**选择**(if-else)、**循环**(for/while)
|
||
- **数据结构**:算法处理的数据的组织方式
|
||
|
||
### 三、算法描述方法
|
||
|
||
描述算法的常见方式有三种:
|
||
|
||
1. **自然语言**:简单直观,但容易产生歧义
|
||
2. **流程图**:图形化表示,直观清晰。注意:**菱形表示判断/选择**
|
||
3. **伪代码**:介于自然语言和编程语言之间,兼顾可读性和精确性
|
||
|
||
```cpp
|
||
#include <vector>
|
||
using namespace std;
|
||
|
||
// 查找数组中的最小值
|
||
int findMin(vector<int>& A, int n) {
|
||
int minVal = A[0];
|
||
for (int i = 1; i < n; i++) {
|
||
if (A[i] < minVal) {
|
||
minVal = A[i];
|
||
}
|
||
}
|
||
return minVal;
|
||
}
|
||
```
|
||
|
||
> [!tip] 流程图关键符号
|
||
> - **矩形**:处理/运算
|
||
> - **菱形**:判断/选择(这是一个高频考点)
|
||
> - **平行四边形**:输入/输出
|
||
> - **椭圆/圆角矩形**:开始/结束
|
||
|
||
### 四、时间复杂度分析
|
||
|
||
#### 4.1 基本概念
|
||
|
||
时间复杂度描述的是算法运行时间随输入规模增长的**增长趋势**,我们使用大 O 表示法。
|
||
|
||
#### 4.2 常见复杂度排序
|
||
|
||
从低到高排列,这是必须记住的核心序列:
|
||
|
||
$$O(1) < O(\log n) < O(n) < O(n\log n) < O(n^2) < O(2^n) < O(n!)$$
|
||
|
||
| 复杂度 | 名称 | 典型算法 | n=1000时的大致运算量 |
|
||
|--------|------|----------|---------------------|
|
||
| O(1) | 常数 | 数组按索引访问 | 1 |
|
||
| O(log n) | 对数 | 二分查找 | ~10 |
|
||
| O(n) | 线性 | 遍历查找最小值 | 1,000 |
|
||
| O(n log n) | 线性对数 | 归并排序 | ~10,000 |
|
||
| O(n²) | 平方 | 冒泡排序 | 1,000,000 |
|
||
| O(2ⁿ) | 指数 | Fibonacci递归 | 10³⁰¹(天文数字) |
|
||
| O(n!) | 阶乘 | 全排列 | 不可计算 |
|
||
|
||
#### 4.3 复杂度增长趋势
|
||
|
||
```mermaid
|
||
xychart-beta
|
||
title "Time Complexity Growth Curves"
|
||
x-axis "Input Size n" [1, 2, 3, 4, 5, 6, 7, 8]
|
||
y-axis "Operations" 0 --> 30
|
||
line "O(1)" [1, 1, 1, 1, 1, 1, 1, 1]
|
||
line "O(logn)" [0, 1, 1.6, 2, 2.3, 2.6, 2.8, 3]
|
||
line "O(n)" [1, 2, 3, 4, 5, 6, 7, 8]
|
||
line "O(nlogn)" [0, 2, 4.8, 8, 11.6, 15.5, 19.6, 24]
|
||
line "O(n^2)" [1, 4, 9, 16, 25, 36, 49, 64]
|
||
```
|
||
|
||
#### 4.4 常见算法复杂度分析
|
||
|
||
| 算法 | 复杂度 | 分析思路 |
|
||
|------|--------|----------|
|
||
| 查找n个数最小值 | O(n) | 需要遍历所有元素一次 |
|
||
| 冒泡排序 | O(n²) | 两层嵌套循环 |
|
||
| 二分查找 | O(logn) | 每次将搜索范围缩小一半 |
|
||
| 归并排序 | O(n logn) | 递归深度 logn,每层合并 O(n) |
|
||
| Fibonacci递归 | O(2ⁿ) | 每次调用产生2个子调用 |
|
||
|
||
### 五、递归与Fibonacci复杂度详解
|
||
|
||
#### 5.1 Fibonacci递归的调用树
|
||
|
||
Fibonacci递归算法的时间复杂度是 O(2ⁿ)。让我们具体分析 f(5) 的递归调用次数:
|
||
|
||
```plaintext
|
||
f(5)
|
||
/ \
|
||
f(4) f(3)
|
||
/ \ / \
|
||
f(3) f(2) f(2) f(1)
|
||
/ \
|
||
f(2) f(1)
|
||
/ \
|
||
f(1) f(0)
|
||
```
|
||
|
||
> [!question] Fibonacci递归算法的时间复杂度为什么是O(2^n)?
|
||
> 因为每次递归调用都会产生**两个子调用**(f(n-1) 和 f(n-2)),形成一棵近似满二叉树。树的节点总数约为 2^n 量级,因此总时间复杂度为 O(2^n)。
|
||
|
||
> [!question] f(5) 需要多少次递归调用(不含基本运算)?
|
||
> 从上面的调用树可以看出,f(5) 展开后共有 **15个节点**(含重复计算)。其中 f(3) 被计算了2次,f(2) 被计算了3次。大量重复计算正是Fibonacci递归效率低下的原因。
|
||
|
||
#### 5.2 递归方程
|
||
|
||
对于递归算法,我们常用**递归方程**来描述其时间复杂度:
|
||
|
||
```
|
||
T(n) = T(n-1) + T(n-2) + 1
|
||
T(0) = 0, T(1) = 0
|
||
```
|
||
|
||
这个递归方程的解约为 O(2ⁿ),与Fibonacci数本身的增长速度一致。
|
||
|
||
### 六、递归与迭代
|
||
|
||
#### 6.1 核心定理
|
||
|
||
> [!tip] 递归与迭代的等价性
|
||
> **每个递归算法都可以转换为迭代算法。** 递归使用函数调用栈,迭代使用显式循环。递归更直观,但迭代通常更节省空间。
|
||
|
||
#### 6.2 栈数据结构
|
||
|
||
递归调用的底层依赖**栈(Stack)**数据结构:
|
||
|
||
- **LIFO**(Last In First Out,后进先出)
|
||
- 每次递归调用,将当前函数的局部变量和返回地址压入栈
|
||
- 每次返回,从栈顶弹出恢复现场
|
||
- **DFS(深度优先搜索)** 也使用栈实现
|
||
|
||
```cpp
|
||
// Fibonacci的迭代版本(时间O(n),空间O(1))
|
||
int fibIterative(int n) {
|
||
if (n <= 1) return n;
|
||
int prev = 0, curr = 1;
|
||
for (int i = 2; i <= n; i++) {
|
||
int next = prev + curr;
|
||
prev = curr;
|
||
curr = next;
|
||
}
|
||
return curr;
|
||
}
|
||
```
|
||
|
||
迭代版本将时间复杂度从 O(2ⁿ) 降至 O(n),空间复杂度从 O(n)(递归栈)降至 O(1)。
|
||
|
||
#### 6.3 迭代加深
|
||
|
||
**迭代加深(Iterative Deepening)** 是一种限制递归深度的策略,常用于搜索算法:
|
||
|
||
- 设定最大深度 limit
|
||
- 从深度 0 开始,逐步增加 limit
|
||
- 每次进行深度受限的DFS
|
||
- 结合了DFS的空间效率和BFS的完备性
|
||
|
||
> [!warning] 迭代加深与递归的关系
|
||
> 迭代加深本质上还是递归,只是人为限制了递归的深度。它不是把递归转成迭代,而是在递归框架内控制搜索深度。
|
||
|
||
### 七、递归调用次数的计算方法
|
||
|
||
计算递归调用次数是考试高频考点。通用方法:
|
||
|
||
1. **画出递归树**,从根节点开始展开
|
||
2. **统计节点总数**(每个节点代表一次调用)
|
||
3. **注意重复计算**:相同子问题被多次计算的节点要重复计数
|
||
|
||
| 问题 | 递归方程 | 调用次数 |
|
||
|------|----------|----------|
|
||
| Fibonacci f(n) | T(n) = T(n-1) + T(n-2) + 1 | ~2ⁿ |
|
||
| 汉诺塔 n个盘 | T(n) = 2T(n-1) + 1 | 2ⁿ - 1 |
|
||
| 二分查找 | T(n) = T(n/2) + 1 | ~log₂n |
|
||
|
||
## 关联笔记
|
||
- [[算法设计与分析/试题册/index|试题册索引]]
|