🧮

数步子的账本:电脑再快,步子太多也来不及

算法对不对是一回事,快不快是另一回事。今天学「数步子」:一层循环 O(n)、两层 O(n²)、每次减半 O(log n)——再拿「每秒 1 亿步」的预算比一比,就知道 n = 10 万时谁来得及、谁超时(TLE)。

💡 打个比方

电脑再快,也有「体力上限」:竞赛裁判机大约 每秒 1 亿步(10⁸ 步/秒),限时 1 秒——你的算法总共只能花 1 亿步。数据规模 n 一变大,「步数」就不是 n、而是 n²、n³ 地涨:n = 10 万时,n² = 100 亿步,要跑 100 秒——裁判早按了「超时」。大 O 记号就是「步子账本」:不数精确步数,只记随 n 增长最快的那一项——2n + 50 记成 O(n),3n² + n 记成 O(n²),因为 n 大了以后,平方项、立方项会「吃掉」所有小的。

怎么数?看循环:一层 for 跑 n 圈 → O(n);两层各 n 圈嵌套 → O(n²);每圈把问题砍半(x = x / 2、二分查找)→ 只砍 log₂n 次就砍完 → O(log n);树图遍历「每个点一次、每条边一次」→ O(n + m)。空间账本更简单:开了几个数组、多大——int 一个 4 字节,int g[1000][1000] 就是 100 万个 int ≈ 4 MB;递归调 n 层就是 O(n) 的调用栈。

为什么二分查找只要 log n 步?10000 个数里找一个:每次看正中间,不对就扔掉一半——10000 → 5000 → 2500 → 1250 → … → 1,13 次就定位;顺序查最坏要 10000 次。「每次减半」是图论之外最强大的加速技巧,没有之一。

术语:时间复杂度 / 大 O 记号(只记随 n 增长最快的项,忽略常系数);O(1) 常数级(一步到位);O(log n) 对数级(每次减半);O(n) 线性;O(n log n) 线性对数(排序的下限级);O(n²)、O(n³) 平方/立方;TLE(Time Limit Exceeded,超时);MLE(Memory Limit Exceeded,超内存);10⁸ 步/秒(裁判机速度的约定估计)。

🖼️ 大图看明白

n = 10000 时的步数(1 亿步 ≈ 1 秒) O(1) 1 步 O(log n) 13 步 O(n) 10000 步 O(n log n) 130000 步 O(n²) 1 亿步 ≈ 1 秒 O(2^n) 天文数字(2 的 10000 次方) 看循环 → 估步数 for i = 1..n → O(n) for i = 1..n 里再 for j = 1..n → O(n²) while x > 1: x = x / 2 → O(log n) 预算:每秒 10^8 步 n = 10^5:O(n log n) 能过,O(n²) 超时

🧩 子任务 1:大 O 速查——n = 10000 时各要多少步

🎯 小目标:背下这张「步数对照表」,看到 n 和算法类型,3 秒估出总步数。
// ── n = 10000 时的步数对照表 ──
// 复杂度        n = 10000 时的步数     1 秒预算(1 亿步)够不够
// O(1)          1 步                  绰绰有余
// O(log n)      13 步(2^13 = 8192 < 10000)
// O(n)          10000 步              绰绰有余
// O(n log n)    10000 x 13 ≈ 130000   绰绰有余
// O(n^2)        10000^2 = 1 亿步      擦边(1 秒)
// O(n^3)        10000^3 = 1000 亿步   要 100 秒 ✗
// O(2^n)        2^10000 = 天文数字    宇宙毁灭都跑不完 ✗
✅ 小检查:① 10 万个数里找一个数,能选什么?要多少步?② 给 1 万个数排序,冒泡 O(n²) vs 快排 O(n log n),各多少步、快多少倍?③ 500 个点 2000 条边的图,DFS 全走一遍多少步?
看看答案 ① 数排好序:二分 O(log n) = 17 步(2^17 = 131072 > 100000);没排序:只能线性 100000 步——或者先花 n log n ≈ 170 万步排好序,以后每次查都只要 17 步。② 冒泡 n² ≈ 1 亿步;快排 n log n ≈ 10000 × 14 = 14 万步;1 亿 ÷ 14 万 ≈ 700 倍。③ O(n + m) = 500 + 2000 = 2500 步——「每个点进一次、每条边看一次」。
易错:「冒泡慢但总能用」——n = 1 万时 1 亿步刚好擦边,n = 10 万就 100 亿步直接超时;「二分随便用」——二分的前提是数组必须有序,没排序就扔一半,可能正好把目标扔掉了。

🧩 子任务 2:怎么数——看循环 + 数数组

