常见算法的时间复杂度

一、常见算法时间复杂度速查表

算法类别 具体算法 最佳情况 平均情况 最坏情况 空间复杂度 (通常) 关键特点/说明
排序算法            
  冒泡排序 (Bubble Sort) O(n) (优化后) O(n²) O(n²) O(1) 简单,效率低,原地排序
  选择排序 (Selection Sort) O(n²) O(n²) O(n²) O(1) 简单,效率低,交换次数少,原地排序
  插入排序 (Insertion Sort) O(n) (已有序) O(n²) O(n²) O(1) 小规模或基本有序数据高效,原地排序
  希尔排序 (Shell Sort) O(n log n) 取决于间隔序列 O(n²) O(1) 插入排序改进版,不稳定
  归并排序 (Merge Sort) O(n log n) O(n log n) O(n log n) O(n) 稳定,高效,分治典范,需额外空间
  快速排序 (Quick Sort) O(n log n) O(n log n) O(n²) (坏划分) O(log n) (递归栈) 通常最快,分治,原地排序,最坏情况依赖划分
  堆排序 (Heap Sort) O(n log n) O(n log n) O(n log n) O(1) 原地排序,不稳定
  计数排序 (Counting Sort) O(n + k) O(n + k) O(n + k) O(n + k) 非比较排序,k是数据范围,范围小高效
  桶排序 (Bucket Sort) O(n + k) O(n + k) O(n²) (坏分布) O(n + k) 非比较排序,数据分布均匀高效
  基数排序 (Radix Sort) O(d*(n + b)) O(d*(n + b)) O(d*(n + b)) O(n + b) 非比较排序,d是位数,b是基数(桶数),稳定
搜索算法            
  线性搜索 (Linear Search) O(1) (第一个) O(n) O(n) O(1) 无序数组
  二分查找 (Binary Search) O(1) (中间) O(log n) O(log n) O(1) (迭代) 必须有序数组,分治思想
数据结构操作            
  数组          
  访问 (by index) O(1) O(1) O(1) -  
  插入/删除 (末尾) O(1) (摊销) O(1) (摊销) O(1) (摊销) - 动态数组扩容有摊销成本
  插入/删除 (中间/开头) O(n) O(n) O(n) - 需要移动元素
  搜索 (无序) O(n) O(n) O(n) -  
  搜索 (有序) O(log n) O(log n) O(log n) - 二分查找
  链表 (单/双)          
  访问 (by index) O(n) O(n) O(n) - 需要遍历
  插入/删除 (已知位置) O(1) O(1) O(1) - 前提是已获得要操作节点的引用
  插入/删除 (头/尾) O(1) O(1) O(1) - 通常维护头尾指针
  搜索 O(n) O(n) O(n) - 需要遍历
  哈希表 (HashMap/Dict)          
  插入 (Insert) O(1) (摊销) O(1) (摊销) O(n) (全冲突) - 理想情况O(1),冲突处理影响效率
  查找 (Lookup) O(1) (摊销) O(1) (摊销) O(n) (全冲突) - 理想情况O(1),冲突处理影响效率
  删除 (Delete) O(1) (摊销) O(1) (摊销) O(n) (全冲突) - 理想情况O(1),冲突处理影响效率
  平衡二叉搜索树 (AVL, RB)          
  插入 (Insert) O(1) O(log n) O(log n) O(log n) (递归栈) 自平衡保证O(log n)
  查找 (Search) O(1) (根) O(log n) O(log n) O(log n) (递归栈) 自平衡保证O(log n)
  删除 (Delete) O(1) (叶子) O(log n) O(log n) O(log n) (递归栈) 自平衡保证O(log n)
  二叉堆 (优先队列)          
  插入 (Insert) / 入队 (Enqueue) O(1) (摊销) O(log n) O(log n) - 堆化 (Heapify Up)
  取最小/最大 (Find Min/Max) O(1) O(1) O(1) - 查看根节点
  删除最小/最大 (Extract) O(log n) O(log n) O(log n) - 堆化 (Heapify Down)
  构建堆 (Heap Build) O(n) O(n) O(n) - Floyd算法,自底向上堆化
