排序算法-归并排序

算法排序-归并排序

简介

归并排序(Merge Sort)是建立在归并操作上的一种有效的排序算法,由约翰·冯·诺伊曼(John von Neumann)在1945年发明。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各种语言或库中都有实现。

归并排序是一种稳定的排序方法,其时间复杂度始终为O(n log n),不受输入数据的影响,这使得它在需要稳定性能的场景中非常有用。

算法原理

归并排序的基本思想是:

  1. 分解(Divide): 将待排序的数组分成两个子数组,直到每个子数组只有一个元素
  2. 解决(Conquer): 递归地对子数组进行排序
  3. 合并(Combine): 将两个已排序的子数组合并成一个有序数组

这个过程可以用递归来实现,递归的终止条件是数组长度为1。

Go语言实现

基础版本

go 复制代码
package main

import "fmt"

// MergeSort 归并排序主函数
func MergeSort(arr []int) []int {
    // 递归终止条件
    if len(arr) <= 1 {
        return arr
    }
    
    // 分解:找到中点,分割数组
    mid := len(arr) / 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++
        }
    }
    
    // 将剩余元素加入结果数组
    for i < len(left) {
        result = append(result, left[i])
        i++
    }
    
    for j < len(right) {
        result = append(result, right[j])
        j++
    }
    
    return result
}

func main() {
    arr := []int{64, 34, 25, 12, 22, 11, 90}
    fmt.Println("排序前:", arr)
    
    sorted := MergeSort(arr)
    fmt.Println("排序后:", sorted)
}

原地归并排序版本

go 复制代码
package main

import "fmt"

// MergeSortInPlace 原地归并排序
func MergeSortInPlace(arr []int, left, right int) {
    if left < right {
        mid := left + (right-left)/2
        
        // 递归排序左半部分
        MergeSortInPlace(arr, left, mid)
        // 递归排序右半部分
        MergeSortInPlace(arr, mid+1, right)
        
        // 合并两个有序部分
        mergeInPlace(arr, left, mid, right)
    }
}

// mergeInPlace 原地合并两个有序数组
func mergeInPlace(arr []int, left, mid, right int) {
    // 创建临时数组存储左半部分
    leftArr := make([]int, mid-left+1)
    copy(leftArr, arr[left:mid+1])
    
    // 创建临时数组存储右半部分
    rightArr := make([]int, right-mid)
    copy(rightArr, arr[mid+1:right+1])
    
    // 合并过程
    i, j, k := 0, 0, left
    
    for i < len(leftArr) && j < len(rightArr) {
        if leftArr[i] <= rightArr[j] {
            arr[k] = leftArr[i]
            i++
        } else {
            arr[k] = rightArr[j]
            j++
        }
        k++
    }
    
    // 复制剩余元素
    for i < len(leftArr) {
        arr[k] = leftArr[i]
        i++
        k++
    }
    
    for j < len(rightArr) {
        arr[k] = rightArr[j]
        j++
        k++
    }
}

// MergeSortInPlaceWrapper 原地归并排序包装函数
func MergeSortInPlaceWrapper(arr []int) {
    if len(arr) > 1 {
        MergeSortInPlace(arr, 0, len(arr)-1)
    }
}

带步骤显示的版本

go 复制代码
package main

import "fmt"

var step int

// MergeSortWithSteps 带步骤显示的归并排序
func MergeSortWithSteps(arr []int, depth int) []int {
    step++
    indent := ""
    for i := 0; i < depth; i++ {
        indent += "  "
    }
    
    fmt.Printf("%s步骤 %d: 处理数组 %v (长度: %d)\n", 
               indent, step, arr, len(arr))
    
    if len(arr) <= 1 {
        fmt.Printf("%s数组长度 <= 1,直接返回\n", indent)
        return arr
    }
    
    mid := len(arr) / 2
    fmt.Printf("%s分割点: %d,分为 %v 和 %v\n", 
               indent, mid, arr[:mid], arr[mid:])
    
    fmt.Printf("%s递归排序左半部分:\n", indent)
    left := MergeSortWithSteps(arr[:mid], depth+1)
    
    fmt.Printf("%s递归排序右半部分:\n", indent)
    right := MergeSortWithSteps(arr[mid:], depth+1)
    
    fmt.Printf("%s合并 %v 和 %v\n", indent, left, right)
    result := mergeWithSteps(left, right, depth)
    fmt.Printf("%s合并结果: %v\n", indent, result)
    
    return result
}

