C++ 时间复杂度详解

时间复杂度是衡量算法执行效率的核心指标,描述算法运行时间随输入规模增长的趋势,而非精确时间。


一、什么是时间复杂度

时间复杂度(Time Complexity)用 大 O 表示法(Big O Notation) 来描述:当输入规模 nnn 趋于无穷大时,算法执行次数的上界

代码算法

分析基本操作次数

抽象为关于 n 的函数 f(n)

取增长最快的项,忽略系数

得到大 O 表示:O(?)

三条核心规则:

规则 说明 示例
保留最高阶项 n→∞n \to \inftyn 时,最高阶项主导 n2+n→O(n2)n^2 + n \to O(n^2)n2+nO(n2)
忽略常数系数 大 O 只关心增长趋势 3n→O(n)3n \to O(n)3nO(n)
加法取最大,乘法保留 多段代码:相加取最大;嵌套:相乘 见下文详解

二、如何计算时间复杂度:四步法

第一步:确定输入规模 n

第二步:找出核心操作(最深层的循环体)

第三步:计算核心操作执行的总次数

第四步:用大 O 规则化简

得到最终时间复杂度

n 可以是数组长度、二叉树节点数、字符串长度等

一次赋值、一次比较、一次加法 = O(1) 基本操作

逐层分析循环嵌套,累加或相乘

保留最高阶项,忽略系数


三、常见时间复杂度代码示例(带详细计算过程)

3.1 O(1) — 常数阶

// 无论 n 多大,只执行一次
int getFirst(vector<int>& arr) {
    return arr[0];  // 1 次基本操作
}
// T(n) = 1 → O(1)
n 的值 执行次数
10 1
100 1
1,000,000 1
O(1) 常数阶 0 10 100 1000 10000 输入规模 n 2 1.8 1.6 1.4 1.2 1 0.8 0.6 0.4 0.2 0 执行次数

3.2 O(log n) — 对数阶

// 二分查找:每次查找范围减半
int binarySearch(vector<int>& arr, int target) {
    int left = 0, right = arr.size() - 1;        // O(1)
    while (left <= right) {                       // 循环多少次?
        int mid = left + (right - left) / 2;      // O(1)
        if (arr[mid] == target) return mid;       // O(1)
        else if (arr[mid] < target)
            left = mid + 1;                       // 范围缩小一半
        else
            right = mid - 1;                      // 范围缩小一半
    }
    return -1;
}

计算过程:

  • 初始范围大小:nnn
  • 第 1 次循环后:n/2n/2n/2
  • 第 2 次循环后:n/4n/4n/4
  • 第 k 次循环后:n/2kn/2^kn/2k
  • n/2k=1n/2^k = 1n/2k=1 时循环结束 → k=log⁡2nk = \log_2 nk=log2n
  • 故:O(log n)
O(log n) 对数阶 1 10 100 1000 10000 100000 输入规模 n 20 18 16 14 12 10 8 6 4 2 0 执行次数

3.3 O(n) — 线性阶

// 遍历数组:每个元素访问一次
int findMax(vector<int>& arr) {
    int maxVal = arr[0];                    // 1 次
    for (int i = 1; i < arr.size(); i++) {  // 循环 n-1 次
        if (arr[i] > maxVal)                // n-1 次比较
            maxVal = arr[i];                // 最多 n-1 次赋值
    }
    return maxVal;                          // 1 次
}
// T(n) = 1 + (n-1) + (n-1) + 1 = 2n ≈ O(n)

3.4 O(n log n) — 线性对数阶

// 归并排序:每层 O(n),共 log n 层
void mergeSort(vector<int>& arr, int left, int right) {
    if (left >= right) return;                   // O(1)
    int mid = left + (right - left) / 2;         // O(1)
    mergeSort(arr, left, mid);                   // 递归左半
    mergeSort(arr, mid + 1, right);              // 递归右半
    merge(arr, left, mid, right);                // O(n) 合并
}

计算过程 — 递归树分析法:

第 log n 层: n 个大小为 1 的子问题

第1层: 2 个大小为 n/2 的子问题, 合并 O(n/2)+O(n/2)=O(n)

第0层: 1 个大小为 n 的子问题, 合并 O(n)

mergeSort(n) cost = O(n)

mergeSort(n/2)

merge O(n/2)

mergeSort(n/2)

merge O(n/2)

...

  • 递归深度:log⁡2n\log_2 nlog2n
  • 每层合并操作总和:O(n)O(n)O(n)
  • 总时间 = 层数 × 每层代价 = O(nlog⁡n)O(n \log n)O(nlogn)

3.5 O(n²) — 平方阶

// 双重循环:外层 n 次,内层 n 次
void printPairs(vector<int>& arr) {
    int n = arr.size();
    for (int i = 0; i < n; i++) {         // 执行 n 次
        for (int j = 0; j < n; j++) {     // 每次执行 n 次
            cout << arr[i] << "," << arr[j] << endl;  // n × n 次
        }
    }
}
// T(n) = n × n = n² → O(n²)

