Day1 时间复杂度
一 概念
在 C++ 中,时间复杂度是衡量算法运行时间随输入规模增长的趋势的关键指标,用于评估算法的效率。它通过 大 O 表示法(Big O Notation) 描述,关注的是输入规模 n
趋近于无穷大时,算法时间增长的主导因素(忽略常数和低阶项)。
- 输入规模(n):算法处理的数据量(如数组长度、链表节点数、矩阵维度等)。
- 大 O 表示法:表示算法时间复杂度的上界,例如
O(n)
表示时间与输入规模n
成线性关系。 - 核心原则:只保留最高阶项,忽略常数因子和低阶项(如
O(2n + 3)
简化为O(n)
,O(n² + n)
简化为O(n²)
)。
二 常见时间复杂度类型
1.O (1):常数时间
无论输入规模多大,算法执行时间恒定(与 n
无关)。
典型场景:
- 数组 /
vector
的随机访问(通过下标[i]
)。 - 哈希表(
unordered_map
)的插入、查找(平均情况)。
示例:访问数组元素
#include <vector>int get_element(const std::vector<int>& vec, int index) {return vec[index]; // 随机访问,时间复杂度 O(1)
}
2. O (n):线性时间
时间与输入规模 n
成正比,需逐个处理每个元素。
典型场景:
- 线性枚举(遍历数组、链表等)。
- 未排序数组的顺序查找。
示例:遍历 vector
求和
#include <vector>int sum_vector(const std::vector<int>& vec) {int sum = 0;for (int num : vec) { // 遍历所有元素,时间复杂度 O(n)sum += num;}return sum;
}
3. O (logn):对数时间
时间随输入规模的对数增长,通常出现在分治算法中(如二分查找)。
典型场景:
- 有序数组的二分查找(
std::lower_bound
)。 - 平衡二叉搜索树(如
std::set
、std::map
)的插入、查找。
示例:二分查找(C++ 标准库实现)
#include <vector>
#include <algorithm> // for std::lower_bound// 查找目标值的位置(返回迭代器)
auto binary_search(const std::vector<int>& vec, int target) {return std::lower_bound(vec.begin(), vec.end(), target);// 时间复杂度 O(logn)(数组已排序)
}
4. O (nlogn):线性对数时间
时间增长趋势为 n × logn
,常见于高效的排序算法。
典型场景:
- 快速排序、归并排序(平均情况)。
std::sort
(C++ 标准库排序,基于快速排序 / 堆排序)。
示例:使用 std::sort
排序
#include <vector>
#include <algorithm>void sort_vector(std::vector<int>& vec) {std::sort(vec.begin(), vec.end()); // 平均时间复杂度 O(nlogn)
}
5.O (n²):平方时间
时间与输入规模的平方成正比,常见于双重循环的暴力算法。
典型场景:
- 冒泡排序、选择排序(最坏 / 平均情况)。
- 二维数组的遍历(如矩阵元素逐个处理)。
示例:冒泡排序
#include <vector>void bubble_sort(std::vector<int>& vec) {int n = vec.size();for (int i = 0; i < n - 1; ++i) {for (int j = 0; j < n - i - 1; ++j) { // 双重循环,时间复杂度 O(n²)if (vec[j] > vec[j + 1]) {std::swap(vec[j], vec[j + 1]);}}}
}
6. 其他复杂度
- O(2ⁿ):指数时间(如斐波那契数列的递归实现,存在大量重复计算)。
- O(n!):阶乘时间(如全排列生成)。
三、时间复杂度的分析方法
1. 单循环:O (n)
单个循环遍历 n
次,时间复杂度为 O(n)
。
for (int i = 0; i < n; ++i) {// 操作(时间复杂度 O(1))
}
// 总时间复杂度:O(n)
2. 双重循环:O (n²)
外层循环 n
次,内层循环 n
次,总次数为 n × n = n²
。
for (int i = 0; i < n; ++i) {for (int j = 0; j < n; ++j) {// 操作(O(1))}
}
// 总时间复杂度:O(n²)
当外层循环到第 i 次时,内层循环 i 次,总次数为 (1 + n)n / 2 = (n + n^2) / 2次。
for (int i = 0; i < n; ++i) {for (int j = 0; j <= i; ++j) {//操作(O(1))}
}
/* i 0 1 2 3 ... n-1j 1 1 2 3 ... n总操作次数为等差数列:(1 + n)n / 2 = (n + n^2) / 2只保留最高阶项,忽略常数因子和低阶项,故时间复杂度O(n^2)
*/
3. 对数循环:O (logn)
每次循环将问题规模减半(如二分查找)。
int i = 1;
while (i < n) {i *= 2; // 每次规模翻倍
}
// 循环次数为 log₂n,时间复杂度 O(logn)
int i = n * n;
while (i != 1) {i /= 2; //每次规模减少为原来的一半
}/*
t(操作次数) 0 1 2 3 ...
i n^2 n^2 / 2 n^2 / 4 n^2 / 8 ...由上面规律可得:i = n^2 / 2^t
将该表达式与 i = 1 联立
得 t = 2log2(n)
所以时间复杂度为 O(log2(n)),记为 O(logn)
*/
4. 递归的时间复杂度
递归的时间复杂度需结合递归深度和每层操作次数。
示例:斐波那契数列的递归实现(低效版)
int fib(int n) {if (n <= 1) return n;return fib(n - 1) + fib(n - 2); // 每次递归分裂为 2 个子问题
}
// 时间复杂度:O(2ⁿ)(存在大量重复计算)
四、时间复杂度的优化策略
1. 用哈希表优化查找(O (n) → O (1))
将线性查找(O (n))替换为哈希表(unordered_map
)的键值查找(O (1)),以空间换时间。
示例:两数之和。在数组中找到两个元素值等于target,返回这两个元素组成的数组。
#include <vector>
#include <unordered_map>std::vector<int> two_sum(const std::vector<int>& nums, int target) {std::unordered_map<int, int> hash; // 哈希表存储值→索引for (int i = 0; i < nums.size(); ++i) {int complement = target - nums[i];if (hash.count(complement)) { // 查找时间 O(1)return {hash[complement], i};}hash[nums[i]] = i; // 插入时间 O(1)}return {};
}
// 原暴力解法时间复杂度 O(n²),优化后 O(n)
2. 用排序 + 二分查找优化(O (n²) → O (nlogn))
将双重循环的暴力匹配替换为排序后二分查找。
示例:查找数组中是否存在两数之和为目标值
#include <vector>
#include <algorithm>bool has_two_sum(std::vector<int>& nums, int target) {std::sort(nums.begin(), nums.end()); // 排序 O(nlogn)for (int num : nums) {int complement = target - num;// 二分查找 complement,时间 O(logn)if (std::binary_search(nums.begin(), nums.end(), complement)) {return true;}}return false;
}
// 原暴力解法 O(n²),优化后 O(nlogn)
3. 避免重复计算(O (2ⁿ) → O (n))
通过动态规划或记忆化搜索,消除递归中的重复子问题。
示例:斐波那契数列的动态规划实现
#include <vector>int fib(int n) {if (n <= 1) return n;std::vector<int> dp(n + 1); // 存储已计算的结果dp[0] = 0;dp[1] = 1;for (int i = 2; i <= n; ++i) {dp[i] = dp[i - 1] + dp[i - 2]; // 避免重复计算,时间 O(n)}return dp[n];
}
// 原递归解法 O(2ⁿ),优化后 O(n)
五 总结
时间复杂度反映算法运行时间随输入规模增长的趋势,不同复杂度的增长速度从低到高排列如下:
O(1) < O(logn) < O(n) < O(nlogn) < O(n²)
时间复杂度是评估算法效率的核心指标。在 C++ 中,通过分析循环嵌套、递归深度或操作次数,可以确定算法的时间复杂度。实际编码时,应优先选择低时间复杂度的算法(如 O (nlogn) 优于 O (n²)),并利用哈希表、排序 + 二分等优化手段降低复杂度。同时,结合数据结构的特性(如unordered_map
的 O (1) 查找),可以显著提升程序性能。