O(n log n):线性对数原理拆解 这个是排序算法的黄金复杂度之一。

一、O(n log n) 原理拆解

1. "log n" 是什么?

log n 是"将数据分半处理的次数"。

举例:

  • n = 8,分半次数 = log₂8 = 3 次
  • n = 16,分半次数 = log₂16 = 4 次
  • n = 100万,分半次数 ≈ log₂1000000 ≈ 20 次

2. "n × log n" 的含义

O(n log n) = 每层处理 n 个数据 × 一共 log n 层

用图理解:

复制代码
原始数据(8个):[8, 3, 1, 7, 0, 9, 2, 5]

第0层(不分):      [8, 3, 1, 7, 0, 9, 2, 5]     ← 8 个数据
                    /                 \
第1层(分2半):  [8, 3, 1, 7]      [0, 9, 2, 5]   ← 每半 4 个,共 8 个
                 /      \          /      \
第2层(分4半):[8,3]   [1,7]    [0,9]   [2,5]    ← 每半 2 个,共 8 个
               /  \    /  \     /  \    /  \
第3层(分8半):[8] [3] [1] [7] [0] [9] [2] [5]   ← 每半 1 个,共 8 个
  • 每层处理的数据总数:都是 n 个(8个)
  • 层数:log₂n = 3 层
  • 总工作量:n × log n = 8 × 3 = 24 次操作

这就是 O(n log n) 的直观含义!


二、归并排序(Merge Sort)详细讲解

核心思想:分而治之 + 合并

python 复制代码
def merge_sort(arr):
    # 1. 递归终止:只剩 1 个元素
    if len(arr) <= 1:
        return arr
    
    # 2. 分(Divide):从中间分成两半
    mid = len(arr) // 2
    left = merge_sort(arr[:mid])   # 递归排序左半
    right = merge_sort(arr[mid:])  # 递归排序右半
    
    # 3. 治(Conquer):合并两个有序数组
    return merge(left, right)

def merge(left, right):
    result = []
    i = j = 0
    
    # 比较两个数组头部,取小的
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1
    
    # 添加剩余元素
    result.extend(left[i:])
    result.extend(right[j:])
    return result

逐步演示(n=8)

数组[8, 3, 1, 7, 0, 9, 2, 5]


第1步:分解阶段(拆分)

复制代码
层0: [8, 3, 1, 7, 0, 9, 2, 5]        ← 8个数据
         ↓ split
层1: [8, 3, 1, 7]    [0, 9, 2, 5]   ← 各4个,共8个
         ↓ split          ↓ split
层2: [8, 3] [1, 7]    [0, 9] [2, 5]  ← 各2个,共8个
       ↓ split ↓ split  ↓ split ↓ split
层3: [8] [3] [1] [7]  [0] [9] [2] [5] ← 各1个,共8个

拆分次数:log₂8 = 3 次


第2步:合并阶段(归并)

层3 → 层2(合并两个1个元素的数组)

复制代码
合并 [8] 和 [3] → [3, 8]      ← 比较1次,移动2次
合并 [1] 和 [7] → [1, 7]      ← 比较1次,移动2次
合并 [0] 和 [9] → [0, 9]      ← 比较1次,移动2次
合并 [2] 和 [5] → [2, 5]      ← 比较1次,移动2次

本层操作:4次合并 × 平均2个元素 = 8次操作


层2 → 层1(合并两个2个元素的数组)

复制代码
合并 [3,8] 和 [1,7] → [1,3,7,8]
  比较: 3 vs 1 → 取1
       3 vs 7 → 取3
       8 vs 7 → 取7
       取8
  共4次操作

合并 [0,9] 和 [2,5] → [0,2,5,9]
  比较: 0 vs 2 → 取0
       9 vs 2 → 取2
       9 vs 5 → 取5
       取9
  共4次操作

本层操作:2次合并 × 4个元素 = 8次操作


层1 → 层0(合并两个4个元素的数组)

