时间复杂度、空间复杂度与算法稳定性详解

一、时间复杂度:衡量算法运行效率

1. 什么是时间复杂度?

时间复杂度不是程序运行了多少秒,而是衡量:

当输入规模 nnn 增大时,算法需要执行的基本操作次数如何增长。

这里的"基本操作"可以是赋值、比较、加法、交换、数组访问等。我们通常假设每种基本操作耗时固定,记为 O(1)O(1)O(1)。

例如:

  • 访问数组元素 arr[0]:一次操作,O(1)O(1)O(1)
  • 遍历长度为 nnn 的数组:执行 nnn 次操作,O(n)O(n)O(n)
  • 两层嵌套循环,每层执行 nnn 次:执行 n2n^2n2 次操作,O(n2)O(n^2)O(n2)

时间复杂度关注的是增长趋势,不是具体次数。


2. 大 OOO 符号

大 OOO 表示算法运行时间的上界。正式定义:

若存在正常数 ccc 和 n0n_0n0,使得对于所有 n≥n0n \ge n_0n≥n0,都有:

0≤f(n)≤c⋅g(n) 0 \le f(n) \le c \cdot g(n) 0≤f(n)≤c⋅g(n)

则称:

f(n)=O(g(n)) f(n) = O(g(n)) f(n)=O(g(n))

简单说:当 nnn 足够大时,f(n)f(n)f(n) 的增长不会超过 g(n)g(n)g(n) 的某个常数倍。

大 OOO 会忽略:

  • 常数项,例如 3n+53n + 53n+5 记为 O(n)O(n)O(n)
  • 低阶项,例如 n2+100n+50n^2 + 100n + 50n2+100n+50 记为 O(n2)O(n^2)O(n2)
  • 常数系数,例如 2n22n^22n2 记为 O(n2)O(n^2)O(n2)

除此之外还有:

  • Ω\OmegaΩ:下界
  • Θ\ThetaΘ:紧确界

但日常算法分析中,最常用的是大 OOO。


3. 常见时间复杂度类型

按效率从高到低排列:

复杂度 名称 增长特点 典型场景
O(1)O(1)O(1) 常数时间 不随 nnn 变化 访问数组元素
O(log⁡n)O(\log n)O(logn) 对数时间 每次问题规模减半 二分查找
O(n)O(n)O(n) 线性时间 与 nnn 成正比 遍历数组
O(nlog⁡n)O(n \log n)O(nlogn) 线性对数时间 高效排序 归并排序、快速排序平均
O(n2)O(n^2)O(n2) 平方时间 两层嵌套循环 冒泡排序、选择排序
O(n3)O(n^3)O(n3) 立方时间 三层嵌套循环 普通矩阵乘法
O(2n)O(2^n)O(2n) 指数时间 每层翻倍 朴素斐波那契、子集枚举
O(n!)O(n!)O(n!) 阶乘时间 全排列 生成所有排列

大小关系:

O(1)<O(log⁡n)<O(n)<O(nlog⁡n)<O(n2)<O(n3)<O(2n)<O(n!) O(1) < O(\log n) < O(n) < O(n \log n) < O(n^2) < O(n^3) < O(2^n) < O(n!) O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2n)<O(n!)

注意:对数底数不影响大 OOO,因为:

log⁡an=log⁡bnlog⁡ba \log_a n = \frac{\log_b n}{\log_b a} logan=logbalogbn

分母是常数,可以忽略。所以 O(log⁡2n)O(\log_2 n)O(log2n)、O(log⁡10n)O(\log_{10} n)O(log10n) 都写成 O(log⁡n)O(\log n)O(logn)。


二、如何计算时间复杂度?

计算时间复杂度一般按以下步骤:

  1. 找出基本操作。
  2. 分析循环执行次数。
  3. 嵌套循环通常相乘。
  4. 并列循环通常相加。
  5. 递归算法列递归式。
  6. 忽略常数和低阶项。
  7. 只保留最高阶项。

下面通过例子详细说明。


例 1:常数时间 O(1)O(1)O(1)

python 复制代码
def get_first(arr):
    return arr[0]

无论数组多长,只访问第一个元素一次。

所以:

T(n)=O(1) T(n) = O(1) T(n)=O(1)


例 2:单层循环 O(n)O(n)O(n)

python 复制代码
def sum_array(arr):
    total = 0
    for x in arr:
        total += x
    return total

循环执行 nnn 次,每次做一次加法。

T(n)=n⋅O(1)=O(n) T(n) = n \cdot O(1) = O(n) T(n)=n⋅O(1)=O(n)


