归并排序(Merge Sort)
归并排序(Merge Sort)
一、核心思想
归并排序是分治思想的经典范本,它的策略可以概括为三步:
- 分:把数组从中间一分为二
- 治:对两个子数组各自递归排序
- 合:把两个已排序的子数组合并为一个有序数组
整个过程就像两支各自有序的队伍合并成一支:每次从两队头部取较小的那个放到结果队中,直到两队都排完。
原始: [8, 3, 7, 1, 5, 2, 6, 4]
第1层分:
左 [8, 3, 7, 1] 右 [5, 2, 6, 4]
第2层分:
[8, 3] [7, 1] [5, 2] [6, 4]
第3层分:
[8] [3] [7] [1] [5] [2] [6] [4] ← 单元素天然有序
第3层合:
[3,8] [1,7] [2,5] [4,6]
第2层合:
[1,3,7,8] [2,4,5,6]
第1层合:
[1,2,3,4,5,6,7,8] ← 最终结果
二、为什么归并排序重要
归并排序有几个快排不具备的独特优势:
- 稳定排序:合并时相同元素保持原始相对顺序
- 最坏也是 O(n log n):不像快排可能退化到 O(n²)
- 适合外部排序:数据量大到内存装不下时,可以分批排序再合并
- 适合链表排序:链表不像数组需要随机访问,归并排序对链表特别高效
三、Go 语言实现
递归版(经典实现)
package main
import "fmt"
// MergeSort 归并排序,返回新的有序切片
func MergeSort(arr []int) []int {
n := len(arr)
if n <= 1 {
return arr
}
mid := n / 2
left := MergeSort(arr[:mid])
right := MergeSort(arr[mid:])
return merge(left, right)
}
// merge 将两个有序切片合并为一个有序切片
func merge(left, right []int) []int {
result := make([]int, 0, len(left)+len(right))
i, j := 0, 0
for i < len(left) && j < len(right) {
if left[i] <= right[j] { // <= 保证稳定性
result = append(result, left[i])
i++
} else {
result = append(result, right[j])
j++
}
}
// 剩余部分直接追加
result = append(result, left[i:]...)
result = append(result, right[j:]...)
return result
}
func main() {
data := []int{8, 3, 7, 1, 5, 2, 6, 4}
fmt.Println("排序前:", data)
sorted := MergeSort(data)
fmt.Println("排序后:", sorted)
}
原地版(减少内存分配)
上面的版本每次 merge 都创建新切片,内存开销大。原地版使用一个预分配的辅助数组,避免反复分配:
package main
import "fmt"
// MergeSortInPlace 原地归并排序,使用预分配辅助数组
func MergeSortInPlace(arr []int) {
n := len(arr)
if n <= 1 {
return
}
aux := make([]int, n) // 只分配一次辅助空间
mergeSortInPlaceHelper(arr, 0, n-1, aux)
}
func mergeSortInPlaceHelper(arr []int, lo, hi int, aux []int) {
if lo >= hi {
return
}
mid := lo + (hi-lo)/2
mergeSortInPlaceHelper(arr, lo, mid, aux)
mergeSortInPlaceHelper(arr, mid+1, hi, aux)
mergeInPlace(arr, lo, mid, hi, aux)
}
func mergeInPlace(arr []int, lo, mid, hi int, aux []int) {
// 先把 [lo..hi] 复制到辅助数组
for k := lo; k <= hi; k++ {
aux[k] = arr[k]
}
i, j := lo, mid+1
for k := lo; k <= hi; k++ {
if i > mid { // 左半已耗尽
arr[k] = aux[j]
j++
} else if j > hi { // 右半已耗尽
arr[k] = aux[i]
i++
} else if aux[i] <= aux[j] { // <=保证稳定
arr[k] = aux[i]
i++
} else {
arr[k] = aux[j]
j++
}
}
}
func main() {
data := []int{38, 27, 43, 3, 9, 82, 10}
fmt.Println("排序前:", data)
MergeSortInPlace(data)
fmt.Println("排序后:", data)
}
迭代版(自底向上,避免递归)
递归版的空间消耗来自栈,迭代版完全消除递归,从长度1的子序列开始逐层合并:
package main
import "fmt"
// MergeSortIterative 自底向上的迭代归并排序
func MergeSortIterative(arr []int) {
n := len(arr)
if n <= 1 {
return
}
aux := make([]int, n)
// width 从1开始,每轮翻倍:1 → 2 → 4 → 8 → ...
for width := 1; width < n; width *= 2 {
for lo := 0; lo < n-width; lo += 2 * width {
mid := lo + width - 1
hi := min(lo+2*width-1, n-1)
mergeInPlaceIter(arr, lo, mid, hi, aux)
}
}
}
func mergeInPlaceIter(arr []int, lo, mid, hi int, aux []int) {
for k := lo; k <= hi; k++ {
aux[k] = arr[k]
}
i, j := lo, mid+1
for k := lo; k <= hi; k++ {
if i > mid {
arr[k] = aux[j]; j++
} else if j > hi {
arr[k] = aux[i]; i++
} else if aux[i] <= aux[j] {
arr[k] = aux[i]; i++
} else {
arr[k] = aux[j]; j++
}
}
}
func min(a, b int) int {
if a < b { return a }
return b
}
func main() {
data := []int{5, 2, 4, 7, 1, 3, 2, 6, 0}
fmt.Println("排序前:", data)
MergeSortIterative(data)
fmt.Println("排序后:", data)
}
四、复杂度分析
| 情况 | 时间复杂度 | 说明 |
|---|---|---|
| 最好 | O(n log n) | 无论如何都要分到底再合回来 |
| 平均 | O(n log n) | 同上 |
| 最坏 | O(n log n) | 不受数据分布影响! |
这是归并排序最大的优势——时间复杂度不因数据特征而变化。
- 空间复杂度:O(n)(需要辅助数组),原地版也需要一个 n 大小的 aux
- 稳定性:✅ 稳定排序——合并时
<=条件保证相等元素左半先出
递归深度分析
每次递归把数组对半分,深度为 log₂n。每一层所有 merge 操作总共处理 n 个元素,所以总工作量 = 每层O(n) × 层数O(log n) = O(n log n)。
空间消耗来源
- 辅助数组:O(n),这是归并排序的主要空间开销
- 递归栈:O(log n),比辅助数组小得多
- 迭代版消除了栈开销,但辅助数组 O(n) 不可省略
五、归并排序 vs 快速排序
| 特性 | 归并排序 | 快速排序 |
|---|---|---|
| 时间(最好) | O(n log n) | O(n log n) |
| 时间(最坏) | O(n log n) ✅ | O(n²) ❌ |
| 空间 | O(n) | O(log n) ✅ |
| 稳定性 | 稳定 ✅ | 不稳定 ❌ |
| 原地排序 | 不是 | 是 |
| 实际速度 | 慢于快排(常数更大) | 工程中最快 |
| 适用场景 | 外部排序、链表排序 | 内存排序、通用排序 |
关键洞察:归并排序的常数因子比快排大——每轮 merge 需要复制整个数组到辅助空间再写回,而快排原地交换不需要额外空间。因此尽管两者都是 O(n log n),快排在实际运行中通常快 2~3 倍。
归并排序的价值在于它的稳定性和最坏情况保障——当这些特性是刚需时(如数据库排序、外部排序),归并排序无可替代。
六、外部排序简介
当数据量超过内存容量时,归并排序的思想依然适用:
- 把大文件分成多个内存能装下的小块
- 每个小块在内存中排序后写入临时文件
- 用多路归并把所有临时文件合并成最终有序文件
这就是外部排序的基本框架,核心就是归并排序的分治思想,只是"分"和"合"发生在磁盘层面。
七、今日练习
package main
import "fmt"
// 练习1: 实现原地归并排序并验证
func MergeSortInPlace(arr []int) {
n := len(arr)
if n <= 1 { return }
aux := make([]int, n)
msHelper(arr, 0, n-1, aux)
}
func msHelper(arr []int, lo, hi int, aux []int) {
if lo >= hi { return }
mid := lo + (hi-lo)/2
msHelper(arr, lo, mid, aux)
msHelper(arr, mid+1, hi, aux)
mergeOp(arr, lo, mid, hi, aux)
}
func mergeOp(arr []int, lo, mid, hi int, aux []int) {
for k := lo; k <= hi; k++ { aux[k] = arr[k] }
i, j := lo, mid+1
for k := lo; k <= hi; k++ {
if i > mid {
arr[k] = aux[j]; j++
} else if j > hi {
arr[k] = aux[i]; i++
} else if aux[i] <= aux[j] {
arr[k] = aux[i]; i++
} else {
arr[k] = aux[j]; j++
}
}
}
// 练习2: 合并两个已排序切片(不依赖归并排序框架)
func MergeTwoSorted(a, b []int) []int {
result := make([]int, 0, len(a)+len(b))
i, j := 0, 0
for i < len(a) && j < len(b) {
if a[i] <= b[j] {
result = append(result, a[i]); i++
} else {
result = append(result, b[j]); j++
}
}
result = append(result, a[i:]...)
result = append(result, b[j:]...)
return result
}
// 练习3: 对切片降序归并排序(修改合并比较方向)
func MergeSortDesc(arr []int) {
n := len(arr)
if n <= 1 { return }
aux := make([]int, n)
msDescHelper(arr, 0, n-1, aux)
}
func msDescHelper(arr []int, lo, hi int, aux []int) {
if lo >= hi { return }
mid := lo + (hi-lo)/2
msDescHelper(arr, lo, mid, aux)
msDescHelper(arr, mid+1, hi, aux)
mergeDesc(arr, lo, mid, hi, aux)
}
func mergeDesc(arr []int, lo, mid, hi int, aux []int) {
for k := lo; k <= hi; k++ { aux[k] = arr[k] }
i, j := lo, mid+1
for k := lo; k <= hi; k++ {
if i > mid {
arr[k] = aux[j]; j++
} else if j > hi {
arr[k] = aux[i]; i++
} else if aux[i] >= aux[j] { // 改为 >=
arr[k] = aux[i]; i++
} else {
arr[k] = aux[j]; j++
}
}
}
func main() {
// 验证原地归并排序
data := []int{38, 27, 43, 3, 9, 82, 10}
fmt.Println("升序排序前:", data)
MergeSortInPlace(data)
fmt.Println("升序排序后:", data)
// 验证合并两个已排序切片
a := []int{1, 3, 5, 7}
b := []int{2, 4, 6, 8, 10}
fmt.Println("\n合并:", a, "和", b)
fmt.Println("结果:", MergeTwoSorted(a, b))
// 验证降序归并排序
data2 := []int{5, 2, 8, 1, 9, 3}
fmt.Println("\n降序排序前:", data2)
MergeSortDesc(data2)
fmt.Println("降序排序后:", data2)
}
运行结果:
升序排序前: [38 27 43 3 9 82 10]
升序排序后: [3 9 10 27 38 43 82]
合并: [1 3 5 7] 和 [2 4 6 8 10]
结果: [1 2 3 4 5 6 7 8 10]
降序排序前: [5 2 8 1 9 3]
降序排序后: [9 8 5 3 2 1]
八、知识要点总结
| 要点 | 说明 |
|---|---|
| 分治三步 | 分(对半切)→ 治(递归排序)→ 合(有序合并) |
| 时间复杂度 | 始终 O(n log n),不受数据分布影响 |
| 空间复杂度 | O(n)(辅助数组不可省略),这是归并排序的主要代价 |
| 稳定排序 | 合并时 <= 判断保证相等元素左半优先 |
| 递归 vs 迭代 | 递归版直觉清晰,迭代版省栈空间、适合外部排序 |
| 实际速度 | 常数因子大于快排,但最坏情况保障优于快排 |
| 外部排序 | 归并思想延伸到磁盘层面,处理超大数据 |

浙公网安备 33010602011771号