复制代码
合并 [1,3,7,8] 和 [0,2,5,9] → [0,1,2,3,5,7,8,9]
  比较: 1 vs 0 → 取0
       1 vs 2 → 取1
       3 vs 2 → 取2
       3 vs 5 → 取3
       7 vs 5 → 取5
       7 vs 9 → 取7
       8 vs 9 → 取8
       取9
  共8次操作

本层操作:1次合并 × 8个元素 = 8次操作


归并排序时间复杂度计算

层数 操作内容 每层操作数
层3→2 4次合并,每次2个元素 8
层2→1 2次合并,每次4个元素 8
层1→0 1次合并,每次8个元素 8

总操作数 = 8 × 3 = 24

公式:n × log₂n = 8 × 3 = 24 ✅


三、堆排序(Heap Sort)详细讲解

核心思想:利用"堆"这种数据结构

堆是一棵完全二叉树,父节点 > 子节点(大顶堆)。

python 复制代码
def heap_sort(arr):
    n = len(arr)
    
    # 1. 建堆(从最后一个非叶子节点开始)
    for i in range(n // 2 - 1, -1, -1):
        heapify(arr, n, i)
    
    # 2. 排序(每次把最大值放到末尾)
    for i in range(n - 1, 0, -1):
        arr[i], arr[0] = arr[0], arr[i]  # 交换堆顶和末尾
        heapify(arr, i, 0)                # 调整堆

def heapify(arr, n, i):
    largest = i
    left = 2 * i + 1
    right = 2 * i + 2
    
    if left < n and arr[left] > arr[largest]:
        largest = left
    if right < n and arr[right] > arr[largest]:
        largest = right
    
    if largest != i:
        arr[i], arr[largest] = arr[largest], arr[i]
        heapify(arr, n, largest)  # 递归调整

复杂度分析

建堆阶段:O(n)

  • 看似 n × log n,但实际数学证明是 O(n)

排序阶段:O(n log n)

  • 每次取出堆顶(最大值):O(1)
  • 调整堆:O(log n)
  • 共 n 次 → O(n log n)

为什么是 n log n?

复制代码
第1次取最大值 → 调整堆 → log₂n 次比较
第2次取最大值 → 调整堆 → log₂n 次比较
...
第n次取最大值 → 调整堆 → log₂n 次比较

总共:n × log₂n


四、快速排序(Quick Sort)平均情况

核心思想:选基准值,分区

python 复制代码
def quick_sort(arr):
    if len(arr) <= 1:
        return arr
    
    pivot = arr[len(arr) // 2]  # 选基准
    left = [x for x in arr if x < pivot]
    middle = [x for x in arr if x == pivot]
    right = [x for x in arr if x > pivot]
    
    return quick_sort(left) + middle + quick_sort(right)

为什么平均是 O(n log n)?

理想情况:每次选的基准值都在中间,把数组均匀分成两半

复制代码
第0层: n 个元素  ← 分区操作:n次比较
  ↓ 分成两半
第1层: n/2 + n/2 = n 个元素  ← 分区操作:n次比较
  ↓ 再分
第2层: n/4 + n/4 + n/4 + n/4 = n 个元素  ← 分区操作:n次比较
  ↓
...
第 log n 层: 1+1+...+1 = n 个元素  ← 分区操作:n次比较

总操作 :每层 n 次 × log n 层 = n log n


对比:最坏情况 O(n²)

如果数组已经有序,每次都选到最大/最小值:

复制代码
第0层: n 个元素
第1层: n-1 个元素  ← 只分出一个
第2层: n-2 个元素
...
总操作: n + (n-1) + (n-2) + ... + 1 = n(n+1)/2 = O(n²)

这就是为什么快速排序需要随机打乱三数取中来避免最坏情况。


五、TimSort(Python 内置排序)

核心思想:混合算法 = 归并排序 + 插入排序

python 复制代码
# Python 实际用的是 TimSort(C语言实现)
arr.sort()  # 或 sorted(arr)

为什么也是 O(n log n)?

策略

  1. 识别自然有序片段(runs):已经有序的子数组

  2. 对小片段用插入排序(≤32个元素):O(k²) 但 k 很小

  3. 用归并排序合并片段:O(n log n)

    数组: [5, 8, 2, 9, 1, 3, 7, 6, 4]
    ↓ 识别 runs
    run1: [5, 8] ← 自然升序
    run2: [2, 9] ← 自然升序
    run3: [1, 3, 7] ← 自然升序
    run4: [6] ← 单独
    run5: [4] ← 单独
    ↓ 插入排序扩展小片段
    [2, 5, 8, 9], [1, 3, 7], [4, 6]
    ↓ 归并合并
    [1, 2, 3, 5, 7, 8, 9], [4, 6]
    ↓ 归并合并
    [1, 2, 3, 4, 5, 6, 7, 8, 9]

时间复杂度

  • 识别 runs:O(n)
  • 插入排序小片段:O(n)(因为片段很小,近似线性)
  • 归并合并:O(n log n)
  • 总复杂度:O(n log n)

六、分治算法汇总

分治三部曲

复制代码
1. 分(Divide):把问题分成子问题
2. 治(Conquer):递归解决子问题
3. 合(Combine):合并子问题的结果

常见分治算法及其复杂度

算法 总复杂度
归并排序 O(1) 2×T(n/2) O(n) O(n log n)
快速排序 O(n) 2×T(n/2) O(1) O(n log n) 平均
二分查找 O(1) T(n/2) O(1) O(log n)
大整数乘法 O(1) 3×T(n/2) O(n) O(n^1.585)
矩阵乘法 O(1) 8×T(n/2) O(n²) O(n³)

分治算法的递推公式

归并排序

复制代码
T(n) = 2T(n/2) + O(n)
     = 2[2T(n/4) + O(n/2)] + O(n)
     = 4T(n/4) + 2O(n/2) + O(n)
     = ...
     = nT(1) + O(n) × log₂n
     = O(n log n)

二分查找

复制代码
T(n) = T(n/2) + O(1)
     = T(n/4) + O(1) + O(1)
     = ...
     = O(log n)

总结对比表

算法 最好 平均 最坏 是否稳定 是否原地
归并排序 O(n log n) O(n log n) O(n log n) ✅ 稳定 ❌ 需额外空间
堆排序 O(n log n) O(n log n) O(n log n) ❌ 不稳定 ✅ 原地
快速排序 O(n log n) O(n log n) O(n²) ❌ 不稳定 ✅ 原地
TimSort O(n) O(n log n) O(n log n) ✅ 稳定 ❌ 需额外空间

记忆口诀

归并排 :层层分,层层合,每层 n 个,共 log n 层

快排 :平均分两半,每层 n 个,共 log n 层

堆排:建堆 O(n),取 n 次,每次调整 log n

希望这个详细讲解能帮你彻底理解 O(n log n)!如果还有疑问,欢迎继续问!

相关推荐
Shell运维手记2 小时前
Linux 常用基础命令学习笔记
linux·运维·笔记·学习·算法·github
船厂电气自动化ai大模型2 小时前
AI大模型与数学 第32课 函数凹凸性与二阶导数:拐点求解、凹凸区间计算(10道二阶导数计算题)
数据结构·人工智能·python·深度学习·算法
旖旎夜光2 小时前
LeetCode 3:无重复字符的最长子串(滑动窗口) —— 题解
数据结构·c++·算法·leetcode·滑动窗口
叠层归一研究院4 小时前
AGI 系统(十一):二阶网络 — 二阶递归 × 多主体 (元符号网络)
开发语言·人工智能·算法·php·agi
zlinear数据采集卡4 小时前
D223的PWM电机控制:6路独立脉冲+加减速算法深度解析
arm开发·stm32·嵌入式硬件·算法·fpga开发·架构
杨石兴5 小时前
31、玩具MoM:用2x2矩阵串起全套流程
人工智能·算法·矩阵·电磁学
江畔柳前堤5 小时前
Function Calling 与 Tool Calling:从认知到工程的全景深度解析
开发语言·网络·人工智能·深度学习·算法·机器学习·php
Navigator_Z5 小时前
LeetCode //C - 1192. Critical Connections in a Network
c语言·算法·leetcode
zander2585 小时前
LeetCode 739:每日温度——为什么单调栈要持续弹出
开发语言·python·算法