Skip to content

归并排序

归并排序的概述与基本原理

归并排序(Merge Sort)是一种基于分治策略的排序算法,它通过将数组递归地分为两半,分别排序后合并成最终的有序数组。工作原理:首先将数组分成两部分,递归地对每个部分进行排序,然后使用归并操作将两个有序子数组合并成一个更大的有序数组。这种方法保证了排序时间与数组长度 N 的 log N 成正比,即时间复杂度为 O(N log N)。

  • 优点: 归并排序在处理大型数据集时表现优异,时间复杂度稳定,适合需要稳定排序的场景。
  • 缺点: 主要缺点是需要额外的辅助数组,空间复杂度为 O(N),这可能在内存受限的场景下成为瓶颈。
  • 适用场景: 特别适合外部排序或链表排序,因为它可以高效地处理数据流。

归并操作的抽象方法

文本提供了原地归并的抽象方法,方法签名为 merge(a, lo, mid, hi),用于将子数组 a[lo..mid] 和 a[mid+1..hi] 合并成有序的 a[lo..hi]。实现过程如下:

  1. 将需要合并的元素复制到辅助数组 aux[] 中。
  2. 使用两个指针分别遍历左右子数组,从中选择较小的元素放回原数组 a[],直到一个子数组用尽。
  3. 如果左半边用尽,取右半边的元素;如果右半边用尽,取左半边的元素;否则比较两个当前元素,取较小的。

以下是代码片段的简化描述:

private static void merge(Comparable[] a, int lo, int mid, int hi) {  
    int m = lo, n = mid + 1;  
    for (int i = lo; i <= hi; i++) {  
        aux[i] = a[i];  
    }  
    for (int i = lo; i <= hi; i++) {  
        if (m > mid) a[i] = aux[n++];  
        else if (n > hi) a[i] = aux[m++];  
        else if (less(a[m], a[n])) a[i] = aux[m++];  
        else a[i] = aux[n++];  
    }  
}
  • 首先复制:for (int k = lo; k <= hi; k++) aux[k] = a[k];
  • 然后合并:通过条件判断(如 i > mid 或 less(aux[j], aux[i]))决定取左半边还是右半边的元素。

这种方法虽然使用了辅助数组,但确保了合并过程的正确性。文本还提到,真正的原地归并(不使用额外空间)实现复杂,与使用额外空间的方法相比,代码难度更高。

自顶向下的归并排序

自顶向下的归并排序是递归实现的典型例子,代码如下:

private static void sort(Comparable[] a, int lo, int hi) {
if (hi <= lo) return;
int mid = lo +(hi - lo) / 2;
sort(a, lo, mid);
sort(a, mid + 1, hi);
merge(a, lo, mid, hi);
}

  • 工作原理: 从整个数组开始,递归地将数组分为两半,直到子数组长度为 1(已排序),然后逐层合并。
  • 性能分析: 自顶向下的归并排序需要 1/2 N log N 至 N log N 次比较,最多需要 6 N log N 次数组访问。

自底向上的归并排序

自底向上的归并排序是迭代实现的,代码如下: `

public static void sortBu(Comparable[] a) {  
    int N = a.length;  
    aux = new Comparable[N];  
    for (int sz = 1; sz < N; sz = sz + sz) {  
        for (int lo = 0; lo < N - sz; lo += sz + sz) {  
            merge(a, lo, lo + sz - 1, Math.min(lo + sz + sz - 1, N - 1));  
        }  
    }  
}
  • 工作原理: 从子数组大小为 1 开始,逐步合并成大小为 2、4、8 等,直到整个数组排序完成。
  • 特点: 文本提到,这种方法适合链表数据,因为只需重新组织链接,无需创建新节点。
  • 性能: 其比较次数和数组访问次数与自顶向下类似,在 N 为 2 的幂时,两者表现一致。

优化策略

  1. 使用插入排序处理小规模子数组: 对于长度小于 15 的子数组,使用插入排序可以减少递归开销,文本称可缩短运行时间 10%-15%。
  2. 测试数组是否已排序: 如果 a[mid] <= a[mid+1],可跳过合并,适用于部分已排序的数组,使运行时间变为线性。
  3. 避免复制到辅助数组: 通过在递归层次交换输入数组和辅助数组的角色,减少复制开销,但空间复杂度不变。

这些优化在实际应用中可能显著提升性能,尤其在处理特定数据分布时。

复杂性分析与理论基础

文本深入探讨了排序算法的复杂性,特别强调基于比较的排序算法的理论下限:

  • 命题 I: 任何基于比较的排序算法在最坏情况下至少需要 log(N!) ≈ N log N 次比较。这是通过二叉树模型证明的:排序过程对应一棵比较树,叶子节点必须覆盖 N! 种排列,树高至少为 log(N!)。
  • 命题 J: 归并排序的比较次数为 O(N log N),与下限匹配,证明其为渐进最优的基于比较排序算法。

  • 上界(如命题 G)为软件工程师提供性能保证;

  • 下界(如命题 I)避免无谓的优化尝试,节省资源。

实际应用与局限性

尽管归并排序理论上最优,但仍有局限性:

  • 空间复杂度 O(N) 可能不适合内存受限场景;
  • 实际应用中不一定总是最坏情况,性能可能受其他因素影响(如数组访问开销);
  • 非基于比较的排序(如计数排序)在特定数据下可能更高效。

表格:归并排序的性能对比

特性 自顶向下 自底向上 优化后
时间复杂度 O(N log N) O(N log N) O(N log N)
空间复杂度 O(N) O(N) O(N)
适合场景 一般数组排序 链表排序 小数组优化后更高效
比较次数范围 1/2 N log N ~ N log N 1/2 N log N ~ N log N 可减少 10%-15%