图算法            
  广度优先搜索 (BFS) O(V + E) O(V + E) O(V + E) O(V) 邻接表或邻接矩阵(需转换)
  深度优先搜索 (DFS) O(V + E) O(V + E) O(V + E) O(V) (递归栈) 邻接表或邻接矩阵(需转换)
  Dijkstra (无负权) O(V log V + E) O(V log V + E) O(V log V + E) O(V) 优先队列 (堆) 优化后
  Bellman-Ford (可有负权) O(V*E) O(V*E) O(V*E) O(V) 检测负权环
  Floyd-Warshall (APSP) O(V³) O(V³) O(V³) O(V²) 所有节点对最短路径,动态规划
  Prim (MST - 邻接矩阵) O(V²) O(V²) O(V²) O(V)  
  Prim (MST - 邻接表+堆) O(E log V) O(E log V) O(E log V) O(V) 优先队列 (堆) 优化后
  Kruskal (MST) O(E log E) O(E log E) O(E log E) O(E+V) 并查集优化后,主要成本在排序边(E log E)
字符串匹配            
  朴素算法 (Brute Force) O(n) (首字符) O((n-m+1)*m) O(n*m) O(1) n=文本长, m=模式长
  KMP (Knuth-Morris-Pratt) O(n + m) O(n + m) O(n + m) O(m) 预处理O(m),匹配O(n)
  Rabin-Karp O(n + m) O(n + m) O(n*m) (坏哈希) O(1) 基于哈希,平均好,最坏差(哈希冲突)

二、时间复杂度推断核心技巧 (如何分析?)

分析时间复杂度关键在于找出基本操作执行次数 T(n) 与输入规模 n 之间的函数关系,然后用大O表示法简化(忽略常数、系数、低阶项)。

