这是 数学重学路线图 阶段三的子页面
📌 相关笔记:函数与图像
函数增长与大O记号
为什么要学这个?
选错数据结构或算法,数据量小的时候看不出区别
一旦数据量上去(百万、千万、上亿),系统直接崩掉
面试必考,工作中做技术选型的核心依据
真实场景:你用了 O(n²) 的算法处理100万条日志,要跑11.5天;换成 O(n log n) 只要20秒
大O 不是学院派玩具,它是工程师的生存工具
核心概念:什么是大O?
一句话:大O描述的是”当输入规模 n 趋向无穷时,算法运行时间的增长趋势”
直觉比喻:
你有一堆快递要送,O(1) = 无人机直投(不管多少件都一次搞定)
O(n) = 骑电动车一件件送
O(n²) = 每送一件都要回仓库重新找一遍
O(2^n) = 每增加一件快递,工作量翻倍
核心思想:忽略常数和低阶项,只关心增长的速度
3n² + 5n + 100 → O(n²)
1000n → O(n),虽然常数大但增长慢
0.001 × 2^n → O(2^n),常数再小指数增长也吓人
常见复杂度排序
O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2^n) < O(n!)
从左到右,增长越来越快,算法越来越慢
直觉理解:n = 1,000,000 时的操作次数
1 | | 复杂度 | 操作次数 | 直觉感受 | 典型场景 | |
关键洞察:O(n²) 和 O(n log n) 看起来只差一点,但在百万级数据上差了 50000 倍
大O 的数学定义(简化版)
f(n) = O(g(n)) 意思是:存在常数 C 和 n₀,当 n > n₀ 时,f(n) ≤ C × g(n)
翻译:f(n) 的增长速度不会超过 g(n)(忽略常数倍)
例子:
3n² + 5n + 100 = O(n²),因为当 n 够大时,3n² + 5n + 100 ≤ 4n²
100n = O(n²) 也成立(上界可以松一点),但我们一般取最紧的上界
相关记号:
Ω(n²):下界,至少这么慢
Θ(n²):紧界,就是这么快/慢
日常口语中说的”复杂度是 O(n²)”通常指的是 Θ(n²)
常见数据结构操作复杂度表
1 | | 数据结构 | 查找 | 插入 | 删除 | 备注 | |
如何分析代码复杂度
规则1:单层循环 → O(n)
1 | for i in range(n): # O(n) |
规则2:嵌套循环 → 相乘
1 | for i in range(n): # O(n) |
规则3:顺序执行 → 取最大
1 | for i in range(n): # O(n) |
规则4:循环内做二分 → O(n log n)
1 | for i in range(n): # O(n) |
规则5:递归用主定理
T(n) = aT(n/b) + O(n^d)
如果 d > log_b(a):O(n^d)
如果 d = log_b(a):O(n^d × log n)
如果 d < log_b(a):O(n^(log_b(a)))
归并排序:T(n) = 2T(n/2) + O(n) → a=2, b=2, d=1 → d=log₂2=1 → O(n log n)
规则6:循环变量每次翻倍/减半 → O(log n)
1 | i = 1 |
后端应用
为什么 MySQL 索引用 B+树 (O(log n)) 不用链表 (O(n))?
1000万条记录:B+树查找约 3-4 次磁盘IO,链表平均 500万次
数据库最怕磁盘IO,B+树的矮胖结构(高扇出)把高度压到极低
为什么 Redis 有序集合用跳表不用红黑树?
两者查找都是 O(log n)
跳表实现更简单、范围查询更方便、并发友好
API 分页设计:
OFFSET 分页:越往后越慢 O(offset + limit)
游标分页:始终 O(limit),利用索引
缓存:把 O(n) 的数据库查询变成 O(1) 的缓存命中
大数据应用
MapReduce 为什么能处理 PB 级数据?
单机排序 1TB:O(n log n),但 n 太大内存放不下
1000台机器各排序 1GB:每台 O(n/k × log(n/k)),然后归并
分治思想把不可能变成可能
分布式排序:外部排序的复杂度 = O(n log n) 但实际瓶颈在 IO
Spark 的 Shuffle:本质是分布式的 O(n) 数据重分布
安全应用
暴力破解复杂度:
8位纯数字密码:10^8 = 1亿种,O(10^8),几分钟就破
8位大小写+数字+特殊字符:95^8 ≈ 6.6×10^15,O(10^15),要好几年
这就是为什么密码要求复杂度
bcrypt 故意设计慢:
MD5 哈希一个密码:微秒级
bcrypt(cost=12):约 250ms
暴力破解从”几小时”变成”几百年”
密码学安全依赖计算复杂度:
RSA 安全性基于大数分解的困难(目前没有多项式时间算法)
AES-128:暴力 2^128 ≈ 3.4×10^38 次,人类算力不够
Python 代码:实测不同复杂度的运行时间
1 | import time |
Python 代码:增长曲线可视化
1 | import matplotlib.pyplot as plt |
常见误区
误区1:O(n) 一定比 O(n²) 快
错!O(100n) 在 n < 100 时比 O(n²) 慢
大O是渐近分析,小数据量常数项也重要
误区2:大O就是运行时间
大O描述的是增长率,不是绝对时间
同样 O(n) 的算法,一个可能比另一个快10倍
误区3:空间复杂度不重要
很多时候空间比时间更关键(内存有限、缓存局部性)
例如:归并排序 O(n) 额外空间 vs 快排 O(log n) 额外空间
误区4:最坏情况就是实际情况
快排最坏 O(n²),但平均 O(n log n),实际中非常快
哈希表最坏 O(n),但平均 O(1)
练习题
练习1:分析以下代码的时间复杂度
1 | def mystery(n): |
答案:外层 while 循环 i 每次翻倍 → O(log n) 次,内层 for 循环 O(n),总共 O(n log n)
练习2:一个数据库表有5000万条记录,用B+树索引查找一条记录大约需要几次磁盘IO?
答案:log₂(50000000) ≈ 25.5,但B+树每个节点有几百个key(假设扇出500),所以高度 ≈ log₅₀₀(50000000) ≈ 3 层。约 3-4 次磁盘IO。
练习3:你的API响应时间随用户数线性增长,当1000用户时响应50ms,10万用户时预期多少?
答案:线性增长 O(n),用户数增长100倍,响应时间也增长100倍 → 50ms × 100 = 5000ms = 5秒。这已经不可接受了,需要优化(缓存、索引、分库分表)。
练习4:为什么说”密码每多一位,暴力破解难度就指数增长”?
答案:假设每位有 k 种可能字符。n 位密码总组合 = k^n。每多一位乘以 k,这是指数增长 O(k^n)。比如 k=62(大小写+数字),8位有 2.18×10^14 种,9位有 1.35×10^16 种,多一位就多62倍。
练习5:以下操作分别是什么复杂度?
(a) Python 的 list.append(x) → 答案:平摊 O(1)
(b) Python 的 list.insert(0, x) → 答案:O(n),所有元素要后移
(c) Python 的 x in set_obj → 答案:平均 O(1),哈希查找
(d) Python 的 x in list_obj → 答案:O(n),线性查找
(e) sorted(list_obj) → 答案:O(n log n),Timsort
| 上一章 | 目录 | 下一章 |
|---|---|---|
| 13-排列组合与计数 | 数学重学路线图 | 15-求和公式与级数 |