面试被问复杂度总懵?这篇指南帮你彻底搞清

12 阅读7分钟

时间复杂度与空间复杂度计算指南

目录


基本概念

复杂度衡量的是算法性能随数据量 n 增长的趋势,不是真实时间或内存。

用大 O 表示法(Big O)描述上界:

O(1) < O(logn) < O(n) < O(nlogn) < O(n²) < O(n³) < O(2ⁿ) < O(n!)

化简规则

规则示例
去掉系数O(3n) → O(n)
只保留最高次项O(n² + n) → O(n²)
常数统一为 O(1)O(100) → O(1)

时间复杂度

O(1):常数时间

执行次数固定,不随 n 变化。

int a = arr[0];           // 直接访问
int b = arr[n - 1];       // 下标访问
map.get(key);             // 哈希表查找

O(logn):对数时间

每次问题规模折半(或缩小为固定比例)。

// 二分查找
while (left <= right) {
    int mid = (left + right) / 2;
    if (arr[mid] == target) return mid;
    else if (arr[mid] < target) left = mid + 1;
    else right = mid - 1;
}
// n → n/2 → n/4 → ... → 1,共 log₂n 次
// 求整数位数
while (n > 0) {
    n /= 10;   // 每次除以10,共 log₁₀n 次
}

O(n):线性时间

遍历一次数据。

// 单层循环
for (int i = 0; i < n; i++) { }

// 链表遍历
while (node != null) { node = node.next; }

// 双指针(两个指针各走一次)
int left = 0, right = n - 1;
while (left < right) { left++; right--; }

O(nlogn):线性对数时间

外层 n 次,内层 logn 次,或分治每层 O(n)。

// 归并排序:logn 层,每层处理 n 个元素
void mergeSort(int[] arr, int l, int r) {
    mergeSort(arr, l, mid);
    mergeSort(arr, mid+1, r);
    merge(arr, l, mid, r);   // O(n)
}

// 堆排序:n 次堆化,每次 O(logn)
for (int i = n-1; i > 0; i--) {
    swap(arr, 0, i);
    heapify(arr, i, 0);      // O(logn)
}

O(n²):平方时间

两层嵌套循环。

// 冒泡排序
for (int i = 0; i < n; i++)
    for (int j = 0; j < n-i-1; j++)
        if (arr[j] > arr[j+1]) swap(arr, j, j+1);

// 注意:内层不从0开始,仍是 O(n²)
// n + (n-1) + (n-2) + ... + 1 = n(n+1)/2 = O(n²)

O(n³):立方时间

三层嵌套循环。

// Floyd 最短路径
for (int k = 0; k < n; k++)
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++)
            dist[i][j] = Math.min(dist[i][j], dist[i][k] + dist[k][j]);

O(2ⁿ):指数时间

每次分裂为两个子问题,递归树节点数翻倍。

// 斐波那契朴素递归
int fib(int n) {
    if (n <= 1) return n;
    return fib(n-1) + fib(n-2);
}

// 子集枚举
void subsets(int[] nums, int idx, List<Integer> path) {
    result.add(new ArrayList<>(path));
    for (int i = idx; i < nums.length; i++) {
        path.add(nums[i]);
        subsets(nums, i+1, path);
        path.remove(path.size()-1);
    }
}

O(n!):阶乘时间

全排列、旅行商问题暴力解。

// 全排列
void permute(int[] nums, int start) {
    if (start == nums.length) { result.add(...); return; }
    for (int i = start; i < nums.length; i++) {
        swap(nums, start, i);
        permute(nums, start+1);
        swap(nums, start, i);
    }
}
// 共 n! 种排列

特殊情况

两段独立循环:O(n + m)
for (int i = 0; i < n; i++) { }   // O(n)
for (int i = 0; i < m; i++) { }   // O(m)

// 总复杂度 O(n + m),若 m >> n 则 O(m)
循环次数非显而易见
// 看起来是 O(n²),实际是 O(nlogn)
for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= n; j += i) {  // j 每次加 i
        // 执行次数:n/1 + n/2 + n/3 + ... + n/n
        // = n × (1 + 1/2 + 1/3 + ... + 1/n)
        // = n × H(n) ≈ n × lnn = O(nlogn)
    }
}
// 看起来两层,实际 O(n)
// 因为 i 和 j 合计只走 n 步
int i = 0, j = 0;
while (i < n && j < n) {
    if (condition) i++;
    else j++;
}

空间复杂度

计算原则

不算:输入数组本身(题目给定的)
算:  代码中额外申请的变量、数组、递归调用栈

O(1):常数空间

只用了固定数量的变量。

// 原地操作,没有额外申请数组
int sum = 0;
for (int num : arr) sum += num;

// 双指针原地反转
int left = 0, right = n - 1;
while (left < right) swap(arr, left++, right--);

O(n):线性空间

申请了和 n 相关的额外空间。

int[] temp = new int[n];          // 额外数组
Map<Integer, Integer> map = ...;  // 哈希表存 n 个元素
List<Integer> result = ...;       // 结果列表

O(n²):平方空间

二维数组或邻接矩阵。

int[][] dp = new int[n][n];       // DP 表
int[][] graph = new int[n][n];    // 邻接矩阵

O(logn):对数空间(递归栈)

递归深度为 logn。

// 二分查找递归版,深度 logn
int binarySearch(int[] arr, int l, int r, int target) {
    int mid = (l + r) / 2;
    return binarySearch(arr, l, mid-1, target);
}