⚠️ 注意区分:内层循环范围从 0 开始时才是 n2n^2n2,若内层从 i+1 开始则变为 n(n−1)/2n(n-1)/2n(n1)/2 → 仍然是 O(n²)(系数被忽略)。

// 内层范围递减 — 仍是 O(n²)
for (int i = 0; i < n; i++)
    for (int j = i + 1; j < n; j++)  // 执行 n-1, n-2, ..., 1 次
        doSomething();
// T(n) = (n-1) + (n-2) + ... + 1 = n(n-1)/2 = 0.5n² - 0.5n → O(n²)

3.6 O(2ⁿ) — 指数阶

// 斐波那契递归(朴素版)
int fib(int n) {
    if (n <= 1) return n;           // O(1)
    return fib(n - 1) + fib(n - 2); // 每次调用分裂为两个子问题
}

fib(5)

fib(4)

fib(3)

fib(3)

fib(2)

fib(2)

fib(1)

fib(2)

fib(1)

fib(1)

fib(0)

fib(1)

fib(0)

fib(1)

fib(0)

调用次数:T(n)=T(n−1)+T(n−2)+O(1)≈O(2n)T(n) = T(n-1) + T(n-2) + O(1) \approx O(2^n)T(n)=T(n1)+T(n2)+O(1)O(2n)
每层节点数倍增,是一棵接近满的二叉树。


3.7 O(n!) — 阶乘阶

// 全排列生成(回溯法)
void permute(vector<int>& nums, int start, vector<vector<int>>& res) {
    if (start == nums.size()) {
        res.push_back(nums);  // 找到一个排列
        return;
    }
    for (int i = start; i < nums.size(); i++) {  // 每层可选数递减
        swap(nums[start], nums[i]);
        permute(nums, start + 1, res);            // 递归
        swap(nums[start], nums[i]);
    }
}
// 第 k 层有 (n-k) 个分支,总共 n × (n-1) × ... × 1 = n! → O(n!)

四、时间复杂度增长对比

各阶时间复杂度增长曲线对比 0 1 2 3 4 5 6 7 8 9 10 100 90 80 70 60 50 40 30 20 10 0 操作次数(对数刻度)

实际运行时间估算(假设每次操作 1ns)

输入规模 n O(log n) O(n) O(n log n) O(n²) O(2ⁿ)
10 3ns 10ns 33ns 100ns 1μs
100 7ns 100ns 664ns 10μs 4×10¹³ 年 😱
1,000 10ns 1μs 10μs 1ms
10⁶ 20ns 1ms 20ms 17 分钟
10⁹ 30ns 1s 30s 32 年

五、多段代码的时间复杂度分析

5.1 顺序结构:加法取最大

void process(vector<int>& arr) {
    // 代码块 A:O(n)
    for (int i = 0; i < arr.size(); i++)
        cout << arr[i] << " ";

    // 代码块 B:O(n²)
    for (int i = 0; i < arr.size(); i++)
        for (int j = 0; j < arr.size(); j++)
            cout << arr[i] + arr[j] << " ";

    // 代码块 C:O(n)
    for (int i = 0; i < arr.size(); i++)
        cout << arr[i] * 2 << " ";
}
// 总时间 = O(n) + O(n²) + O(n) = O(n²)(取最大)

代码块 A: O(n)

代码块 B: O(n²)

代码块 C: O(n)

总时间 = max(A, B, C) = O(n²)


5.2 循环嵌套:乘法

for (int i = 0; i < n; i++) {         // O(n)
    for (int j = 0; j < m; j++) {     // O(m)
        cout << i + j;                // O(1)
    }
}
// 总时间 = O(n) × O(m) × O(1) = O(n × m)
// 若 m = n,则 O(n²)

5.3 混合结构示例

void example(int n, int m) {
    // 部分 1:嵌套循环 O(n × m)
    for (int i = 0; i < n; i++)
        for (int j = 0; j < m; j++)
            doA();                          // O(1)

    // 部分 2:单循环 O(n)
    for (int i = 0; i < n; i++)
        doB();                              // O(1)

    // 部分 3:递归 O(2ⁿ)
    fibonacci(n);
}
// 总时间 = O(n×m + n + 2ⁿ) = O(2ⁿ)

经验法则: 有递归看递归,没递归看嵌套最深层,多段代码取最大。


六、递归算法的时间复杂度

6.1 主定理(Master Theorem)

对于递推式 T(n)=a⋅T(n/b)+f(n)T(n) = a \cdot T(n/b) + f(n)T(n)=aT(n/b)+f(n)

条件 时间复杂度
f(n)=O(nlog⁡ba−ϵ)f(n) = O(n^{\log_b a - \epsilon})f(n)=O(nlogbaϵ) O(nlog⁡ba)O(n^{\log_b a})O(nlogba)
f(n)=Θ(nlog⁡ba)f(n) = \Theta(n^{\log_b a})f(n)=Θ(nlogba) O(nlog⁡balog⁡n)O(n^{\log_b a} \log n)O(nlogbalogn)
f(n)=Ω(nlog⁡ba+ϵ)f(n) = \Omega(n^{\log_b a + \epsilon})f(n)=Ω(nlogba+ϵ) O(f(n))O(f(n))O(f(n))