以下是实用的推断技巧:

  1. 聚焦循环 (Loops are Key):
    • 单层循环: 循环体执行次数通常与 n 线性相关 -> O(n)
      for i in range(n):  # 循环 n 次
          do_something()  # O(1) 操作
      # T(n) = n * O(1) = O(n)
      
    • 嵌套循环: 分析每一层循环的迭代次数。
      • 独立嵌套: 内层循环次数固定或与外层无关 -> 相乘。
        for i in range(n):      # 循环 n 次
            for j in range(10): # 固定循环 10 次 (常数)
                do_something()  # O(1)
        # T(n) = n * 10 * O(1) = O(n)
        
      • 相关嵌套: 内层循环次数依赖于外层循环变量 -> 通常是求和。
        for i in range(n):         # i 从 0 到 n-1
            for j in range(i, n):  # j 从 i 到 n-1 -> 执行 (n - i) 次
                do_something()     # O(1)
        # T(n) = Σ(i=0 to n-1) Σ(j=i to n-1) 1 = Σ(i=0 to n-1) (n - i)
        #       = n + (n-1) + ... + 1 = n(n+1)/2 = O(n²)
        
      • 多重嵌套: 嵌套层数 k 层,且每层大致循环 n 次 -> O(n^k)
        for i in range(n):
            for j in range(n):
                for k in range(n):
                    do_something() # O(1)
        # T(n) = n * n * n * O(1) = O(n³)
        
  2. 理解递归 (Recursion Requires Equations):
    • 递归算法的时间复杂度分析通常需要建立递归方程并求解。
    • 主定理 (Master Theorem): 解决形如 T(n) = a * T(n/b) + f(n) 的递归方程。这是分析分治算法(如归并、快排、二分查找)的利器。主定理根据 f(n)n^(log_b(a)) 的关系给出解:
      • f(n) = O(n^(log_b(a) - ε)) (ε > 0) -> T(n) = Θ(n^(log_b(a)))
      • f(n) = Θ(n^(log_b(a))) -> T(n) = Θ(n^(log_b(a)) * log n)
      • f(n) = Ω(n^(log_b(a) + ε)) (ε > 0) 且 a*f(n/b) <= c*f(n) (c<1) -> T(n) = Θ(f(n))
    • 递归树 (Recursion Tree): 可视化递归调用过程,计算每层工作量和总层数。
      • 例如斐波那契 F(n) = F(n-1) + F(n-2),递归树是二叉树,节点数约 2^n -> O(2^n)
    • 展开/代入法 (Iteration/Substitution): 反复将递归式右边的 T(...) 用定义展开,观察规律或求和。
  3. 识别分治 (Divide and Conquer Pattern):
    • 模式:将问题分成 a 个大小为 n/b 的子问题,合并结果代价为 f(n)
    • 时间复杂度通式:T(n) = a * T(n/b) + f(n) -> 用主定理分析。
    • 经典例子:
      • 二分查找: T(n) = T(n/2) + O(1) -> a=1, b=2, f(n)=O(1) -> O(log n) (Case 2)。
      • 归并排序: T(n) = 2T(n/2) + O(n) -> a=2, b=2, f(n)=O(n) -> O(n log n) (Case 2)。
      • 快速排序 (平均): T(n) = T(k) + T(n-k-1) + O(n),假设划分均衡 k≈n/2 -> ≈ 2T(n/2) + O(n) -> O(n log n) (Case 2)。最坏划分 k=0 或 k=n-1 -> T(n) = T(n-1) + O(n) -> O(n²)
  4. 关注数据结构和操作 (Data Structures Matter):
    • 不同数据结构的同一操作(如查找、插入、删除)时间复杂度可能天差地别。
    • 关键问题:
      • 这个操作需要遍历多少元素?(线性搜索 vs 二分查找 vs 哈希查找)
      • 这个操作需要移动多少元素?(数组中间插入 vs 链表已知位置插入)
      • 这个操作需要维护什么额外结构?(堆插入后需要堆化 O(log n),平衡树插入后可能需要旋转 O(log n))
    • 例子: 在已排序数组中查找 -> O(log n) (二分),在哈希表中查找 -> O(1) (平均),在链表中查找 -> O(n)
  5. 注意“隐藏”成本 (Watch for Hidden Costs):
    • 函数调用: 递归调用、辅助函数调用会增加栈开销,影响实际常数因子,但不改变大O阶(除非递归深度很大,如链表递归 O(n) 深度)。
    • 内存分配: 动态数组扩容 (append 的摊销 O(1))。
    • 内置操作: 某些语言的内置操作可能不是 O(1)。例如 Python 的 len(list)O(1)(列表存储了长度),而 min(list)O(n)
  6. 考虑输入特征 (Consider Input Characteristics):
    • 最好/最坏/平均: 明确分析的是哪种情况。快排最坏 O(n²),平均 O(n log n)
    • 数据分布: 桶排序、计数排序的效率高度依赖输入数据的范围或分布。
    • 问题规模: n 代表什么?是数组长度、节点数、边数、还是数值大小?分析 T(n)n 必须一致。
  7. 利用已知结论 (Leverage Known Results):
    • 记住常见算法和操作的复杂度(如排序下限 O(n log n),堆操作 O(log n),BFS/DFS O(V+E))。
    • 组合操作:如果算法由多个步骤组成,总复杂度由最高阶的步骤决定(O(n²) + O(n log n) + O(n) = O(n²))。

三、核心推断步骤总结

  1. 定义 n: 明确输入规模 n 指什么(数组长度?节点数?数值位数?)。
  2. 识别基本操作: 找到执行最频繁、最耗时的核心操作(比较、赋值、算术、特定函数调用)。
  3. 计算 T(n)
    • 对于循环:分析循环次数(单层、嵌套层数、迭代次数关系)。
    • 对于递归:建立递归方程,使用主定理、递归树或代入法求解。
    • 对于分治:识别 a, b, f(n),应用主定理。
    • 考虑数据结构的固有操作成本。
  4. 简化到大O: 忽略常数因子、系数、低阶项,保留最高阶项。
  5. 明确情况: 说明是最好、最坏还是平均时间复杂度。

参考资料