7.4 KiB
tags, create time
| tags | create time | |||
|---|---|---|---|---|
|
2026-06-10 11:11 |
算法基础与复杂度分析
概述
本文系统梳理算法的基本概念、四个特性、算法三要素、时间复杂度的计算与排序,以及递归与迭代的关系。这是算法课程的基石,掌握这些内容才能顺利理解后续的分治法、贪心算法、动态规划等高级策略。
正文
一、算法的定义与四个特性
算法(Algorithm)是对特定问题求解步骤的一种描述,它是指令的有限序列。每个指令表示一个或多个操作。
一个合法的算法必须满足以下四个特性:
| 特性 | 含义 | 反例 |
|---|---|---|
| 有穷性 | 算法必须在执行有限步之后终止 | 死循环 |
| 确定性 | 每条指令的含义明确、无二义性 | 模糊的自然语言描述 |
| 可行性 | 每条指令都是可执行的基本操作 | 无法实现的操作 |
| 输入/输出 | 有零个或多个输入,有一个或多个输出 | 无输出的程序 |
[!question] 为什么算法可以没有输入,但不能没有输出? 因为算法的目的是解决问题并给出结果。没有输入意味着算法不依赖外部数据(如计算 π 的某一位),但无论如何必须有输出才能体现其价值。
二、算法三要素
算法的三个核心要素是:运算、控制结构和数据结构。
[!warning] 常见误区 很多同学把"输入/输出"当作算法三要素之一,这是错误的。算法三要素是运算、控制结构和数据结构。
- 运算:算法中执行的每一种操作
- 控制结构:三种基本结构——顺序、选择(if-else)、循环(for/while)
- 数据结构:算法处理的数据的组织方式
三、算法描述方法
描述算法的常见方式有三种:
- 自然语言:简单直观,但容易产生歧义
- 流程图:图形化表示,直观清晰。注意:菱形表示判断/选择
- 伪代码:介于自然语言和编程语言之间,兼顾可读性和精确性
#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 复杂度增长趋势
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) 的递归调用次数:
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(深度优先搜索) 也使用栈实现
// 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] 迭代加深与递归的关系 迭代加深本质上还是递归,只是人为限制了递归的深度。它不是把递归转成迭代,而是在递归框架内控制搜索深度。
七、递归调用次数的计算方法
计算递归调用次数是考试高频考点。通用方法:
- 画出递归树,从根节点开始展开
- 统计节点总数(每个节点代表一次调用)
- 注意重复计算:相同子问题被多次计算的节点要重复计数
| 问题 | 递归方程 | 调用次数 |
|---|---|---|
| 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 |