大O表示法与算法复杂度分析
大O表示法与算法复杂度分析
一、为什么需要复杂度分析
当我们面对同一个问题有多种算法可选时,如何判断哪个更"好"?最直觉的方式是写代码、运行、计时比较——但这种方式有严重缺陷:
- 硬件差异:同一算法在不同机器上运行时间完全不同
- 数据规模依赖:小数据看不出差异,大数据才暴露瓶颈
- 语言/编译器影响:Go 和 Python 运行同一逻辑速度天差地别
所以我们需要一种脱离具体运行环境、只关注算法本身效率的度量方式——这就是复杂度分析。
二、时间复杂度
2.1 什么是时间复杂度
时间复杂度衡量的是:随着输入规模 n 增大,算法执行基本操作的次数增长率。
它不是精确计时,而是回答一个定性问题:"当数据量翻倍时,算法的运行时间大致翻倍?翻四倍?还是几乎不变?"
2.2 大O表示法(Big-O Notation)
大O表示法描述的是算法的渐进上界(asymptotic upper bound),即:在最坏情况下,算法执行次数的增长趋势。
数学定义:如果存在常数 c 和 n₀,使得对所有 n ≥ n₀,都有 T(n) ≤ c × f(n),则称 T(n) = O(f(n))。
直观理解:大O给出了一个"不超过"的承诺——运行时间不会比 O(f(n)) 更差。
2.3 大O的简化规则
-
只保留最高阶项
3n³ + 2n + 100→O(n³)因为当 n 足够大时,n³ 远远大于 n 和常数,低阶项可以忽略。
-
去掉最高阶项的系数
5n² + 3n→O(n²)(不是 O(5n²))系数对增长趋势的影响可以忽略——重要的是"按什么速度增长",不是"跑多快"。
-
常数都简化为 O(1)
100→O(1)无论常数多大,只要不随 n 变化,就是 O(1)。
2.4 常见复杂度等级(从快到慢)
| 复杂度 | 名称 | n=1000 时的操作次数 | 典型算法 |
|---|---|---|---|
| O(1) | 常数 | 1 | 数组按下标访问、哈希表查找 |
| O(log n) | 对数 | ~10 | 二分查找、平衡BST操作 |
| O(n) | 线性 | 1000 | 遍历数组/链表、线性查找 |
| O(n log n) | 线性对数 | ~10000 | 快排、归并排序、堆排序 |
| O(n²) | 平方 | 1000000 | 冒泡排序、选择排序、双重循环 |
| O(n³) | 立方 | 1000000000 | 三重嵌套循环、矩阵乘法朴素版 |
| O(2ⁿ) | 指数 | 极大 | 穷举子集、递归斐波那契 |
| O(n!) | 阶乘 | 极大 | 全排列穷举 |
关键认知:O(n²) 到 O(n log n) 是质变。n=100万时,n²=10¹²(不可接受),而 n log n ≈ 2×10⁷(可接受)。
2.5 如何分析一段代码的复杂度
规则一:顺序执行 → 取最大
// O(n) + O(1) → O(n)
for i := 0; i < n; i++ { ... } // O(n)
x := arr[0] // O(1)
规则二:嵌套循环 → 相乘
// O(n) × O(n) → O(n²)
for i := 0; i < n; i++ { // O(n)
for j := 0; j < n; j++ { // O(n)
...
}
}
规则三:条件分支 → 取最坏
if condition {
// O(n)
} else {
// O(1)
}
// → O(n)(取最坏情况)
2.6 对数复杂度的理解
为什么二分查找是 O(log n)?
每一步将搜索范围缩小一半:n → n/2 → n/4 → ... → 1
需要多少步才能从 n 缩到 1?答案是 log₂n 步。
在算法分析中,O(log n) 通常不标注底数——因为 logₐn = logₐb × log_bn,换底只差一个常数系数,大O里系数可以忽略。
2.7 实战分析示例
func example(arr []int) {
n := len(arr)
// 片段A: O(n²)
for i := 0; i < n; i++ {
for j := 0; j < n; j++ {
fmt.Println(arr[i] + arr[j])
}
}
// 片段B: O(n log n) —— 二分查找在循环中
for i := 0; i < n; i++ {
binarySearch(arr, arr[i]) // O(log n)
}
// 片段C: O(1)
fmt.Println(arr[0])
// 总复杂度: O(n²) + O(n log n) + O(1) → O(n²)
}
三、空间复杂度
3.1 什么是空间复杂度
空间复杂度衡量的是:算法在运行过程中额外占用的存储空间随 n 的增长率。
注意:输入数据本身占用的空间不算在内——我们只关心算法为了处理输入而额外开辟了多少空间。
3.2 常见等级
| 空间复杂度 | 说明 | 例子 |
|---|---|---|
| O(1) | 不随 n 变化 | 冒泡排序(原地交换)、快排原地版 |
| O(n) | 与 n 线性增长 | 归并排序(需要临时数组)、哈希表 |
| O(n²) | 与 n² 增长 | 二维 DP 表、图的邻接矩阵 |
| O(log n) | 对数级增长 | 递归调用栈深度(如二分查找递归版) |
3.3 Go 中的空间考量
Go 中切片的底层数组是引用类型,但 make([]int, n) 会分配 O(n) 的空间。递归函数的空间复杂度 = 递归深度 × 每层的栈帧大小。
// 空间 O(1) —— 原地操作
func inplaceSwap(arr []int, i, j int) {
arr[i], arr[j] = arr[j], arr[i]
}
// 空间 O(n) —— 需要辅助数组
func mergeSort(arr []int) []int {
if len(arr) < 2 {
return arr
}
// 每层递归都需要临时数组
mid := len(arr) / 2
left := mergeSort(arr[:mid])
right := mergeSort(arr[mid:])
return merge(left, right) // merge 内部 make([]int, 0) 逐步增长
}
// 空间 O(log n) —— 递归深度
func binarySearchRec(arr []int, target, lo, hi int) int {
if lo > hi {
return -1
}
mid := lo + (hi-lo)/2
if arr[mid] == target {
return mid
}
if arr[mid] > target {
return binarySearchRec(arr, target, lo, mid-1)
}
return binarySearchRec(arr, target, mid+1, hi)
}
四、最好、最坏、平均复杂度
4.1 三种情况
| 类型 | 含义 | 例子 |
|---|---|---|
| 最好情况 | 输入最有利时的复杂度 | 插入排序对已排好序的数组 → O(n) |
| 最坏情况 | 输入最不利时的复杂度 | 插入排序对逆序数组 → O(n²) |
| 平均情况 | 所有等可能输入的期望复杂度 | 插入排序平均 → O(n²) |
4.2 为什么通常用最坏情况
- 最坏情况给出了安全承诺——算法不会比这更慢
- 平均情况需要假设输入的分布,这在实际中往往无法确定
- 很多算法的最坏情况和平均情况相同(如冒泡排序始终 O(n²))
4.3 一个特例:快速排序
- 最好情况:O(n log n)(每次 pivot 刚好中分)
- 最坏情况:O(n²)(每次 pivot 都是最大/最小值)
- 平均情况:O(n log n)
这说明快速排序虽然理论最坏是 O(n²),但在实践中绝大多数时候都是 O(n log n),所以它仍是实际中最快的通用排序算法。
五、复杂度分析实战练习
5.1 练习一:分析下面函数的复杂度
func mystery(n int) int {
count := 0
for i := 1; i < n; i *= 2 { // i 按2倍增长: 1,2,4,8,... → O(log n)
count++
}
return count
}
答案:O(log n),因为循环变量每次翻倍,执行次数 = log₂n
5.2 练习二:分析嵌套循环
func nested(n int) {
for i := 0; i < n; i++ { // O(n)
for j := i; j < n; j++ { // 内层执行 (n-i) 次
fmt.Println(i, j)
}
}
}
内层总执行次数 = n + (n-1) + (n-2) + ... + 1 = n(n+1)/2 → O(n²)
5.3 练习三:递归复杂度
func fib(n int) int {
if n <= 1 {
return n
}
return fib(n-1) + fib(n-2)
}
调用树呈二叉树展开,总节点数 ≈ 2ⁿ → O(2ⁿ)(指数级,极慢)
六、小结
大O表示法是算法分析的基石。掌握它需要记住几个核心要点:
- 大O看趋势不看细节:忽略常数和低阶项,只保留增长最快的那个
- O(n²) 和 O(n log n) 是关键分水岭:前者在大数据下不可接受
- 嵌套循环相乘、顺序语句取最大、分支取最坏——三条分析规则覆盖绝大多数场景
- 空间和时间同等重要:有些算法时间好但空间差,需要权衡

浙公网安备 33010602011771号