一、算法复杂度分析:大O表示法、时间复杂度、空间复杂度、最好/最坏/平均情况分析

聊算法,第一个绕不开的就是复杂度分析。

说实话,我刚开始学算法时,觉得这东西挺虚的。不就是数数循环跑了多少次吗?后来在项目中吃了亏,才明白——不懂复杂度,写出来的代码可能就是一颗定时炸弹。

1.1 为什么我们要关心复杂度?

你想想看,同样的功能,你的代码跑 1 秒,别人的跑 1 小时。差别在哪?

不是语法问题,也不是机器配置问题。核心在于——算法选错了。

复杂度分析,就是帮你提前判断:这个算法,到底行不行。

核心观点:复杂度分析不是理论游戏,它是你写出高性能代码的「体检报告」。

1.2 大O表示法——算法的「量级」标签

大O表示法,说白了就是描述算法运行时间随数据规模增长的趋势。

它不关心具体跑了多少毫秒,只关心「当 n 很大时,大概是什么量级」。

举个例子:

// 场景:在数组中查找某个值

// 方式一:遍历查找
function find(arr, target) {
  for (let i = 0; i < arr.length; i++) {
    if (arr[i] === target) return i;
  }
  return -1;
}
// 时间复杂度:O(n)

// 方式二:二分查找(前提是数组已排序)
function binarySearch(arr, target) {
  let left = 0, right = arr.length - 1;
  while (left <= right) {
    let mid = Math.floor((left + right) / 2);
    if (arr[mid] === target) return mid;
    else if (arr[mid] < target) left = mid + 1;
    else right = mid - 1;
  }
  return -1;
}
// 时间复杂度:O(log n)

看到区别了吗?当 n=100 万时,O(n) 可能要跑 100 万步,而 O(log n) 只需要大约 20 步。这就是量级的碾压。

我的习惯:写代码前,先估算一下数据规模。如果 n 超过 10 万,O(n²) 的算法基本就要慎重了。

1.3 常见时间复杂度速查表

大O表示 名称 典型算法 n=1000 时的大致操作数
O(1) 常数时间 数组随机访问、哈希表查找 1
O(log n) 对数时间 二分查找、平衡树查找 约 10
O(n) 线性时间 数组遍历、链表查找 1000
O(n log n) 线性对数时间 归并排序、快速排序(平均) 约 10000
O(n²) 平方时间 冒泡排序、选择排序 1,000,000
O(2ⁿ) 指数时间 递归斐波那契(未优化) 天文数字

我曾经踩过的坑:有一次处理 50 万条数据,用了 O(n²) 的双层循环。本地测试数据少没发现,上线后直接卡死。后来改成 O(n log n) 的方案,问题秒解。所以——数据量一大,复杂度就是生死线。

1.4 时间复杂度分析——三个关键点

分析时间复杂度,我一般看三个地方:

  1. 循环层数——一层循环通常是 O(n),两层嵌套就是 O(n²)
  2. 递归深度——递归调用了多少次?每次调用数据规模减半?那就是 O(log n)
  3. 核心操作——循环体里有没有更耗时的操作?比如数组插入、字符串拼接

举个例子:

function example(arr) {
  let sum = 0;                // O(1)
  for (let i = 0; i < n; i++) {  // 循环 n 次
    sum += arr[i];            // O(1)
  }
  for (let i = 0; i < n; i++) {  // 循环 n 次
    for (let j = 0; j < n; j++) { // 嵌套循环 n 次
      console.log(i, j);      // O(1)
    }
  }
  return sum;
}
// 总复杂度:O(1) + O(n) + O(n²) = O(n²)

这里有个原则:取最高阶。O(n²) 是主导项,O(n) 和 O(1) 在 n 很大时可以忽略。

1.5 空间复杂度——内存也是成本

时间复杂度看时间,空间复杂度看内存。

说白了就是:你的算法额外占了多少存储空间。

常见的空间复杂度:

  • O(1):只用了几个变量,不随 n 变化
  • O(n):用了一个数组或哈希表,大小和 n 成正比
  • O(n²):用了二维矩阵

一个实用的经验:很多时候,时间复杂度和空间复杂度是「跷跷板」。你想跑得快,往往要多占内存;你想省内存,可能就要多花时间。比如缓存就是典型的「用空间换时间」。

1.6 最好、最坏、平均情况

同一个算法,面对不同的输入,表现可能天差地别。

拿快速排序来说:

  • 最好情况:每次选的基准值正好在中间,O(n log n)
  • 最坏情况:每次选的基准值都是最大或最小,退化成 O(n²)
  • 平均情况:随机输入下,期望复杂度 O(n log n)

我个人习惯:关注最坏情况。因为线上环境,你永远不知道用户会给你什么数据。

避坑指南:我曾经写过一个哈希表的实现,以为平均 O(1) 就万事大吉。结果用户传了大量哈希冲突的数据,性能直接崩了。后来才意识到——哈希函数的设计、扩容策略,这些细节才是决定「平均情况」能不能成立的关键。

1.7 知识体系总览

下面这张图,帮你把本章的核心脉络串起来:

算法复杂度分析 大O表示法:描述增长趋势 时间复杂度:运行时间随n变化 O(1) 常数 O(log n) 对数 O(n) 线性 O(n²) 平方 空间复杂度:额外内存消耗 O(1) 原地 O(n) 线性 最好/最坏/平均情况 最好 最坏 平均 核心原则:关注最坏情况,取最高阶,忽略常数 时间与空间往往是跷跷板,需要根据场景权衡

1.8 总结一下

复杂度分析,说白了就是三个问题:

  • 数据规模 n 变大时,时间怎么涨?——时间复杂度
  • 数据规模 n 变大时,内存怎么涨?——空间复杂度
  • 最差情况下,会不会崩?——关注最坏情况

嗯,这一章的内容就到这。记住一句话:不懂复杂度,你写的代码就是盲盒。