🎯 小目标:拿到一段代码,30 秒读出它的时间和空间复杂度。
// ── 看循环:时间复杂度 ──
// 一层 for 跑 n 圈        →  O(n)
// 两层嵌套、各 n 圈       →  O(n x n) = O(n^2)
// 每圈把问题砍半          →  O(log n)(二分、快速幂)
// 外层 n 圈、内层 m 圈    →  O(n x m)
// 外层 n、内层固定 k 圈   →  O(n)!k 是常数,忽略
// 串联执行(先后各跑一遍)→  取最大的:O(n^2) + O(n) = O(n^2)
// 2n + 50 圈             →  O(n)(常系数扔掉)
// 树图遍历:每点一次每边一次 →  O(n + m)

// ── 数数组:空间复杂度 ──
// 1 个 int = 4 字节
// int g[500][500]        →  25 万个 int x 4 B = 100 万 B ≈ 1 MB
// int a[1000000]         →  100 万个 int = 4 MB
// 递归套 n 层            →  调用栈 O(n)
✅ 小检查:① x = 10000; while (x > 1) x = x / 2; 一共跑几圈?② 外层 i 跑 n 圈、内层 j 跑 m 圈,复杂度?③ int g[500][500]; 占多少内存?
看看答案 ① 13 圈:10000→5000→2500→1250→625→312→156→78→39→19→9→4→2→1,数一下箭头是 13 次(log₂10000 ≈ 13.3,取 13)。② O(n × m)——两层圈数相乘,n、m 都大就是 O(nm)。③ 500 × 500 = 250000 个 int × 4 B = 1000000 B ≈ 1 MB。
易错:「两个循环必然 O(n²)」——只有两层圈数都跟 n 走才是 O(n²);内层固定跑 k 圈(k 是常数),整体还是 O(n)。

🧩 子任务 3:算法复杂度速查表

🎯 小目标:看到算法名字,直接说出它的复杂度——像认字一样熟。
// ── 查找 ──
// 顺序查找(逐个比)      →  O(n),最坏查遍全部
// 二分查找(必须有序)     →  O(log n),每次扔一半

// ── 排序 ──
// 冒泡 / 选择 / 插入      →  O(n^2),简单但慢
// 快速 / 归并 / 堆        →  O(n log n),竞赛主力

// ── 树 / 图 ──
// DFS / BFS 遍历          →  O(n + m):n 个点 + m 条边
// Dijkstra(数组版)       →  O(n^2)
// Floyd 全源最短路         →  O(n^3),n 别超几百
// Kruskal(排序选边)      →  O(m log m)

// ── 动态规划 ──
// DP = 状态数 x 每个状态的转移数
// 爬楼梯 n 级              →  O(n)
// 01 背包 n 件容量 v       →  O(n x v)

// ── 快慢排序(从小到大)──
// O(1) < O(log n) < O(n) < O(n log n) < O(n^2) < O(n^3) < O(2^n)
✅ 小检查:① 1000 个点 3000 条边的图跑一遍 DFS 多少步?② 500 个点要全源最短路,Floyd 多少步?③ 80 件物品、容量 1000 的 01 背包 DP 多少步?
看看答案 ① O(n + m) = 1000 + 3000 = 4000 步。② 500³ = 125000000 = 1.25 亿步 ≈ 1.25 秒,擦边能过(n = 1000 就 10 亿步超了)。③ 80 × 1000 = 80000 步,眨眼完成——DP 的账 = 状态数 × 转移数。
易错:「DP 都很快」——DP 的复杂度是状态数 × 转移数,状态一大(比如状态里带 n² 项)就 O(n³),别想当然当 O(n) 用。

🧩 子任务 4:估 TLE——n = 10 万时谁来得及

🎯 小目标:拿到 n 和算法,10 秒判断「过 / 超时」,并知道 n 多大该换什么算法。
// ── 裁判预算:每秒 10^8 步 ──
// n = 10^5 = 10 万时:
// O(n)        = 10^5            ✓ 0.001 秒
// O(n log n)  = 10^5 x 17 ≈ 1.7x10^6  ✓ 0.02 秒
// O(n^2)      = 10^10           ✗ 约 100 秒 → TLE
// O(n^3)      = 10^15           ✗ 10 万秒 → TLE

// ── 决策速查表(n 多大 → 极限复杂度)──
// n ≤ 10^2 (100)      →  O(n^3) 可用,O(n^2) 随便用
// n ≤ 10^3 ~ 10^4     →  O(n^2) 是极限,O(n^3) 危险
// n ≤ 10^5 ~ 10^6     →  O(n log n) 是极限,O(n^2) 超时
// n ≥ 10^6            →  只能 O(n) / O(log n) 级别

