时间复杂度描述算法运行时间随输入规模增长的变化趋势,用大O记号表示算法的最坏或平均情况。
1. 时间复杂度核心概念
4种常见复杂度
O(1): 常数时间,与输入规模无关
O(log n): 对数时间,输入翻倍,操作+1
O(n): 线性时间,输入翻倍,时间翻倍
O(n²): 平方时间,输入翻倍,时间×4
计算规则
// 1. 只保留最高阶项
O(3n² + 2n + 1) = O(n²)
// 2. 忽略常数系数
O(2n) = O(n)
O(100) = O(1)
// 3. 考虑最坏情况
// 比如查找可能在第一个或最后一个找到
2. O(1) 常数时间
定义
操作时间不随输入规模变化
vector 随机访问,这里的"随机"是任意、随时、不按顺序的意思。
cpp
std::vector<int> vec = {1, 2, 3, 4, 5, ..., 100 0000};
int value = vec[500000]; // 直接计算地址访问
// 计算过程:
地址 = 起始地址 + 索引 × 元素大小
地址 = 0x1000 + 500000 × 4 = 0x1E8480
// 一次计算,一次内存访问
为什么是 O(1):
-
数组连续存储
-
地址 = 基址 + 索引×元素大小
-
计算复杂度与 n 无关
其他 O(1) 操作
cpp
// 1. 哈希表查找(平均)
std::unordered_map<int, string> m;
auto it = m.find(key); // 平均 O(1)
// 2. 栈操作
stack.top(); // O(1)
stack.empty(); // O(1)
栈操作 O(1) 是因为只操作栈顶
// 3. 链表插入(特定位置)
auto it = list.begin();
list.insert(it, value); // 已知位置,O(1)
3. O(n) 线性时间
定义
操作时间与输入规模成正比
vector 中间插入
std::vector<int> vec = {1, 2, 3, 4, 5};
// 在位置2插入99
vec.insert(vec.begin() + 2, 99);
// 需要移动元素3,4,5
移动元素过程:
原始: [1, 2, 3, 4, 5]
↑ 插入位置
移动: 3→4, 4→5, 5→新位置
结果: [1, 2, 99, 3, 4, 5]
// 移动了 n-2 个元素
计算复杂度:
最坏情况:在开头插入
移动 n 个元素 → O(n)
平均情况:在中间插入
移动 n/2 个元素 → O(n)
顺序查找
// 在无序数组中查找
bool linear_search(const vector<int>& v, int target) {
for (int i = 0; i < v.size(); ++i) { // 循环 n 次
if (v[i] == target) return true;
}
return false;
}
// 遍历所有元素,最多 n 次比较
数学表达:
比较次数:1, 2, 3, ..., n
平均:n/2
最坏:n
→ O(n)
4. O(log n) 对数时间
定义
每步将问题规模减半
二分查找
vector<int> v = {1, 3, 5, 7, 9, 11, 13}; // 已排序
int target = 7;
// 查找过程:
[1, 3, 5, 7, 9, 11, 13] mid=7 ✓
↑
找到!比较1次
查找 1:
[1, 3, 5, 7, 9, 11, 13] mid=7 > 1
[1, 3, 5] mid=3 > 1
[1] mid=1 ✓
// 比较3次
数学推导:
每次比较后,范围减半
n → n/2 → n/4 → ... → 1
需要 k 次比较:
n / 2^k = 1
2^k = n
k = log₂(n)
→ O(log n)
实际计算:
n=8: log₂8 = 3次
n=1024: log₂1024 = 10次
n=1000000: log₂1000000 ≈ 20次
// 输入百万,仅需20次比较!
5. O(n log n) 对数线性时间
定义
执行 n 次 O(log n) 操作
快速排序:每个递归层级处理的元素总数是 Θ(n),递归深度平均是 Θ(log n)
vector<int> v = {5, 2, 8, 1, 9, 3};
std::sort(v.begin(), v.end());
分治过程:
原始: [5, 2, 8, 1, 9, 3]
1. 分区: [2, 1, 3] 5 [8, 9] // 比较5次
2. 递归左: [1, 2, 3] // 比较2次
3. 递归右: [8, 9] // 比较1次
总计: 5+2+1 = 8次比较
复杂度分析:
每个递归层级处理的元素总数是 Θ(n)的原因:

6. O(n²) 平方时间
定义
操作时间与输入规模的平方成正比
冒泡排序
void bubble_sort(vector<int>& v) {
int n = v.size();
for (int i = 0; i < n-1; ++i) { // n-1 次
for (int j = 0; j < n-i-1; ++j) { // 平均 n/2 次
if (v[j] > v[j+1]) {
swap(v[j], v[j+1]);
}
}
}
}
比较次数:
外层循环: i = 0 到 n-2 → n-1 次
内层循环: 当 i=0 时比较 n-1 次
i=1 时比较 n-2 次
...
i=n-2 时比较 1 次
总计比较:
(n-1) + (n-2) + ... + 1
= n(n-1)/2
= (n² - n)/2
→ O(n²)
实际计算:
n=10: 比较 45 次
n=100: 比较 4950 次
n=1000: 比较 499500 次 ≈ 50万
7. vector /list 操作复杂度分析
vector
// 1. 末尾添加
vec.push_back(value); // 平摊 O(1)
// 偶尔扩容 O(n),但平摊到每次是 O(1)
// 2. 随机访问
vec[i]; // O(1)
// 3. 中间插入
vec.insert(pos, value); // O(n)
// 4. 查找
auto it = find(vec.begin(), vec.end(), x); // O(n)
list 操作
// 1. 插入(已知位置)
list.insert(it, value); // O(1)
// 但找到位置可能需要 O(n)
// 2. 随机访问
// 没有索引操作,必须遍历
auto it = std::next(list.begin(), k); // O(k)
// 3. 查找
auto it = find(list.begin(), list.end(), x); // O(n)
8. 复杂度对比表
| 复杂度 | n=10 | n=100 | n=1000 | n=10000 | 例子 |
|---|---|---|---|---|---|
| O(1) | 1 | 1 | 1 | 1 | 数组访问 |
| O(log n) | 3.3 | 6.6 | 10 | 13.3 | 二分查找 |
| O(√n) | 3.2 | 10 | 31.6 | 100 | 质数检查 |
| O(n) | 10 | 100 | 1000 | 10000 | 线性查找 |
| O(n log n) | 33 | 664 | 9966 | 132877 | 快速排序 |
| O(n²) | 100 | 10000 | 10⁶ | 10⁸ | 冒泡排序 |
| O(2ⁿ) | 1024 | 1.3×10³⁰ | 巨大 | 天文数字 | 汉诺塔 |
9. 计算代码复杂度的总和
规则1:顺序相加
void example(vector<int>& v) {
// 步骤1: O(n)
for (int i = 0; i < v.size(); ++i) { // n 次
process1(v[i]);
}
// 步骤2: O(n)
for (int i = 0; i < v.size(); ++i) { // n 次
process2(v[i]);
}
// 总计: O(n) + O(n) = O(2n) = O(n)
}
规则2:嵌套相乘
void nested_example(vector<int>& v) {
int n = v.size();
// 外层: n 次
for (int i = 0; i < n; ++i) {
// 内层: n 次
for (int j = 0; j < n; ++j) {
process(v[i], v[j]); // 执行 n×n 次
}
}
// 总计: O(n²)
}
规则3:递归分析
int binary_search(const vector<int>& v, int l, int r, int target) {
if (l > r) return -1;
int mid = l + (r - l) / 2;
if (v[mid] == target) return mid;
if (v[mid] > target)
return binary_search(v, l, mid-1, target); // 问题减半
else
return binary_search(v, mid+1, r, target);
}
// 递归深度: O(log n)
// 每次递归: O(1)
// 总计: O(log n)
10. 选择 vector vs list
// 场景1:频繁随机访问
std::vector<int> vec; // ✅ O(1) 访问
// 场景2:频繁中间插入/删除
std::list<int> lst; // ✅ O(1) 插入
// 但注意:list 找到位置需要 O(n)
// vector 插入需要 O(n)
// 需权衡
总结
"时间复杂度用大O记号表示算法最坏情况下的增长趋势:O(1) 常数时间(数组访问),O(log n) 对数时间(二分查找),O(n) 线性时间(顺序查找),O(n log n) 对数线性时间(快速排序),O(n²) 平方时间(冒泡排序)。计算时保留最高阶、忽略常数系数。"
关键记忆:
-
O(1):一次操作,与n无关
-
O(log n):每步问题规模减半
-
O(n):遍历一次
-
O(n log n):分治法典型复杂度
-
O(n²):双层循环
-
计算:分析循环嵌套和递归深度