// mergeWithSteps 带步骤显示的合并函数
func mergeWithSteps(left, right []int, depth int) []int {
    result := make([]int, 0, len(left)+len(right))
    i, j := 0, 0
    
    indent := ""
    for k := 0; k <= depth; k++ {
        indent += "  "
    }
    
    fmt.Printf("%s开始合并过程:\n", indent)
    
    for i < len(left) && j < len(right) {
        if left[i] <= right[j] {
            fmt.Printf("%s  选择 %d (来自左数组)\n", indent, left[i])
            result = append(result, left[i])
            i++
        } else {
            fmt.Printf("%s  选择 %d (来自右数组)\n", indent, right[j])
            result = append(result, right[j])
            j++
        }
    }
    
    // 添加剩余元素
    for i < len(left) {
        fmt.Printf("%s  添加剩余元素 %d (来自左数组)\n", indent, left[i])
        result = append(result, left[i])
        i++
    }
    
    for j < len(right) {
        fmt.Printf("%s  添加剩余元素 %d (来自右数组)\n", indent, right[j])
        result = append(result, right[j])
        j++
    }
    
    return result
}

func main() {
    // 测试基础版本
    arr1 := []int{64, 34, 25, 12, 22, 11, 90}
    fmt.Println("=== 基础归并排序 ===")
    fmt.Println("排序前:", arr1)
    sorted1 := MergeSort(arr1)
    fmt.Println("排序后:", sorted1)
    
    // 测试原地版本
    arr2 := []int{5, 2, 8, 1, 9, 3}
    fmt.Println("\n=== 原地归并排序 ===")
    fmt.Println("排序前:", arr2)
    MergeSortInPlaceWrapper(arr2)
    fmt.Println("排序后:", arr2)
    
    // 测试带步骤显示的版本
    arr3 := []int{5, 2, 8, 1}
    fmt.Println("\n=== 带步骤显示的归并排序 ===")
    fmt.Printf("初始数组: %v\n", arr3)
    step = 0
    result := MergeSortWithSteps(arr3, 0)
    fmt.Printf("最终结果: %v\n", result)
}

复杂度分析

时间复杂度

  • 所有情况: O(n log n) - 无论输入数据如何,时间复杂度都是稳定的

空间复杂度

  • 基础版本: O(n) - 需要额外的数组空间存储临时结果
  • 原地版本: O(n) - 仍需要临时数组进行合并操作

算法特点

优点

  1. 稳定性: 相等元素的相对位置不会改变
  2. 时间复杂度稳定: 始终为O(n log n),不受输入数据影响
  3. 可预测性: 性能稳定,适合对时间要求严格的场景
  4. 适合外部排序: 可以处理大于内存的数据集
  5. 并行化友好: 容易实现并行版本

缺点

  1. 空间复杂度高: 需要O(n)的额外空间
  2. 不是原地排序: 需要额外的存储空间
  3. 对小数组效率不高: 递归开销相对较大

优化策略

1. 小数组优化

go 复制代码
// 对小数组使用插入排序
func MergeSortOptimized(arr []int) []int {
    if len(arr) <= 10 { // 小数组阈值
        return insertionSort(arr)
    }
    
    mid := len(arr) / 2
    left := MergeSortOptimized(arr[:mid])
    right := MergeSortOptimized(arr[mid:])
    
    return merge(left, right)
}

func insertionSort(arr []int) []int {
    result := make([]int, len(arr))
    copy(result, arr)
    
    for i := 1; i < len(result); i++ {
        key := result[i]
        j := i - 1
        
        for j >= 0 && result[j] > key {
            result[j+1] = result[j]
            j--
        }
        result[j+1] = key
    }
    
    return result
}

2. 自底向上的归并排序

go 复制代码
// BottomUpMergeSort 自底向上的归并排序
func BottomUpMergeSort(arr []int) {
    n := len(arr)
    
    // 子数组大小从1开始,每次翻倍
    for size := 1; size < n; size *= 2 {
        // 对每个大小为size的子数组进行合并
        for left := 0; left < n-size; left += 2 * size {
            mid := left + size - 1
            right := min(left+2*size-1, n-1)
            
            mergeInPlace(arr, left, mid, right)
        }
    }
}

