Files
final-exam/算法设计与分析/复习文档/算法基础与复杂度分析.md
T

7.4 KiB
Raw Blame History

tags, create time
tags create time
算法设计与分析
复习
复杂度分析
2026-06-10 11:11

算法基础与复杂度分析

概述

本文系统梳理算法的基本概念、四个特性、算法三要素、时间复杂度的计算与排序,以及递归与迭代的关系。这是算法课程的基石,掌握这些内容才能顺利理解后续的分治法、贪心算法、动态规划等高级策略。

正文

一、算法的定义与四个特性

算法(Algorithm)是对特定问题求解步骤的一种描述,它是指令的有限序列。每个指令表示一个或多个操作。

一个合法的算法必须满足以下四个特性:

特性 含义 反例
有穷性 算法必须在执行有限步之后终止 死循环
确定性 每条指令的含义明确、无二义性 模糊的自然语言描述
可行性 每条指令都是可执行的基本操作 无法实现的操作
输入/输出 有零个或多个输入,有一个或多个输出 无输出的程序

[!question] 为什么算法可以没有输入,但不能没有输出? 因为算法的目的是解决问题并给出结果。没有输入意味着算法不依赖外部数据(如计算 π 的某一位),但无论如何必须有输出才能体现其价值。

二、算法三要素

算法的三个核心要素是:运算、控制结构和数据结构。

[!warning] 常见误区 很多同学把"输入/输出"当作算法三要素之一,这是错误的。算法三要素是运算、控制结构和数据结构。

  • 运算:算法中执行的每一种操作
  • 控制结构:三种基本结构——顺序、选择(if-else)、循环(for/while)
  • 数据结构:算法处理的数据的组织方式

三、算法描述方法

描述算法的常见方式有三种:

  1. 自然语言:简单直观,但容易产生歧义
  2. 流程图:图形化表示,直观清晰。注意:菱形表示判断/选择
  3. 伪代码:介于自然语言和编程语言之间,兼顾可读性和精确性
#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] 迭代加深与递归的关系 迭代加深本质上还是递归,只是人为限制了递归的深度。它不是把递归转成迭代,而是在递归框架内控制搜索深度。

七、递归调用次数的计算方法

计算递归调用次数是考试高频考点。通用方法:

  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

关联笔记