算法时间复杂度:给小白的一堂"算快慢"课

不需要数学基础,用大白话讲清楚:为什么有的代码跑得快,有的跑得慢?怎么一眼看出代码的"速度等级"?


一、什么是"时间复杂度"?一句话讲清楚

想象你要从 1 数到 100:

  • 方式A :一个一个数:1, 2, 3, 4... 要数 100 下
  • 方式B:先数到 50,再数到 75,再数到 88... 每次砍掉一半

时间复杂度 就是描述:当数字变得超级超级大时,你需要"操作"多少次

💡 记住一句话:时间复杂度不看实际秒数,只看"操作次数随数据量变大的增长趋势"

为什么要用大 O?

因为电脑有快有慢,但算法的"快慢基因"是不变的。就像:

  • 乌龟和兔子比赛,兔子永远比乌龟快
  • 但如果是 O(n²) 的算法,数据量翻倍时,它永远比 O(n) 的算法慢得多(不管跑在超级计算机还是老爷机上)

常见复杂度排名(从快到慢):

scss 复制代码
O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2ⁿ)

就像游戏里的段位:青铜 → 白银 → 黄金 → 铂金 → 钻石 → 王者(越往后越慢)


二、非递归算法:数循环,就像数糖果

非递归算法最常见的就是循环 。分析它,就像数你吃了多少颗糖果

2.1 单层循环:O(n) ------ 一人发一颗糖

java 复制代码
for (int i = 0; i < n; i++) {
    吃一颗糖();
}

有 n 个人,每人发一颗糖 ,总共发了 n 颗

→ 时间复杂度:O(n)(线性,像一条直线往上走)


2.2 双层循环:O(n²) ------ 每两个人握一次手

java 复制代码
for (int i = 0; i < n; i++) {
    for (int j = 0; j < n; j++) {
        握手();
    }
}

班里有 n 个同学,每个人都和所有人握一次手

第1个人握 n 次,第2个人握 n 次... 总共 n × n = n² 次。

→ 时间复杂度:O(n²)(平方,增长很快!)

🌰 实际例子:冒泡排序,两两比较,就是 O(n²)


2.3 对数循环:O(log n) ------ 折纸游戏

java 复制代码
for (int i = 1; i < n; i *= 2) {
    操作();
}

想象你有一张纸,每次对折(乘以2),问:对折多少次能超过 n 张纸的厚度?

  • 1 次 → 2 层
  • 2 次 → 4 层
  • 3 次 → 8 层
  • k 次 → 2^k 层

2^k = n 时,k = log₂n

→ 时间复杂度:O(log n)(超快!像折纸一样指数级减少操作)

🌰 实际例子:二分查找,每次砍掉一半数据


2.4 常见模式速查(小白版)

代码样子 像什么 复杂度 为什么
单层 for 循环 排队点名 O(n) 每人点一次
两层 for 循环 全班互相握手 O(n²) n×n 次
循环变量 ×2 折纸翻倍 O(log n) 每次翻倍,次数=log
外层 n,内层 log n 点名+每次折纸 O(n log n) n × log n
没有循环,直接返回 一眼看到答案 O(1) 固定时间

三、递归算法:像俄罗斯套娃

递归就是函数自己调用自己,就像俄罗斯套娃:打开一个,里面还有一个更小的...

分析递归算法,核心就一句话:把问题拆成多小的子问题?每个子问题要花多少时间?

3.0 先搞懂递推式

写递推式就像写"账单":

scss 复制代码
T(n) = 分成几个小问题 + 合并这些小问题要花的时间

比如归并排序:

  • 把数组切成两半 → 2 个子问题
  • 每个子问题规模是原来的一半 → n/2
  • 合并两个有序数组 → 花 n 的时间

所以递推式是:T(n) = 2T(n/2) + n


3.1 展开法:像剥洋葱,一层层剥开

适合:简单的递推,比如每次只解决一个子问题。

例子:计算 n 的阶乘

java 复制代码
int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);  // 自己调用自己
}

递推式:T(n) = T(n-1) + 1(算完 n-1 的阶乘,再乘个 n,花 1 步)

展开过程就像剥洋葱

scss 复制代码
T(n) = T(n-1) + 1                    ← 剥开第1层
     = [T(n-2) + 1] + 1              ← 剥开第2层
     = T(n-2) + 2
     = [T(n-3) + 1] + 2              ← 剥开第3层
     = T(n-3) + 3
     = ...
     = T(1) + (n-1)                  ← 剥到最后
     = 1 + n - 1
     = n

→ 时间复杂度:O(n)

💡 口诀:每次规模减 1,总共剥 n 层,就是 O(n)


3.2 递归树法:像画一棵"公司组织架构图"