6.2 典型递归案例速查

算法 递推式 时间复杂度
二分查找 T(n)=T(n/2)+O(1)T(n) = T(n/2) + O(1)T(n)=T(n/2)+O(1) O(log n)
归并排序 T(n)=2T(n/2)+O(n)T(n) = 2T(n/2) + O(n)T(n)=2T(n/2)+O(n) O(n log n)
快速排序(平均) T(n)=2T(n/2)+O(n)T(n) = 2T(n/2) + O(n)T(n)=2T(n/2)+O(n) O(n log n)
快速排序(最坏) T(n)=T(n−1)+O(n)T(n) = T(n-1) + O(n)T(n)=T(n1)+O(n) O(n²)
斐波那契(朴素) T(n)=T(n−1)+T(n−2)+O(1)T(n) = T(n-1) + T(n-2) + O(1)T(n)=T(n1)+T(n2)+O(1) O(2ⁿ)
二分查找变体 T(n)=T(n/2)+O(n)T(n) = T(n/2) + O(n)T(n)=T(n/2)+O(n) O(n)

七、C++ STL 常见操作时间复杂度

容器操作复杂度一览

适配器

stack/queue: 插入/删除 O(1)

priority_queue: 插入 O(log n), 取最大 O(1)

关联容器

map/set: 插入/查找/删除 O(log n)

unordered_map/set: 插入/查找/删除 O(1) 均摊, 最坏 O(n)

顺序容器

vector: 随机访问 O(1), 尾部插入 O(1) 均摊, 中间插入 O(n)

deque: 随机访问 O(1), 两端插入 O(1)

list: 插入/删除 O(1), 随机访问 O(n)

STL 算法复杂度速查

算法 时间复杂度 说明
sort() O(n log n) 内省排序(快排+堆排+插入排序混合)
binary_search() O(log n) 二分查找
find() / count() O(n) 线性遍历
lower_bound() / upper_bound() O(log n) 基于二分查找
next_permutation() O(n) 每次调用
reverse() O(n) 线性遍历
make_heap() O(n) 建堆
push_heap() / pop_heap() O(log n) 堆调整
nth_element() O(n) 平均 快排分区

八、常见误区与易错点

8.1 误区一:以为循环减半就是 O(log n)

// ❌ 错误:这是 O(n)!
for (int i = 0; i < n; i += 2)  // 执行 n/2 次
    doSomething();
// n/2 ∈ O(n),减半 ≠ 对半分割
// ✅ 正确:这才是 O(log n)
for (int i = 1; i < n; i *= 2)  // i: 1,2,4,8,... → 执行 log n 次
    doSomething();

8.2 误区二:多循环一定嵌套

// 三个独立循环 → O(n + m + k),不是 O(n³)
for (int i = 0; i < n; i++) doA();
for (int j = 0; j < m; j++) doB();
for (int k = 0; k < kMax; k++) doC();
// T(n) = n + m + kMax → O(n + m + kMax)

8.3 误区三:忽略隐藏的循环

// string 拼接:看似 O(1),实则 O(m)
string s = "";
for (int i = 0; i < n; i++)
    s += "a";           // 每次 += 是 O(s.length()),总计 O(n²)!
// 修正:使用 vector<char> 或 string::reserve + push_back

8.4 误区四:递归看起来很简短 ≠ 复杂度低

void dfs(int n) {
    if (n == 0) return;
    dfs(n - 1);   // T(n) = T(n-1) + O(1) → O(n)
    dfs(n - 1);   // T(n) = 2T(n-1) + O(1) → O(2ⁿ) ← 多一条语句,天壤之别
}

九、复杂度分析决策流程

开始分析代码时间复杂度

包含递归调用?

写出递推式 T(n) = a·T(n/b) + f(n)

用主定理求解,或画递归树

有循环嵌套?

逐层分析循环范围

嵌套循环:将各层范围相乘

注意:内层范围是否依赖外层变量?

若是递减序列需用等差数列求和

顺序结构:逐段分析,取最大值

化简 → 保留最高阶项 → 得到 O(?)

完成


十、速记口诀

「常对线,线性对,平方指数阶乘尾」

复杂度 口诀 记忆点
O(1) 常数阶 一行代码,不依赖 n
O(log n) 对数阶 每次减半,i *= 2
O(n) 线性阶 一层完整循环
O(n log n) 线性对数阶 分治+合并,快排/归并
O(n²) 平方阶 双层嵌套完整循环
O(n³) 立方阶 三层嵌套,Floyd 算法
O(2ⁿ) 指数阶 每次递归分裂两个子问题
O(n!) 阶乘阶 全排列,暴力枚举

更多推荐