分治策略下的归并排序:数据结构中的经典排序方法
归并排序:分治思想的精彩展现
基本思想
归并排序(Merge Sort)是分治法(Divide and Conquer)的典型应用,由计算机科学领域的先驱约翰·冯·诺依曼在1945年提出。它的核心思路是:把一个大的问题拆分成多个小问题,解决完小问题后再将结果合并起来。
算法的流程包含两个关键阶段:
- 分解(Divide) :通过递归的方式将当前的数组分割成两个子数组,直到每个子数组里仅仅包含一个元素(此时子数组天然就是有序的)
 - 治理(Conquer) :把两个已经有序的子数组合并成一个全新的有序数组
 
归并排序的核心操作是合并两个有序数组:每次比较两个数组的首元素,把较小的那个元素放到新数组中,直到所有元素都合并完成。

#include<string.h>
#include<stdio.h>
#include<stdlib.h>
void _MergeSort(int* a, int* tmp, int begin, int end)
{
    // 递归终止条件:当子数组只有一个元素时
    if (begin == end)
    {
        return;
    }
     // 计算中间点
    int mid = (begin+end)/2;
    //如果[begin,mid]和[mid+1,end]有序就可以进行归并了 这里区间不能分为[begin,mid-1][mid,end] 这是个坑 比如2 3   6 7
    _MergeSort(a,tmp, begin,mid);//把一组分成左右两个区间
    _MergeSort(a, tmp, mid+1, end);
    //归并
    int begin1 = begin, end1 = mid ;
    int begin2 = mid+1, end2 = end;
    int i = begin;
    while (begin1 <= end1 && begin2 <= end2)//有一个结束了就把没结束的那个全部尾插到后边
    {
        if (a[begin1] < a[begin2])
        {
            tmp[i++] = a[begin1++];//让小的那个++
        }
        else
        {
            tmp[i++] = a[begin2++];
        }
    }
    //有一个结束了,就把另一个依次插入
    //两个循环只会进一个
    while (begin1 <= end1)
    {
        tmp[i++] = a[begin1++];
    }
    while (begin2 <= end2)
    {
        tmp[i++] = a[begin2++];
    }
    // 将合并结果复制回原数组
    memcpy(a + begin, tmp + begin, (end - begin + 1) * sizeof(int));//begin和end在变化不是一直从0开始 看动图
}
// 归并排序入口函数
void MergeSort(int* a, int n)
{
    // 分配临时数组空间
    int* tmp = (int*)malloc(sizeof(int) * n);
    if (tmp == NULL) {
        perror("malloc fail");
        return;
    }
    // 调用递归函数
    _MergeSort(a, tmp, 0, n - 1);
    // 释放临时数组
    free(tmp);
    tmp = NULL;
}
//测试案例
int main()
{
    int a[] = { 2,3,5,4,7,1 };
    MergeSort(a, 6);
    for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
    {
        printf("%d", a[i]);
    }
    return 0;
}
为何区间不能用[end,mid-1]与[mid,end]
解释为什么不能把区间划分成[begin,mid-1]和[mid,end]。例如有10个数据,下标从0到9,若按[begin,mid-1]和[mid,end]划分,假设mid-1=3,就会分成[0,3]和[4,9]两个区间。在[0,3]区间内,mid-1=0,又会分成[0,0]和[1,3],[0,0]很快结束,而[1,3]的mid-1=1,再分成[1,1]和[2,3],[2,3]的mid-1=1,又分成[2,1]和[1,3],这显然是不合理的情况。

| 操作 | 时间复杂度 | 说明 | 
|---|---|---|
| 分解数组 | O(log n) | 递归深度 | 
| 合并子数组 | O(n) | 每层需遍历所有元素 | 
| 总复杂度 | O(n log n) | 稳定高效排序 | 
空间复杂度
归并排序需要 O(n) 的额外空间:
- 用于存储合并过程中的临时数组
- 递归调用栈空间为 O(log n),通常临时数组占主导地位
非递归归并
可理解为11归并是两组数据中每个子数组元素个数均为1,11归并完成后进行22归并,依此类推。
void MergeSortNonR(int* a, int n)
{
    int* tmp = (int*)malloc(sizeof(int) * n);
    if (tmp == NULL)
    {
        perror("malloc fail");
        return;
    }
    //gap为每组归并数据的元素个数
    int gap = 1;
    for (int i = 0; i < n; i+=2*gap)//一整组归并含gap+gap个元素,i为每组归并起始位置
    {
        int begin1 = i, end1 = i+gap-1;
        int begin2 = gap + i, end2 = i+2*gap-1;//begin2为begin1跳过一组数据后的起始位置,end2跳过两组数据
        //第二组越界则跳过本次合并
        if (begin2 >= n)
        {
            break;
        }
        //第二组未完全越界但部分越界,修正边界继续归并
        if (end2 >= n)
        {
            end2 = n - 1;
        }
        int j=i;
        while (begin1 <= end1 && begin2 <= end2)//一数组处理完则将另一数组剩余元素放新数组后
        {
            if (a[begin1] < a[begin2])
            {
                tmp[j++] = a[begin1++];//放较小元素到临时数组
            }
            else
            {
                tmp[j++] = a[begin2++];
            }
        }
        //处理剩余元素
        while (begin1 <= end1)
        {
            tmp[j++] = a[begin1++];
        }
        while (begin2 <= end2)
        {
            tmp[j++] = a[begin2++];
        }
        memcpy(a + i, tmp + i, (end2 - i + 1) * sizeof(int));//归并结果拷贝回原数组
    }
    free(tmp);
    tmp = NULL;
}
10个数可能出现溢出情况,通过打印归并区间分析,主要有第二个子数组完全越界和部分越界两种情况。时间复杂度上,每层遍历n个元素,共logn层,故为n log n。空间复杂度为O(n),因需额外临时空间。
非递归实现要点
- gap参数:控制当前归并子数组大小,从1开始,迭代时翻倍
 - 边界处理:关键步骤,处理数组大小非2的幂情况:
- 第二组完全越界(begin2 ≥ n),跳过合并
 - 第二组部分越界(end2 ≥ n),修正边界为n-1
 
 - 迭代过程:
- 第一轮:11归并,子数组大小为1
 - 第二轮:22归并,子数组大小为2
 - 第k轮:2ᵏ归并,子数组大小为2ᵏ
 
 
归并排序特性总结
- 稳定排序:
- 元素相等时优先选左子数组元素,保持相等元素原始相对位置,适用于多关键字排序
 
 - 时间复杂度:
- 最优、平均、最差均为O(n log n),性能稳定
 
 - 空间复杂度:
- 需O(n)额外空间,递归实现有O(log n)栈空间
 
 - 外排序优势:
- 能高效处理外部存储数据,适合海量数据,流程为:分割大文件为内存块→内存排序→合并已排序块
 
 
结语
归并排序是分治思想的卓越体现,通过“分而治之”策略实现O(n log n)高效排序。其核心优势:
  1. 稳定可靠:不受输入数据影响,性能稳定
  2. 外排序能力:是处理海量磁盘数据的通用高效算法
  3. 稳定排序:保持相等元素原始顺序
  4. 并行潜力:天然适合并行化
虽需O(n)额外空间,但在现代系统中空间换时间策略可行。归并排序在数据库、大数据、外部排序等场景不可或缺,是程序员必掌握的经典算法。
