CSP-J/S冲奖第22天:时间复杂度
应粉丝要求出本期
一、为什么需要时间复杂度?
1.1 程序性能的度量
- 问题:如何衡量不同算法的效率?
// 示例1:求1+2+...+n int sum1(int n) { // 时间复杂度 O(n) int total = 0; for(int i=1; i<=n; i++) total += i; return total; } int sum2(int n) { // 时间复杂度 O(1) return n*(n+1)/2; }
-
结论:
sum2
比sum1
更高效(尤其当n很大时)
1.2 时间复杂度的定义
-
定义:算法执行时间随输入规模增长的增长率
- 特点:
-
关注最坏情况下的时间消耗
-
忽略常数项和低阶项(关注增长趋势)
-
使用大O表示法(如 O(n²))
-
二、大O表示法核心规则
2.1 常见时间复杂度
复杂度 | 名称 | 示例代码 |
---|---|---|
O(1) | 常数复杂度 | 直接访问数组元素 |
O(log n) | 对数复杂度 | 二分查找 |
O(n) | 线性复杂度 | 遍历数组 |
O(n log n) | 线性对数复杂度 | 快速排序 |
O(n²) | 平方复杂度 | 双重循环(冒泡排序) |
O(2ⁿ) | 指数复杂度 | 暴力破解子集问题 |
O(n!) | 阶乘复杂度 | 全排列问题 |
2.2 计算步骤
-
确定输入规模(n)
-
统计基本操作的执行次数
-
保留最高阶项,去掉系数
示例:
for(int i=0; i<n; i++) { // n次
for(int j=0; j<n; j++) // n次
cout << i+j; // 基本操作
}
// 总次数:n*n = n² → O(n²)
三、C++代码复杂度分析
3.1 典型代码模式
1. 单循环
for(int i=0; i<n; i++) { // O(n)
// 常数时间操作
}
2. 双重循环
for(int i=0; i<n; i++) { // O(n²)
for(int j=0; j<n; j++) {
// 常数时间操作
}
}
3. 递归
int factorial(int n) { // O(n)
if(n <= 1) return 1;
return n * factorial(n-1);
}
3.2 复杂度陷阱
错误示例:
// 计算斐波那契数列(低效版)
int fib(int n) { // O(2ⁿ)
if(n <= 1) return n;
return fib(n-1) + fib(n-2);
}
优化方案:
int fib(int n) { // O(n)
int a = 0, b = 1;
for(int i=2; i<=n; i++) {
int c = a + b;
a = b;
b = c;
}
return b;
}
四、复杂度对比实验
4.1 不同复杂度的增长趋势
n | log n | n | n log n | n² | 2ⁿ |
---|---|---|---|---|---|
10 | 3 | 10 | 30 | 100 | 1024 |
100 | 6 | 100 | 600 | 10^4 | 1.27e30 |
1000 | 9 | 1000 | 9000 | 10^6 | 1e301 |
4.2 实际运行时间对比
#include <iostream>
#include <chrono>
using namespace std;
// O(n) 算法
void linear(int n) {
for(int i=0; i<n; i++);
}
// O(n²) 算法
void quadratic(int n) {
for(int i=0; i<n; i++)
for(int j=0; j<n; j++);
}
int main() {
int n = 10000;
auto start = chrono::high_resolution_clock::now();
linear(n);
auto end = chrono::high_resolution_clock::now();
cout << "O(n) 耗时:"
<< chrono::duration_cast<chrono::milliseconds>(end - start).count()
<< " ms" << endl;
start = chrono::high_resolution_clock::now();
quadratic(n);
end = chrono::high_resolution_clock::now();
cout << "O(n²) 耗时:"
<< chrono::duration_cast<chrono::milliseconds>(end - start).count()
<< " ms" << endl;
}
输出示例:
O(n) 耗时:0 ms
O(n²) 耗时:45 ms
五、复杂度优化技巧
5.1 优化策略
-
减少嵌套循环:用数学公式替代多重循环
-
提前终止:利用break/continue减少不必要的循环
-
空间换时间:使用哈希表(O(1)查找)
-
算法选择:优先选择低复杂度算法(如快速排序 vs 冒泡排序)
5.2 案例分析
问题:查找数组中是否存在重复元素
低效方案:
bool containsDuplicate(vector<int>& nums) { // O(n²)
for(int i=0; i<nums.size(); i++)
for(int j=i+1; j<nums.size(); j++)
if(nums[i] == nums[j]) return true;
return false;
}
优化方案:
bool containsDuplicate(vector<int>& nums) { // O(n)
unordered_set<int> s;
for(int num : nums) {
if(s.count(num)) return true;
s.insert(num);
}
return false;
}
六、实战练习
6.1 基础题
题目:分析以下代码的时间复杂度
for(int i=0; i<n; i++) {
for(int j=0; j<i; j++) {
cout << "Hello";
}
}
答案:O(n²)(等差数列求和:1+2+...+n-1 = n(n-1)/2)
6.2 进阶题
题目:优化以下代码的复杂度
// 原始版本:O(n³)
int countTriplets(vector<int>& arr) {
int count = 0;
for(int i=0; i<arr.size(); i++)
for(int j=i+1; j<arr.size(); j++)
for(int k=j+1; k<arr.size(); k++)
if(arr[i] + arr[j] + arr[k] == 0)
count++;
return count;
}
优化思路:
-
先排序(O(n log n))
-
固定第一个元素,双指针查找后两个元素(O(n²))
七、复杂度速查表
算法类型 | 时间复杂度 | 适用场景 |
---|---|---|
二分查找 | O(log n) | 有序数据查找 |
深度优先搜索 | O(n) | 树/图遍历 |
快速排序 | O(n log n) | 通用排序 |
Dijkstra算法 | O((V+E) log V) | 最短路径(稀疏图) |
Floyd-Warshall | O(n³) | 所有节点间最短路径 |
课后作业:
-
分析插入排序的时间复杂度(最好/最坏/平均)
-
比较冒泡排序和选择排序的复杂度差异
-
实现一个O(n log n)的排序算法(如归并排序)