func min(a, b int) int {
    if a < b {
        return a
    }
    return b
}

实际应用场景

归并排序广泛应用于:

  1. 外部排序: 处理大于内存的数据文件
  2. 稳定排序需求: 需要保持相等元素相对位置的场景
  3. 链表排序: 归并排序特别适合链表数据结构
  4. 并行计算: 容易实现并行版本
  5. 数据库系统: 大数据集的排序操作
  6. 分布式系统: MapReduce等框架中的排序阶段

性能测试

go 复制代码
package main

import (
    "fmt"
    "math/rand"
    "time"
)

// 性能测试函数
func benchmarkMergeSort(size int) {
    // 生成随机数组
    arr := make([]int, size)
    for i := 0; i < size; i++ {
        arr[i] = rand.Intn(10000)
    }
    
    start := time.Now()
    MergeSort(arr)
    duration := time.Since(start)
    
    fmt.Printf("数组大小: %d, 排序时间: %v\n", size, duration)
}

// 比较不同输入情况下的性能
func comparePerformance() {
    size := 10000
    
    // 随机数组
    randomArr := make([]int, size)
    for i := 0; i < size; i++ {
        randomArr[i] = rand.Intn(10000)
    }
    
    // 已排序数组
    sortedArr := make([]int, size)
    for i := 0; i < size; i++ {
        sortedArr[i] = i
    }
    
    // 逆序数组
    reverseArr := make([]int, size)
    for i := 0; i < size; i++ {
        reverseArr[i] = size - i
    }
    
    // 测试随机数组
    start := time.Now()
    MergeSort(randomArr)
    fmt.Printf("随机数组排序时间: %v\n", time.Since(start))
    
    // 测试已排序数组
    start = time.Now()
    MergeSort(sortedArr)
    fmt.Printf("已排序数组排序时间: %v\n", time.Since(start))
    
    // 测试逆序数组
    start = time.Now()
    MergeSort(reverseArr)
    fmt.Printf("逆序数组排序时间: %v\n", time.Since(start))
}

func main() {
    fmt.Println("=== 归并排序性能测试 ===")
    sizes := []int{1000, 10000, 100000, 1000000}
    
    for _, size := range sizes {
        benchmarkMergeSort(size)
    }
    
    fmt.Println("\n=== 不同输入情况性能比较 ===")
    comparePerformance()
}

总结

归并排序是一个非常重要的排序算法,其稳定的O(n log n)时间复杂度和稳定性使其在许多场景中都有重要应用。虽然空间复杂度较高,但其可预测的性能和良好的稳定性使其成为许多系统中的首选排序算法。

关键要点:

  • 使用分治法,递归地分解和合并数组
  • 时间复杂度稳定为O(n log n),空间复杂度为O(n)
  • 是稳定的排序算法
  • 适合外部排序和需要稳定性的场景
  • 容易实现并行版本,适合大数据处理
相关推荐
墨染点香9 分钟前
LeetCode 刷题【172. 阶乘后的零】
算法·leetcode·职场和发展
做怪小疯子12 分钟前
LeetCode 热题 100——链表——反转链表
算法·leetcode·链表
linweidong35 分钟前
4399 Go开发面试题及参考答案(下)
排序算法·http状态码·消息推送·topk·go并发·tcp握手·并发模型
做怪小疯子2 小时前
LeetCode 热题 100——矩阵——旋转图像
算法·leetcode·矩阵
努力学习的小廉2 小时前
我爱学算法之—— BFS之最短路径问题
算法·宽度优先
高山上有一只小老虎3 小时前
构造A+B
java·算法
木头左3 小时前
缺失值插补策略比较线性回归vs.相邻填充在LSTM输入层的性能差异分析
算法·线性回归·lstm
sin_hielo3 小时前
leetcode 2435
数据结构·算法·leetcode
crescent_悦4 小时前
PTA L1-020 帅到没朋友 C++
数据结构·c++·算法
鳄鱼儿4 小时前
密码算法的OID查阅
算法