例 3:并列循环 O(n+m)O(n + m)O(n+m)

python 复制代码
def two_loops(n, m):
    for i in range(n):
        print(i)

    for j in range(m):
        print(j)

第一个循环 nnn 次,第二个循环 mmm 次。

T(n,m)=O(n+m) T(n, m) = O(n + m) T(n,m)=O(n+m)

如果 m=nm = nm=n,则为:

O(n) O(n) O(n)


例 4:双层嵌套循环 O(n2)O(n^2)O(n2)

python 复制代码
def nested_loop(n):
    for i in range(n):
        for j in range(n):
            print(i, j)

外层执行 nnn 次,内层也执行 nnn 次。

总次数:

n×n=n2 n \times n = n^2 n×n=n2

所以:

T(n)=O(n2) T(n) = O(n^2) T(n)=O(n2)


例 5:三角形嵌套循环仍然是 O(n2)O(n^2)O(n2)

python 复制代码
def triangle_loop(n):
    for i in range(n):
        for j in range(i):
            print(i, j)

执行次数:

0+1+2+⋯+(n−1)=n(n−1)2 0 + 1 + 2 + \cdots + (n-1) = \frac{n(n-1)}{2} 0+1+2+⋯+(n−1)=2n(n−1)

展开:

n22−n2 \frac{n^2}{2} - \frac{n}{2} 2n2−2n

忽略常数和低阶项:

T(n)=O(n2) T(n) = O(n^2) T(n)=O(n2)

虽然比标准双层循环少一半,但增长趋势仍是平方级。


例 6:循环变量倍增 O(log⁡n)O(\log n)O(logn)

python 复制代码
def logarithmic_loop(n):
    i = 1
    while i < n:
        print(i)
        i *= 2

执行次数 kkk 满足:

2k≥n 2^k \ge n 2k≥n

所以:

k=⌈log⁡2n⌉ k = \lceil \log_2 n \rceil k=⌈log2n⌉

因此:

T(n)=O(log⁡n) T(n) = O(\log n) T(n)=O(logn)

典型场景:二分查找、平衡二叉树查找。


例 7:二分查找 O(log⁡n)O(\log n)O(logn)

python 复制代码
def binary_search(arr, target):
    left, right = 0, len(arr) - 1

    while left <= right:
        mid = (left + right) // 2

        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            left = mid + 1
        else:
            right = mid - 1

    return -1

每次比较后,搜索范围大约减半:

n→n2→n4→⋯→1 n \rightarrow \frac{n}{2} \rightarrow \frac{n}{4} \rightarrow \cdots \rightarrow 1 n→2n→4n→⋯→1

需要次数:

log⁡2n \log_2 n log2n

所以:

T(n)=O(log⁡n) T(n) = O(\log n) T(n)=O(logn)

空间复杂度:迭代版本只用了几个变量,O(1)O(1)O(1)。


例 8:归并排序 O(nlog⁡n)O(n \log n)O(nlogn)

归并排序递归式:

T(n)=2T(n2)+O(n) T(n) = 2T\left(\frac{n}{2}\right) + O(n) T(n)=2T(2n)+O(n)

解释:

  • 分成两个规模为 n/2n/2n/2 的子问题:2T(n/2)2T(n/2)2T(n/2)
  • 合并两个有序数组需要 O(n)O(n)O(n)

根据主定理:

T(n)=O(nlog⁡n) T(n) = O(n \log n) T(n)=O(nlogn)

空间复杂度:需要额外数组,O(n)O(n)O(n)。


例 9:递归阶乘 O(n)O(n)O(n)

python 复制代码
def factorial(n):
    if n <= 1:
        return 1
    return n * factorial(n - 1)

递归式:

T(n)=T(n−1)+O(1) T(n) = T(n-1) + O(1) T(n)=T(n−1)+O(1)

展开:

T(n)=T(n−1)+1=T(n−2)+2=⋯=O(n) T(n) = T(n-1) + 1 = T(n-2) + 2 = \cdots = O(n) T(n)=T(n−1)+1=T(n−2)+2=⋯=O(n)

所以时间复杂度:

O(n) O(n) O(n)

空间复杂度:递归调用栈深度为 nnn,所以:

O(n) O(n) O(n)


例 10:朴素斐波那契 O(2n)O(2^n)O(2n)

python 复制代码
def fib(n):
    if n <= 1:
        return n
    return fib(n - 1) + fib(n - 2)

递归式近似:

