学习算法复杂度
算法复杂度学习文档
目录
什么是算法复杂度
算法复杂度用来衡量算法执行所需资源(时间和空间)随输入规模增长的变化趋势。
使用大O符号(Big O Notation)表示,描述的是最坏情况下的上界。
核心原则:只关注增长最快的项,忽略常数和低阶项。
时间复杂度
O(1) — 常数时间
操作次数固定,与输入规模无关。
// 直接访问数组元素
static int getFirst(int[] arr) {
return arr[0]; // 无论数组多大,只执行一次
}
典型场景:数组索引访问、哈希表查找、简单赋值
O(log n) — 对数时间
数据规模每次按固定比例缩减(通常是减半)。
// 二分查找:每次范围减半
static int binarySearch(int[] arr, int target) {
int left = 0, right = arr.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == target) return mid;
else if (arr[mid] < target) left = mid + 1;
else right = mid - 1; // 范围减半
}
return -1;
}
判断方法:循环中数据规模每次除以某个常数(如2)。
假设 n=1024,只需要约 log₂(1024)=10 次就能找到。
典型场景:二分查找、平衡二叉树操作
O(n) — 线性时间
执行次数与输入规模成正比。
// 查找最大值:遍历所有元素
static int findMax(int[] arr) {
int max = arr[0];
for (int x : arr) { // 循环 n 次
if (x > max) {
max = x;
}
}
return max;
}
判断方法:单层循环,遍历所有 n 个元素。
典型场景:遍历数组、线性查找
O(n log n) — 线性对数时间
分治算法的典型复杂度,每层 O(n),共 log n 层。
// 归并排序:分治 + 合并
static void mergeSort(int[] arr, int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2;
mergeSort(arr, left, mid); // 递归左半:T(n/2)
mergeSort(arr, mid + 1, right); // 递归右半:T(n/2)
merge(arr, left, mid, right); // 合并:O(n)
}
}
// 递归式:T(n) = 2T(n/2) + O(n)
// 用主定理或递归树可得 O(n log n)
判断方法:分治算法,将问题分成若干子问题,然后合并结果。
典型场景:快速排序、归并排序、堆排序
O(n²) — 平方时间
双重嵌套循环,外层 n × 内层 n。
// 冒泡排序:双重循环
static void bubbleSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n; i++) { // 外层 n 次
for (int j = 0; j < n - 1; j++) { // 内层 n 次
if (arr[j] > arr[j + 1]) {
// 交换元素
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
判断方法:嵌套循环,层数决定幂次。
典型场景:冒泡排序、选择排序、插入排序、双重循环遍历
O(n³) — 立方时间
三重嵌套循环。
// 三重循环示例
static void tripleLoop(int n) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
for (int k = 0; k < n; k++) {
// 执行 n³ 次
}
}
}
}
典型场景:Floyd-Warshall算法、三重循环暴力求解
O(2ⁿ) — 指数时间
递归树呈指数增长,每个节点产生多个子节点。
// 斐波那契数列:递归树指数增长
static int fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2); // 每次分裂成2个
}
判断方法:递归产生多个相同规模的子问题。
典型场景:递归求解子集、汉诺塔、未优化的斐波那契
O(n!) — 阶乘时间
全排列,每个位置有 n, n-1, n-2... 种选择。
// 全排列
static void permutation(String str, String prefix) {
if (str.length() == 0) {
System.out.println(prefix);
return;
}
for (int i = 0; i < str.length(); i++) {
String rem = str.substring(0, i) + str.substring(i + 1);
permutation(rem, prefix + str.charAt(i));
}
}
典型场景:全排列、旅行商问题(暴力求解)
空间复杂度
空间复杂度衡量算法执行过程中额外使用的内存空间。
| 复杂度 | 典型场景 |
|---|---|
| O(1) | 原地排序、固定变量 |
| O(log n) | 递归调用栈(二分、快排) |
| O(n) | 辅助数组、线性递归 |
| O(n²) | 二维数组、邻接矩阵 |
// O(1) 空间:原地排序
static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
// O(n) 空间:辅助数组
static int[] copyArray(int[] arr) {
int[] copy = new int[arr.length]; // 额外分配 n 个空间
for (int i = 0; i < arr.length; i++) {
copy[i] = arr[i];
}
return copy;
}
// O(log n) 空间:递归调用栈
static int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1); // 递归深度 log n 或 n
}
常见复杂度对比
当 n = 1000 时:
| 复杂度 | 操作次数 | 可接受性 |
|---|---|---|
| O(1) | 1 | 极佳 |
| O(log n) | ~10 | 极佳 |
| O(n) | 1,000 | 良好 |
| O(n log n) | ~10,000 | 良好 |
| O(n²) | 1,000,000 | 小规模可用 |
| O(n³) | 1,000,000,000 | 仅小规模 |
| O(2ⁿ) | 约 10³⁰¹ | 不可行 |
| O(n!) | 远超宇宙原子数 | 不可行 |
Java代码示例
完整的可运行代码见 AlgorithmComplexityDemo.java。
运行方法
javac AlgorithmComplexityDemo.java
java AlgorithmComplexityDemo
输出示例
========== 算法复杂度演示 ==========
--- O(1) 常数时间 ---
数组第一个元素: 5
操作次数: 1
--- O(n) 线性时间 ---
数组最大值: 9
遍历次数: 6
--- O(log n) 对数时间 ---
二分查找 7 的索引: 3
查找次数: 2
--- O(n²) 平方时间 ---
冒泡排序结果: 1 2 3 5 8 9
比较次数: 36
--- O(n log n) 线性对数时间 ---
归并排序结果: 1 2 3 5 8 9
递归层数: 3
--- O(2ⁿ) 指数时间 ---
斐波那契第10项: 55
递归调用次数: 177
========== 复杂度对比表 ==========
数据规模 n=1000 时的操作次数:
O(1): 1
O(log n): 10
O(n): 1,000
O(n log n): 10,000
O(n²): 1,000,000
O(2ⁿ): 约 10^301 (不可行)
如何分析复杂度
步骤
- 找循环 — 循环是复杂度的主要来源
- 看嵌套 — 嵌套层数决定幂次
- 看变化 — 变量是线性增长还是对数增长
- 取最大 — 多个部分取复杂度最高的项
多个独立循环
void example(int[] arr) {
for (int x : arr) { // O(n)
System.out.println(x);
}
for (int x : arr) { // O(n)
System.out.println(x);
}
// 总复杂度 = O(n) + O(n) = O(2n) = O(n)
// 常数省略,仍是 O(n)
}
不同规模参数
void process(int m, int n) {
for (int i = 0; i < m; i++) { // m 次
for (int j = 0; j < n; j++) { // n 次
System.out.println(i, j);
}
}
// 复杂度:O(m × n)
// 不能简写,因为 m 和 n 是独立的
}
实用技巧
快速判断口诀
| 代码特征 | 复杂度 |
|---|---|
| 无循环,直接计算 | O(1) |
| 单层循环,遍历 n | O(n) |
| 两层嵌套循环 | O(n²) |
| 三层嵌套循环 | O(n³) |
| 循环变量 i *= 2 或 i //= 2 | O(log n) |
| 递归分成2份 + 合并 | O(n log n) |
| 递归产生多个子问题 | 可能是指数级 |
优化方向
- O(n²) → O(n log n):使用分治或更优算法
- O(n) → O(log n):使用哈希表或二分查找
- O(2ⁿ) → O(n²):使用动态规划
总结
掌握算法复杂度的分析能力,可以帮助我们:
- 选择合适的数据结构 — 不同操作的复杂度不同
- 优化算法性能 — 识别瓶颈,针对性优化
- 预估系统容量 — 根据数据规模选择合适的算法
- 面试准备 — 算法面试的核心考察点
记住:先保证正确性,再考虑优化。过早优化是万恶之源。

浙公网安备 33010602011771号