适合:分成多个子问题的情况(比如分成2份、3份)

例子 :归并排序 T(n) = 2T(n/2) + n

想象一家公司:

  • CEO 管 n 个人的大项目(花 n 时间做整合)
  • CEO 把项目分给 2个经理,每人管 n/2 个人
  • 经理再分给 4个组长,每人管 n/4 个人
  • ...直到最底层的 员工,每人管 1 个人
css 复制代码
                    CEO [n]                    ← 第0层,花 n 时间
                   /     \
              经理A[n/2]  经理B[n/2]           ← 第1层,花 n/2 + n/2 = n
             /    \      /    \
          组长...      组长...               ← 第2层,还是花 n

关键发现

  • 每一层花的总时间都是 n(不管分得多细,加起来都是 n)
  • 一共有多少层?每次规模减半,从 n 减到 1,需要 log₂n
  • 总时间 = 每层时间 × 层数 = n × log n

→ 时间复杂度:O(n log n)

💡 口诀:每层代价一样,数有几层,乘起来就是答案


3.3 代入法:先猜后证(面试装逼用)

适合:你已经大概知道答案,需要严谨证明。

步骤

  1. 猜一个答案(凭直觉)
  2. 用数学归纳法验证

例子:证明归并排序是 O(n log n)

T(n) ≤ cn log n(c 是某个常数)

验证

scss 复制代码
假设对于比 n 小的问题都成立

T(n) = 2T(n/2) + n
     ≤ 2 × c(n/2)log(n/2) + n    ← 把假设代进去
     = cn(log n - 1) + n
     = cn log n - cn + n
     = cn log n - (c-1)n

只要 c ≥ 1,最后一项就是负数或零,T(n) ≤ cn log n 成立!

💡 小白可以跳过这一步,知道有这种方法就行。面试时如果面试官让你"严格证明",再用这招。


3.4 主定理:递归分析的"作弊码" ⭐

这是最实用的方法,记住三个规则,大部分递归题秒出答案!

递推式的标准形式

scss 复制代码
T(n) = a × T(n/b) + O(n^d)
字母 什么意思 生活比喻
a 每次分成几个子问题 一个经理管几个下属
b 子问题规模是原来的几分之一 每个下属管多少比例的人
d 合并子问题的"工作量等级" 经理整合报告要花多大精力

三个规则(记住这个表!)

先算一个数:log_b(a)(即以 b 为底 a 的对数)

情况 条件 结果 直观理解
规则1 d < log_b(a) O(n^(log_b a)) 下属太多,"分"比"合"累
规则2 d = log_b(a) O(n^d × log n) 分和合一平衡,每层一样
规则3 d > log_b(a) O(n^d) 合并太费劲,"合"比"分"累

💡 超级口诀 :比较 dlog_b(a),谁大听谁的,一样大就乘个 log n


实战:用主定理秒算常见算法

🌰 案例1:二分查找

java 复制代码
int binarySearch(int[] arr, int target, int l, int r) {
    if (l > r) return -1;
    int mid = (l + r) / 2;
    if (arr[mid] == target) return mid;
    if (arr[mid] > target) 
        return binarySearch(arr, target, l, mid-1);  // 只走左边
    else 
        return binarySearch(arr, target, mid+1, r);  // 只走右边
}

分析:

  • 每次只走 1 个子问题a = 1
  • 数据规模减半 → b = 2
  • 找中间点 + 比较 → O(1)d = 0

计算:log₂(1) = 0,而 d = 0规则2

结果:O(n⁰ × log n) = O(log n)


🌰 案例2:归并排序

  • 分成 2 个子问题a = 2
  • 每个子问题规模减半 → b = 2
  • 合并两个数组 → O(n)d = 1

计算:log₂(2) = 1,而 d = 1规则2

结果:O(n¹ × log n) = O(n log n)


🌰 案例3:二叉树遍历

java 复制代码
void traverse(TreeNode root) {
    if (root == null) return;
    traverse(root.left);
    traverse(root.right);
    // 访问当前节点 O(1)
}
  • 每个节点产生 2 个子问题 (左右子树)→ a = 2
  • 子树规模大约减半 → b = 2
  • 访问节点 → O(1)d = 0

计算:log₂(2) = 1,而 d = 0d < log_b(a)规则1

结果:O(n^(log₂2)) = O(n¹) = O(n)

💡 虽然每个节点访问一次,但主定理算出来也是 O(n),吻合!


🌰 案例4:快速排序(理想情况)

  • 分成 2 个子问题a = 2
  • 子问题规模大约各半 → b = 2
  • 分区操作 → O(n)d = 1

计算:log₂(2) = 1 = d规则2

结果:O(n log n)


