AIGC标识 归并排序(Merge Sort)

归并排序(Merge Sort)

一、核心思想

归并排序是分治思想的经典范本,它的策略可以概括为三步:

  1. :把数组从中间一分为二
  2. :对两个子数组各自递归排序
  3. :把两个已排序的子数组合并为一个有序数组

整个过程就像两支各自有序的队伍合并成一支:每次从两队头部取较小的那个放到结果队中,直到两队都排完。

原始: [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)。

空间消耗来源

  1. 辅助数组:O(n),这是归并排序的主要空间开销
  2. 递归栈:O(log n),比辅助数组小得多
  3. 迭代版消除了栈开销,但辅助数组 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 倍。

归并排序的价值在于它的稳定性和最坏情况保障——当这些特性是刚需时(如数据库排序、外部排序),归并排序无可替代。

六、外部排序简介

当数据量超过内存容量时,归并排序的思想依然适用:

  1. 把大文件分成多个内存能装下的小块
  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 迭代 递归版直觉清晰,迭代版省栈空间、适合外部排序
实际速度 常数因子大于快排,但最坏情况保障优于快排
外部排序 归并思想延伸到磁盘层面,处理超大数据
posted @ 2026-07-29 09:17  FfHUCisI  阅读(12)  评论(0)    收藏  举报