1. 概述

本文将深入讲解 Big O 符号的实际含义,并通过多个 Java 示例,直观展示不同时间复杂度对代码执行效率的影响。掌握这些知识,能帮你写出更高效的算法,避免在性能上“踩坑”。

2. Big O 符号的直观理解

我们经常听到某个算法的性能用 Big O 符号 来描述。算法性能分析(也叫算法复杂度分析)属于 算法分析 领域,主要关注算法执行时消耗的资源,比如时间或内存。

本文重点关注 时间资源。通常来说,算法执行时间越短,性能越好。Big O 关注的是:当输入规模增长时,运行时间如何变化。✅ 理解增长趋势,比纠结具体耗时更重要。

3. 常数时间算法 – O(1)

输入规模如何影响运行时间?关键在于理解不同增长速率。这里我们关注的是:每单位输入所消耗的时间

看下面这段代码:

int n = 1000;
System.out.println("Hey - your input is: " + n);

无论 n 是多少,这段代码的执行时间都是固定的,与输入大小无关。

再看这个例子:

int n = 1000;
System.out.println("Hey - your input is: " + n);
System.out.println("Hmm.. I'm doing more stuff with: " + n);
System.out.println("And more: " + n);

虽然执行了 3 次打印,耗时可能是上一个例子的 3 倍,但依然 n 的大小无关。这类算法称为 常数时间算法,记作 *O(1)*。

⚠️ 注意:O(2)O(3) 甚至 O(1000) 都等价于 *O(1)*。Big O 不关心常数因子,只关心是否随输入增长。

4. 对数时间算法 – O(log n)

常数时间最快,接下来就是 对数时间。这类算法理解起来稍微抽象一点。

最常见的例子是 **二分查找**。其核心思想是每次将搜索范围减半,因此运行时间与输入的对数(以 2 为底)成正比。

for (int i = 1; i < n; i = i * 2){
    System.out.println("Hey - I'm busy looking at: " + i);
}

如果 n 是 8,输出为:

Hey - I'm busy looking at: 1
Hey - I'm busy looking at: 2
Hey - I'm busy looking at: 4

循环执行了 log₂(8) = 3 次。✅ 增长极其缓慢,性能非常优秀。

5. 线性时间算法 – O(n)

比对数时间慢的是 线性时间算法。所谓线性增长,就是运行时间与输入规模 n 成正比。

最典型的例子是单层循环:

for (int i = 0; i < n; i++) {
    System.out.println("Hey - I'm busy looking at: " + i);
}

循环执行 n 次。我们不关心每次迭代具体耗时,只关心它随 n 线性增长。

即使改成这样:

for (int i = 0; i < n; i++) {
    System.out.println("Hey - I'm busy looking at: " + i);
    System.out.println("Hmm.. Let's have another look at: " + i);
    System.out.println("And another: " + i);
}

虽然每次迭代做了 3 次操作,但总执行次数仍是 3n,依然属于 线性增长。我们记作 *O(n)*。

✅ 核心:O(2n+1)O(n) 被视为等价。Big O 只关注最高次项和增长趋势,忽略常数项和低阶项。

6. 线性对数时间算法 – O(n log n)

这是下一个复杂度层级。运行时间与 n log n 成正比。

典型例子是嵌套循环,其中内层循环是对数时间:

for (int i = 1; i <= n; i++){
    for(int j = 1; j < n; j = j * 2) {
        System.out.println("Hey - I'm busy looking at: " + i + " and " + j);
    }
}

外层循环 n 次,内层循环 log n 次,总执行次数约为 n * log n

例如,当 n=8 时,执行次数为 8 * log₂(8) = 8 * 3 = 24 次。

⚠️ 在分析 Big O 时,循环条件中的 <= 还是 < 通常不影响最终结论。

7. 多项式时间算法 – O(n^p)

这类算法比 O(n log n) 更慢。"多项式" 是一个统称,包含二次方 *(n²)*、三次方 *(n³)*、四次方 (n⁴) 等。

关键点:

  • ✅ O(n²) 比 O(n³) 快
  • ✅ O(n³) 比 O(n⁴) 快
  • 复杂度随指数增加而急剧恶化

看一个简单的 O(n²) 算法:

for (int i = 1; i <= n; i++) {
    for(int j = 1; j <= n; j++) {
        System.out.println("Hey - I'm busy looking at: " + i + " and " + j);
    }
}

n=8 时,执行 8² = 64 次。

⚠️ 如果再嵌套一层循环,复杂度就变成 O(n³),性能会显著下降。

8. 指数时间算法 – O(k^n)

⚠️ 警告:进入危险区域!这类算法的运行时间随输入规模呈指数级增长。

例如:

  • **O(2^n)**:每增加一个输入,运行时间翻倍
  • **O(3^n)**:每增加一个输入,运行时间变为 3 倍
  • **O(k^n)**:每增加一个输入,运行时间变为 k 倍

简单示例:

for (int i = 1; i <= Math.pow(2, n); i++){
    System.out.println("Hey - I'm busy looking at: " + i);
}

n=8 时,循环执行 2^8 = 256 次。随着 n 增大,执行次数爆炸式增长,实际项目中应极力避免。

9. 阶乘时间算法 – O(n!)

在大多数情况下,这已经是性能最差的级别了。运行时间与输入规模的阶乘成正比。

经典案例是使用 暴力穷举法 解决 **旅行商问题**。其时间复杂度为 O(n!),因为需要检查所有可能的城市排列。

简单示例:

for (int i = 1; i <= factorial(n); i++){
    System.out.println("Hey - I'm busy looking at: " + i);
}

其中 factorial(n) 计算 n!。当 n=8 时,算法执行 8! = 40320 次。✅ 可以看出,这类算法仅适用于极小的输入。

10. 渐近函数

Big O 是一种渐近函数。它只关心算法在输入规模趋于无穷大时的性能表现。

关键点:

  • ✅ Big O 关注的是极限情况下的性能(例如排序一亿个数 vs. 排序 5 个数)
  • ❌ 它不关心小规模输入下的具体表现
  • ✅ 除了 Big O,还有 Big Θ (Theta) 和 Big Ω (Omega),它们都描述算法在极限下的行为

三者区别(用集合论理解):

  • Big O:上界,描述算法最坏情况下的运行时间(“不会比这个更差”)
  • Big Ω:下界,描述算法最好情况下的运行时间(“不会比这个更好”)
  • Big Θ:紧确界,描述算法运行时间的精确阶数(“就等于这个”)

虽然日常交流中我们常用 Big O 泛指复杂度,但了解三者的区别会让你的分析更严谨。

11. 总结

本文通过 Java 示例讲解了 Big O 符号的核心概念,展示了不同时间复杂度对算法性能的巨大影响。

  • ✅ 掌握复杂度分析,能帮你快速识别代码中的性能瓶颈
  • ✅ 优先选择 O(1)、O(log n)、O(n) 和 O(n log n) 的算法
  • ❌ 尽量避免 O(n²) 以上的算法,尤其是 O(2^n) 和 O(n!),它们在大数据量下会“原地爆炸”

一个直观展示各种复杂度对比的图表可以参考:Big O Cheat Sheet

文中所有代码示例均可在 GitHub 上找到:https://github.com/eugenp/tutorials/tree/master/algorithms-miscellaneous-3


原始标题:Practical Java Examples of the Big O Notation