🌰 案例5:斐波那契(朴素递归)

java 复制代码
int fib(int n) {
    if (n <= 1) return n;
    return fib(n-1) + fib(n-2);
}
  • 每次产生 2 个子问题a = 2
  • 但子问题规模是 n-1n-2不是除以 b

⚠️ 主定理用不了! 因为子问题规模不是"按比例缩小"的。

用展开法/递归树法分析,会发现它像一棵"歪脖子树",节点数接近 2ⁿ

→ 时间复杂度:O(2ⁿ)(超慢!)

💡 这就是为什么斐波那契要用动态规划优化到 O(n)


四、什么时候主定理"失灵"?

主定理很强,但不是万能钥匙。以下情况别用

情况 例子 该用什么方法
子问题规模不按比例 T(n)=T(n-1)+T(n-2) 递归树/展开法
合并代价不是多项式 T(n)=2T(n/2)+n log n 递归树法
子问题规模不一样 T(n)=T(n/2)+T(n/4)+n 递归树法

五、一张图记住所有常见算法

scss 复制代码
速度(快 → 慢)

O(1)        ████                    数组按下标取值
O(log n)    ████████                二分查找
O(n)        ████████████████        遍历数组
O(n log n)  ████████████████████    归并排序、快排
O(n²)       ████████████████████████████  冒泡排序
O(2ⁿ)       ████████████████████████████████████████████████  斐波那契递归

六、给小白的学习路径

  1. 先学会数循环:单层 O(n),双层 O(n²),折半 O(log n)
  2. 理解递归的本质:自己调用自己,像套娃
  3. 掌握展开法:像剥洋葱,一层层展开
  4. 会用递归树:画树,算每层代价,数层数
  5. 背熟主定理三个规则:面试秒杀 90% 的递归复杂度题
  6. 知道主定理的边界:不是所有递归都能用

七、课后练习(测测你学会了没)

题目1:下面代码的时间复杂度是多少?

java 复制代码
for (int i = 0; i < n; i++) {
    for (int j = i; j < n; j++) {
        System.out.println("hi");
    }
}

点击查看答案

内层循环从 i 开始到 n,总次数是 n + (n-1) + (n-2) + ... + 1 = n(n+1)/2

O(n²)(虽然有优化,但最高阶还是 n²)


题目2:用主定理分析下面递归:

java 复制代码
void func(int n) {
    if (n == 1) return;
    func(n / 3);
    func(n / 3);
    func(n / 3);
    // 合并操作 O(n²)
}

点击查看答案

  • a = 3(3个子问题)
  • b = 3(规模变为 1/3)
  • d = 2(合并是 O(n²))

计算:log₃(3) = 1,而 d = 2

1 < 2规则3

O(n²)


题目3(思考题):为什么下面代码主定理用不了?

java 复制代码
void weird(int n) {
    if (n == 1) return;
    weird(n - 1);
    weird(n - 1);
}

点击查看答案

子问题规模是 n-1,不是 n/b(不是按比例缩小,而是减1)。

需要用递归树法:每层节点数翻倍,深度为 n,总节点数 ≈ 2ⁿ

O(2ⁿ)


八、结语

时间复杂度分析不是数学竞赛,而是培养对代码"速度基因"的直觉

记住几个关键点:

  • 循环就数执行次数
  • 递归就写递推式
  • 分治型递归用主定理秒算
  • 不是分治型就用展开法或递归树

多练几道真题,你会发现:原来时间复杂度分析,真的没那么难!😊


如果这篇文章帮到你,欢迎点赞收藏,有问题评论区见!

相关推荐
vivo互联网技术16 分钟前
TinySR:面向真实世界图像超分辨率的轻量级扩散模型
人工智能·算法
Nturmoils28 分钟前
内网穿透原来这么简单:Natapp 从注册到公网访问完整教程
后端
寻求出路的程序媛40 分钟前
分布式 & 高性能 & 高可用 体系、学习重点、面试点
分布式·后端·面试·性能优化
SimonKing42 分钟前
GenOffice上手指南:免费替代Word+PPT+Excel的AI办公神器
java·后端·程序员
不会就选b1 小时前
数据结构之栈的算法题(OJ)
linux·数据结构·算法
掘金者阿豪1 小时前
Let‘s Encrypt 证书到底会不会自动续期?从一次服务器迁移后的证书排查说起
后端
Zane19941 小时前
手写"判断key存不存在"的模板代码太烦?一文讲透 collections 四件套
后端·python
joinwell521 小时前
Agent 中断后,原任务如何安全接管?从恢复标记到效果事实
人工智能·后端·架构
人工智能培训1 小时前
人工智能性别与地域偏见的成因及消解路径
大数据·人工智能·算法·生活