一、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)?
策略:
-
识别自然有序片段(runs):已经有序的子数组
-
对小片段用插入排序(≤32个元素):O(k²) 但 k 很小
-
用归并排序合并片段: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)!如果还有疑问,欢迎继续问!