// 快速排序平均情况,深度 logn
void quickSort(int[] arr, int l, int r) {
    int p = partition(arr, l, r);
    quickSort(arr, l, p-1);
    quickSort(arr, p+1, r);
}

递归栈空间计算

空间 = 递归深度 × 每帧大小
每帧存储:参数 + 局部变量 + 返回地址 → 通常是 O(1)
所以:空间复杂度 = O(递归深度)
场景递归深度空间复杂度
二分查找lognO(logn)
快速排序(平均)lognO(logn)
快速排序(最坏)nO(n)
归并排序logn(栈)+ n(数组)O(n)
斐波那契朴素递归nO(n)
DFS 树遍历树高 hO(h)

递归的复杂度

计算公式

时间 = 递归调用总次数 × 每次调用的工作量
空间 = 递归深度(树的高度)× 每帧大小

递归树分析法

画出递归调用树,计算:

  • 树的节点总数 = 总调用次数
  • 每个节点的工作量
  • 树的高度 = 递归深度
fib(4) 的递归树:

              fib(4)              层01个节点
            /        \
        fib(3)       fib(2)       层12个节点
        /    \       /    \
    fib(2) fib(1) fib(1) fib(0)  层24个节点

总节点数 ≈ 2⁴ = 16 → 时间 O(2ⁿ)
树高    = n      → 空间 O(n)

主定理(Master Theorem)

适用于形如 T(n) = aT(n/b) + f(n) 的递归:

a = 子问题个数
b = 每次规模缩小比例
f(n) = 每层额外工作量

情况1f(n) < n^(log_b a)  → T(n) = O(n^(log_b a))
情况2f(n) = n^(log_b a)  → T(n) = O(n^(log_b a) × logn)
情况3f(n) > n^(log_b a)  → T(n) = O(f(n))

实例:

归并排序:T(n) = 2T(n/2) + O(n)
  a=2, b=2, f(n)=n, n^(log₂2)=n¹=n
  f(n) = n^(log_b a) → 情况2
  T(n) = O(nlogn) ✅

二分查找:T(n) = T(n/2) + O(1)
  a=1, b=2, f(n)=1, n^(log₂1)=n⁰=1
  f(n) = n^(log_b a) → 情况2
  T(n) = O(logn) ✅

三路归并:T(n) = 3T(n/3) + O(n)
  a=3, b=3, f(n)=n, n^(log₃3)=n
  f(n) = n^(log_b a) → 情况2
  T(n) = O(nlogn) ✅

常见算法复杂度速查

排序算法

算法平均时间最坏时间最好时间空间稳定
冒泡排序O(n²)O(n²)O(n)O(1)
选择排序O(n²)O(n²)O(n²)O(1)
插入排序O(n²)O(n²)O(n)O(1)
希尔排序O(n^1.3)O(n²)O(n)O(1)
归并排序O(nlogn)O(nlogn)O(nlogn)O(n)
快速排序O(nlogn)O(n²)O(nlogn)O(logn)
堆排序O(nlogn)O(nlogn)O(nlogn)O(1)
计数排序O(n+k)O(n+k)O(n+k)O(k)
基数排序O(d×n)O(d×n)O(d×n)O(n)
桶排序O(n+k)O(n²)O(n)O(n)

数据结构操作

数据结构访问查找插入删除
数组O(1)O(n)O(n)O(n)
链表O(n)O(n)O(1)O(1)
栈 / 队列O(n)O(n)O(1)O(1)
哈希表O(1) 均摊O(1) 均摊O(1) 均摊
二叉搜索树(平衡)O(logn)O(logn)O(logn)O(logn)
二叉搜索树(退化)O(n)O(n)O(n)O(n)
O(1) 取顶O(n)O(logn)O(logn)
前缀树 TrieO(m)O(m)O(m)O(m)

m 为字符串长度

图算法

算法时间空间
BFS / DFSO(V+E)O(V)
Dijkstra(堆优化)O((V+E)logV)O(V)
Bellman-FordO(V×E)O(V)
Floyd-WarshallO(V³)O(V²)
Prim(堆优化)O((V+E)logV)O(V)
KruskalO(ElogE)O(V)
拓扑排序O(V+E)O(V)

动态规划常见模式

问题类型时间空间滚动数组优化后
线性 DP(爬楼梯)O(n)O(n)O(1)
0-1 背包O(n×W)O(n×W)O(W)
完全背包O(n×W)O(n×W)O(W)
最长公共子序列 LCSO(m×n)O(m×n)O(min(m,n))
最长递增子序列 LISO(n²) / O(nlogn)O(n)
区间 DPO(n³)O(n²)
树形 DPO(n)O(n)

复杂度判断口诀

无循环              → O(1)
一层循环            → O(n)
两层嵌套循环        → O(n²)
每次折半            → O(logn)
一层循环 + 每次折半 → O(nlogn)
三层嵌套循环        → O(n³)

递归看树:
  总节点数 × 每节点工作量 → 时间复杂度
  树的高度               → 空间复杂度

分治看主定理:
  T(n) = aT(n/b) + f(n)
  比较 f(n) 和 n^(log_b a) 的大小决定结果

陷阱:
  看起来两层但指针不重叠 → 实际 O(n)
  看起来一层但步长非1   → 实际 O(nlogn) 或更低
  递归不一定是 O(n)     → 要画递归树分析