// ── 空间账本(128 MB 限制)──
// 1 个 int = 4 B
// 10^5 个 int   = 400 KB   ✓
// 10^6 个 int   = 4 MB     ✓
// 10^7 个 int   = 40 MB    紧但能过
// int[10^4][10^4] = 1 亿个 = 400 MB ✗ 爆
✅ 小检查:① n = 10 万、双层循环 O(n²)、限时 1 秒,能过吗?② n = 5000 时 O(n²) 和 O(n³) 各多少步、各能过吗?③ int a[10000000]; 占 40 MB,限制 16 MB,会怎样?
看看答案 ① 10^10 = 100 亿步 ÷ 10^8 = 100 秒 > 1 秒 → TLE,必须换成 O(n log n) 的算法。② O(n²) = 2500 万 ≈ 0.25 秒 ✓;O(n³) = 1.25 × 10^11 = 1250 亿 ≈ 125 秒 ✗。③ 40 MB > 16 MB → MLE(超内存)——空间账本和时间账本是两本独立的账,都要过。
易错:「我家电脑跑起来挺快的」——裁判机速度和你家不一样,一切以 10^8 步/秒估算;「10^10 离 10^8 不远」——差 100 倍,1 秒的预算跑 100 秒,裁判不等。

💻 完整代码:同一批数据,顺序查找 vs 二分查找,数比较次数

// 数据:a[1..20] = 10, 20, 30, ..., 200(从小到大,共 20 个)
// 任务:找 x = 200(在队尾),数一数两种方法各比较几次
#include <iostream>
using namespace std;

int main() {
    int a[21];
    for (int i = 1; i <= 20; i++) a[i] = i * 10;   // a[1]=10 ... a[20]=200
    int x = 200;

    // ── 方法 1:顺序查找(从头一个个比)──
    int pos1 = -1, cmp1 = 0;
    for (int i = 1; i <= 20; i++) {
        cmp1++;                              // 每比一次 +1
        if (a[i] == x) { pos1 = i; break; }
    }
    cout << "顺序查找:比较 " << cmp1 << " 次,位置 " << pos1 << endl;
    // 输出:顺序查找:比较 20 次,位置 20(200 在最末尾,比了全部 20 个)

    // ── 方法 2:二分查找(每次看中间,扔一半)──
    int low = 1, high = 20, pos2 = -1, cmp2 = 0;
    while (low <= high) {
        int mid = (low + high) / 2;
        cmp2++;                              // 每比一次 +1
        if (a[mid] == x) { pos2 = mid; break; }
        else if (a[mid] < x) low = mid + 1;  // 中间比目标小 → 目标在右半
        else high = mid - 1;                 // 中间比目标大 → 目标在左半
    }
    cout << "二分查找:比较 " << cmp2 << " 次,位置 " << pos2 << endl;
    // 输出:二分查找:比较 5 次,位置 20
    // 过程:mid=10(100<200)→右;mid=15(150<200)→右;mid=18(180<200)→右;
    //        mid=19(190<200)→右;mid=20(200==200)→命中!共 5 次

    // 把 x 换成 10(在队首)再想想:顺序查找 1 次就命中;二分查找 mid=10→5→2→1
    // 要 4 次——「目标靠前时线性更快」。但最坏情形(目标在最末或不存在)
    // 线性要 20 次、二分只要 5 次:n 越大差距越悬殊(n=10 万时 10 万 vs 17)。
    return 0;
}
// 运行结果:
// 顺序查找:比较 20 次,位置 20
// 二分查找:比较 5 次,位置 20

✋ 你来试试

第 1 题:n = 10 万,算法是 O(n²),限时 1 秒,能过吗? O(n²) = 10^10 = 100 亿步,÷ 10^8 步/秒 ≈ 100 秒 > 1 秒 → TLE。得换 O(n log n)(≈170 万步,0.02 秒)甚至 O(n) 的算法。
第 2 题:给 100 万个数排序,冒泡 O(n²) vs 快排 O(n log n),各多少秒?快多少倍?你选谁? 冒泡:10^12 = 1 万亿步 ≈ 10000 秒(约 2.8 小时);快排:log₂10^6 ≈ 20,10^6 × 20 = 2×10^7 步 ≈ 0.2 秒。10^12 ÷ 2×10^7 = 50000 倍——选快排。
第 3 题:1 万个点、5 万条边的图,DFS 走一遍多少步?1 秒够吗? O(n + m) = 10000 + 50000 = 60000 步 ≈ 0.0006 秒,绰绰有余——图算法的账是「点 + 边」,不是 n²。
第 4 题(加餐):二分查找为什么必须有序?乱序的数组为什么不能直接二分? 二分「扔一半」靠的是大小关系:mid 左边的数都 ≤ a[mid]、右边都 ≥ a[mid],所以「a[mid] < x」就能断定目标一定在右半。乱序时这个保证没了——目标可能正好被你扔掉的半区里,扔错了就找不到。
🎯 一句话记住:复杂度 = 数步子的账本——一层循环 O(n)、两层 O(n²)、每次减半 O(log n),常系数扔掉;预算 10^8 步/秒:n = 10 万时 O(n log n) 能过、O(n²) 要 100 秒超时;空间账本独立算:1 个 int = 4 B,10^7 个 int = 40 MB;二分最坏只 log n 步(20 个数只要 5 次),顺序查找最坏 n 步。