GESP 6级

时间复杂度

6级 · 指针/STL/复杂度
💡 为什么重要

时间复杂度是评价算法好坏的核心标准。GESP 6级考试几乎每道编程题都会问"分析时间复杂度"。更重要的是,理解它能帮你在面对问题时选择正确的解法。比如 n=100000 时,O(n²) 需要 100 亿次操作(超时),而 O(n log n) 只需约 170 万次(瞬间完成)。

📖 前置知识
  • for 循环和 while 循环
  • 函数的定义和调用
  • 基本的递归概念(可选但有帮助)
🔍 什么是"时间复杂度"?

时间复杂度不是程序实际运行了多少秒,而是程序的执行次数随输入规模 n 增长的趋势。我们用大 O 符号表示,忽略常数和低阶项。

核心思想:忽略常数系数和低阶项

  • 执行了 3n 次 → O(n)(忽略系数 3)
  • 执行了 2n + 5 次 → O(n)(忽略系数 2 和常数 5)
  • 执行了 n² + n 次 → O(n²)(忽略低阶项 n)
📐 常见复杂度从小到大
O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2ⁿ) < O(n!)

O(1) → 常数时间:直接取值,与n无关
O(log n) → 对数时间:二分查找
O(n) → 线性时间:遍历一遍
O(n log n) → 线性对数:归并排序
O(n²) → 平方时间:双重循环
O(2ⁿ) → 指数时间:暴力枚举子集
O(n!) → 阶乘时间:全排列

★ n=100000 时的大致操作次数:
O(1) ≈ 1次
O(log n) ≈ 17次
O(n) ≈ 100,000次
O(n log n) ≈ 1,700,000次
O(n²) ≈ 10,000,000,000次(超时!)
💻 代码与复杂度对应关系
📝 O(1) — 常数时间
// O(1): 不管n多大,固定操作次数
int getFirst(int arr[], int n) {
    return arr[0];      // 直接取值,1次操作
}

bool isEven(int n) {
    return n % 2 == 0;  // 一次取余和比较
}
📝 O(n) — 线性时间:单层循环
// O(n): 循环体执行约n次
int findMax(int arr[], int n) {
    int maxVal = arr[0];
    for (int i = 1; i < n; i++) {  // 循环n-1次
        if (arr[i] > maxVal) {
            maxVal = arr[i];
        }
    }
    return maxVal;
}
📝 O(n²) — 平方时间:双重嵌套循环
// O(n²): 外层n次 × 内层n次
void bubbleSort(int arr[], int n) {
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n - 1; j++) {
            if (arr[j] > arr[j+1]) {
                int t = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = t;
            }
        }
    }
}
📝 O(log n) — 对数时间:每次排除一半
// O(log n): 二分查找
int binarySearch(int arr[], int n, int target) {
    int lo = 0, hi = n - 1;
    while (lo <= hi) {             // 循环约log₂(n)次
        int mid = (lo + hi) / 2;
        if (arr[mid] == target) return mid;
        else if (arr[mid] < target) lo = mid + 1;
        else hi = mid - 1;
        // 每次范围减半:n → n/2 → n/4 → ... → 1
    }
    return -1;
}
📝 O(n log n) — 线性对数时间
// O(n log n): 外层n次 × 内层每次减半log(n)次
void someOperation(int n) {
    for (int i = 0; i < n; i++) {  // 外层n次
        int x = n;
        while (x > 1) {            // 内层每次减半
            x /= 2;                 // 约log(n)次
        }
    }
    // 总计:n × log(n) = O(n log n)
}
// 典型代表:归并排序、快速排序(平均)
📝 O(2ⁿ) — 指数时间:暴力枚举
// O(2ⁿ): 枚举所有子集(每个元素选或不选)
void printAllSubsets(int arr[], int n) {
    for (int mask = 0; mask < (1 << n); mask++) {
        // mask从0到2ⁿ-1,共2ⁿ次
        cout << "{ ";
        for (int i = 0; i < n; i++) {
            if (mask & (1 << i)) cout << arr[i] << " ";
        }
        cout << "}" << endl;
    }
    // n=20时约100万次,n=30时约10亿次!
}
🔍 分析技巧
  • 找循环:看循环执行多少次,嵌套循环次数相乘
  • 忽略常数:3n → O(n),n²/2 → O(n²)
  • 取最高阶:n² + n → O(n²)
  • 二分/减半:每次减半 → O(log n)
  • STL函数:sort() 是 O(n log n),find() 在 vector 上是 O(n)
⚠️ 易错点
  • 把 O(2n) 写成 O(2n):正确是 O(n)!常数系数要忽略
  • 分不清 O(n) 和 O(n²):两层嵌套循环就是 O(n²)
  • 误认为递归都是 O(2ⁿ):尾递归可以是 O(n),二分递归是 O(log n)
  • 忘记 sort 的复杂度:C++ 的 sort 是 O(n log n),不是 O(n²)
  • 忽略"取最高阶"原则:O(n³ + n² + n) = O(n³),只看最大的那项
🎯 练习建议
  • 入门:写出以下代码的时间复杂度:单层循环、双重循环、三重循环
  • 进阶:分析冒泡排序、选择排序、插入排序的时间复杂度
  • 挑战:同一道题用 O(n²) 暴力和 O(n log n) 排序实现,对比运行时间
  • 思考:为什么 n=10⁵ 时 O(n²) 会超时但 O(n log n) 不会?
  • 画图:用表格画出不同 n 值下各复杂度的操作次数