--- tags: - 算法设计与分析 - 复习 - 复杂度分析 create time: 2026-06-10 11:11 --- # 算法基础与复杂度分析 ## 概述 本文系统梳理算法的基本概念、四个特性、算法三要素、时间复杂度的计算与排序,以及递归与迭代的关系。这是算法课程的基石,掌握这些内容才能顺利理解后续的分治法、贪心算法、动态规划等高级策略。 ## 正文 ### 一、算法的定义与四个特性 算法(Algorithm)是对特定问题求解步骤的一种描述,它是指令的有限序列。每个指令表示一个或多个操作。 一个合法的算法必须满足以下**四个特性**: | 特性 | 含义 | 反例 | |------|------|------| | **有穷性** | 算法必须在执行有限步之后终止 | 死循环 | | **确定性** | 每条指令的含义明确、无二义性 | 模糊的自然语言描述 | | **可行性** | 每条指令都是可执行的基本操作 | 无法实现的操作 | | **输入/输出** | 有零个或多个输入,有一个或多个输出 | 无输出的程序 | > [!question] 为什么算法可以没有输入,但不能没有输出? > 因为算法的目的是解决问题并给出结果。没有输入意味着算法不依赖外部数据(如计算 π 的某一位),但无论如何必须有输出才能体现其价值。 ### 二、算法三要素 算法的三个核心要素是:**运算**、**控制结构**和**数据结构**。 > [!warning] 常见误区 > 很多同学把"输入/输出"当作算法三要素之一,这是错误的。算法三要素是运算、控制结构和数据结构。 - **运算**:算法中执行的每一种操作 - **控制结构**:三种基本结构——**顺序**、**选择**(if-else)、**循环**(for/while) - **数据结构**:算法处理的数据的组织方式 ### 三、算法描述方法 描述算法的常见方式有三种: 1. **自然语言**:简单直观,但容易产生歧义 2. **流程图**:图形化表示,直观清晰。注意:**菱形表示判断/选择** 3. **伪代码**:介于自然语言和编程语言之间,兼顾可读性和精确性 ```plaintext // 伪代码示例:查找数组中的最小值 function findMin(A, n): minVal = A[1] for i = 2 to n: 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(深度优先搜索)** 也使用栈实现 ```plaintext // Fibonacci的迭代版本(使用栈模拟的思想,但实际是循环) function fibIterative(n): if n <= 1: return n prev = 0 curr = 1 for i = 2 to n: 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|试题册索引]]