T(n)=T(n−1)+T(n−2)+O(1) T(n) = T(n-1) + T(n-2) + O(1) T(n)=T(n−1)+T(n−2)+O(1)

这棵递归树接近指数增长:

T(n)=O(2n) T(n) = O(2^n) T(n)=O(2n)

更精确地说,约为:

O(φn) O(\varphi^n) O(φn)

其中 φ≈1.618\varphi \approx 1.618φ≈1.618,但通常写 O(2n)O(2^n)O(2n)。

空间复杂度:递归栈最大深度为 nnn,所以 O(n)O(n)O(n)。

如果使用记忆化,时间复杂度降为 O(n)O(n)O(n),空间复杂度为 O(n)O(n)O(n)。


例 11:全排列 O(n!)O(n!)O(n!)

python 复制代码
def permute(arr, start):
    if start == len(arr):
        print(arr)
        return

    for i in range(start, len(arr)):
        arr[start], arr[i] = arr[i], arr[start]
        permute(arr, start + 1)
        arr[start], arr[i] = arr[i], arr[start]

第一次有 nnn 种选择,第二次有 n−1n-1n−1 种,第三次 n−2n-2n−2 种......

总排列数:

n×(n−1)×(n−2)×⋯×1=n! n \times (n-1) \times (n-2) \times \cdots \times 1 = n! n×(n−1)×(n−2)×⋯×1=n!

所以:

T(n)=O(n!) T(n) = O(n!) T(n)=O(n!)

空间复杂度:递归栈深度 nnn,即 O(n)O(n)O(n),不计输出结果。


例 12:最好、最坏、平均情况

以顺序查找为例:

python 复制代码
def linear_search(arr, target):
    for i in range(len(arr)):
        if arr[i] == target:
            return i
    return -1
  • 最好情况:第一个元素就是目标,O(1)O(1)O(1)
  • 最坏情况:目标不存在或在最后,O(n)O(n)O(n)
  • 平均情况:大约检查 n/2n/2n/2 个元素,仍为 O(n)O(n)O(n)

所以通常说顺序查找时间复杂度是:

O(n) O(n) O(n)


三、空间复杂度

1. 定义

空间复杂度衡量算法运行过程中所需存储空间随输入规模 nnn 的增长关系。

严格来说,总空间包括:

  • 输入数据本身
  • 算法代码
  • 临时变量
  • 辅助数据结构
  • 递归调用栈

但在算法分析中,通常只关注额外辅助空间,不把输入本身算进去。


2. 常见空间复杂度

复杂度 名称 典型场景
O(1)O(1)O(1) 常数空间 冒泡排序、迭代二分
O(log⁡n)O(\log n)O(logn) 对数空间 递归二分、平衡递归
O(n)O(n)O(n) 线性空间 复制数组、归并排序
O(n2)O(n^2)O(n2) 平方空间 二维矩阵、邻接矩阵

3. 空间复杂度计算例子

例 1:冒泡排序 O(1)O(1)O(1)

冒泡排序只用了几个临时变量,如 i、j、temp,不随 nnn 增大而增大。

空间复杂度:

O(1) O(1) O(1)

例 2:复制数组 O(n)O(n)O(n)
python 复制代码
def copy_array(arr):
    new_arr = []
    for x in arr:
        new_arr.append(x)
    return new_arr

新数组长度与输入相同。

空间复杂度:

O(n) O(n) O(n)

例 3:递归二分查找 O(log⁡n)O(\log n)O(logn)

递归版二分查找每次调用栈深度为:

log⁡n \log n logn

所以空间复杂度:

O(log⁡n) O(\log n) O(logn)

迭代版则是:

O(1) O(1) O(1)

例 4:归并排序 O(n)O(n)O(n)

归并排序需要额外数组来合并两个有序子数组。

空间复杂度:

O(n) O(n) O(n)

递归栈深度是 O(log⁡n)O(\log n)O(logn),但辅助数组 O(n)O(n)O(n) 更大,所以总体为 O(n)O(n)O(n)。


四、算法稳定性

1. 什么是稳定性?

排序算法的稳定性是指:

对于数组中值相同的元素,排序后它们的相对顺序是否保持不变。

如果相同值的元素排序后仍然保持原来的先后顺序,则算法是稳定的。

例如原数组:

text 复制代码
[(3, A), (1, B), (3, C), (2, D)]

按数字升序排序。

稳定排序结果:

text 复制代码
[(1, B), (2, D), (3, A), (3, C)]

两个 333 中,原来的 AAA 在 CCC 前面,排序后仍然 AAA 在 CCC 前面。

不稳定排序可能得到:

text 复制代码
[(1, B), (2, D), (3, C), (3, A)]

两个 333 的相对顺序改变了。


2. 常见排序算法稳定性

排序算法 时间复杂度 空间复杂度 是否稳定
冒泡排序 最好 O(n)O(n)O(n),最坏 O(n2)O(n^2)O(n2) O(1)O(1)O(1) 稳定
插入排序 最好 O(n)O(n)O(n),最坏 O(n2)O(n^2)O(n2) O(1)O(1)O(1) 稳定
归并排序 O(nlog⁡n)O(n \log n)O(nlogn) O(n)O(n)O(n) 稳定
快速排序 平均 O(nlog⁡n)O(n \log n)O(nlogn),最坏 O(n2)O(n^2)O(n2) 平均 O(log⁡n)O(\log n)O(logn),最坏 O(n)O(n)O(n) 不稳定
选择排序 O(n2)O(n^2)O(n2) O(1)O(1)O(1) 不稳定
堆排序 O(nlog⁡n)O(n \log n)O(nlogn) O(1)O(1)O(1) 不稳定

3. 为什么选择排序不稳定?

原数组:

text 复制代码
[(5, A), (5, B), (2, C)]

选择排序第一轮找到最小值 222,然后与第一个元素 5A5A5A 交换:

text 复制代码
[(2, C), (5, B), (5, A)]

两个 555 的顺序从原来的:

text 复制代码
5A, 5B

变成了:

text 复制代码
5B, 5A

所以选择排序不稳定。


4. 为什么冒泡排序稳定?

冒泡排序只在前一个元素大于后一个元素时交换。

如果两个元素相等:

python 复制代码
if arr[j] > arr[j + 1]:
    swap(arr[j], arr[j + 1])

使用 > 而不是 >=,相等时不会交换。

因此相同值的元素相对顺序不会改变,所以冒泡排序稳定。


5. 为什么归并排序稳定?

归并排序在合并两个有序数组时,如果左右两边元素相等,优先取左边的元素。

例如:

text 复制代码
左边:[3A]
右边:[3B]

合并时先取 3A,再取 3B,保持原顺序。

所以归并排序稳定。


五、总结

1. 时间复杂度

时间复杂度衡量算法操作次数随输入规模 nnn 的增长趋势。

大 OOO 忽略常数和低阶项,只保留最高阶。

常见复杂度:

O(1)<O(log⁡n)<O(n)<O(nlog⁡n)<O(n2)<O(2n)<O(n!) O(1) < O(\log n) < O(n) < O(n \log n) < O(n^2) < O(2^n) < O(n!) O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(2n)<O(n!)

2. 计算时间复杂度

  • 单层循环:O(n)O(n)O(n)
  • 双层嵌套:通常 O(n2)O(n^2)O(n2)
  • 每次减半:O(log⁡n)O(\log n)O(logn)
  • 递归分治:列递归式,如归并排序 O(nlog⁡n)O(n \log n)O(nlogn)
  • 指数递归:如朴素斐波那契 O(2n)O(2^n)O(2n)
  • 全排列:O(n!)O(n!)O(n!)

3. 空间复杂度

空间复杂度衡量额外辅助空间增长,常见有 O(1)O(1)O(1)、O(log⁡n)O(\log n)O(logn)、O(n)O(n)O(n)。

4. 稳定性

稳定性表示相同值元素排序后相对顺序是否保持。

  • 稳定排序:冒泡、插入、归并。
  • 不稳定排序:快速、选择、堆。

掌握这些概念后,你就可以分析大多数常见算法的时间复杂度、空间复杂度和稳定性。

相关推荐
白色的北极熊1 小时前
c语言 求余运算符
c语言·数据结构·算法
波尔德1 小时前
Origin 2024 绘制钟形正态分布曲线:填充颜色并导出透明 PNG
人工智能·算法
Omics Pro1 小时前
微软:多模态生物世界模型
数据库·人工智能·算法·microsoft·机器学习·自然语言处理
by209991 小时前
list 的使用:把节点、位置和操作联系起来的综合叙述(上)
数据结构·c++·笔记·list
hai3152475432 小时前
语言学集合定律
人工智能·算法
小溪学编程2 小时前
C语言篇:语言结构
c语言·开发语言·算法
東隅已逝,桑榆非晚2 小时前
数据结构队列
数据结构·笔记·学习
老王爱玩车2 小时前
字符串和字符串函数
c语言·开发语言·数据结构·学习
Ivanqhz2 小时前
KV Cache
服务器·数据库·人工智能·深度学习·算法