3.3 归并排序:稳定高效的分治典范

历史背景

归并排序由 John von Neumann 于 1945 年发明。冯·诺依曼是最早认识到排序在现代计算中重要性的科学家之一。归并排序是第一个被严格证明时间复杂度为 O(n log n) 的排序算法。它也是外部排序的基础——当数据太大无法全部装入内存时,归并排序是最常用的方法。Java 的 Arrays.sort() 对对象数组使用的就是 TimSort(归并排序的改进版)。

算法原理

归并排序同样采用分治策略:

  1. 分解:将数组从中间分成两个子数组
  2. 递归:对左右两个子数组分别进行归并排序
  3. 合并:将两个已排序的子数组合并为一个有序数组

合并操作是归并排序的核心:使用两个指针分别指向两个子数组的起始位置,比较指针指向的元素,将较小的元素放入结果数组,然后移动该指针。

Java 实现

java
复制代码
public class MergeSort {
    
    /**
     * 归并排序(自顶向下递归版本)
     */
    public static void sort(int[] arr) {
        if (arr.length <= 1) return;
        int[] temp = new int[arr.length];
        mergeSort(arr, temp, 0, arr.length - 1);
    }
    
    private static void mergeSort(int[] arr, int[] temp, int left, int right) {
        if (left >= right) return;
        
        int mid = left + (right - left) / 2;
        mergeSort(arr, temp, left, mid);
        mergeSort(arr, temp, mid + 1, right);
        merge(arr, temp, left, mid, right);
    }
    
    /**
     * 合并两个已排序的子数组
     */
    private static void merge(int[] arr, int[] temp, int left, int mid, int right) {
        // 将 arr[left..right] 复制到 temp[left..right]
        System.arraycopy(arr, left, temp, left, right - left + 1);
        
        int i = left;       // 左子数组指针
        int j = mid + 1;    // 右子数组指针
        int k = left;       // 合并后数组指针
        
        while (i <= mid && j <= right) {
            if (temp[i] <= temp[j]) {
                arr[k++] = temp[i++];
            } else {
                arr[k++] = temp[j++];
            }
        }
        
        // 复制剩余元素
        while (i <= mid) {
            arr[k++] = temp[i++];
        }
        while (j <= right) {
            arr[k++] = temp[j++];
        }
    }
    
    /**
     * 归并排序(自底向上迭代版本)
     * 避免递归开销,适合链表等数据结构
     */
    public static void sortIterative(int[] arr) {
        int n = arr.length;
        int[] temp = new int[n];
        
        for (int size = 1; size < n; size *= 2) {
            for (int left = 0; left < n - size; left += 2 * size) {
                int mid = left + size - 1;
                int right = Math.min(left + 2 * size - 1, n - 1);
                merge(arr, temp, left, mid, right);
            }
        }
    }
    
    /**
     * 归并排序(泛型版本)
     */
    public static <T extends Comparable<T>> void sortGeneric(T[] arr) {
        if (arr.length <= 1) return;
        @SuppressWarnings("unchecked")
        T[] temp = (T[]) new Comparable[arr.length];
        mergeSortGeneric(arr, temp, 0, arr.length - 1);
    }
    
    private static <T extends Comparable<T>> void mergeSortGeneric(T[] arr, T[] temp, int left, int right) {
        if (left >= right) return;
        
        int mid = left + (right - left) / 2;
        mergeSortGeneric(arr, temp, left, mid);
        mergeSortGeneric(arr, temp, mid + 1, right);
        mergeGeneric(arr, temp, left, mid, right);
    }
    
    private static <T extends Comparable<T>> void mergeGeneric(T[] arr, T[] temp, int left, int mid, int right) {
        System.arraycopy(arr, left, temp, left, right - left + 1);
        
        int i = left, j = mid + 1, k = left;
        
        while (i <= mid && j <= right) {
            if (temp[i].compareTo(temp[j]) <= 0) {
                arr[k++] = temp[i++];
            } else {
                arr[k++] = temp[j++];
            }
        }
        
        while (i <= mid) arr[k++] = temp[i++];
        while (j <= right) arr[k++] = temp[j++];
    }
    
    /**
     * 归并排序(原地优化版本)
     * 减少空间使用,但实现更复杂
     */
    public static void sortInPlace(int[] arr) {
        sortInPlace(arr, 0, arr.length - 1);
    }
    
    private static void sortInPlace(int[] arr, int left, int right) {
        if (left >= right) return;
        int mid = left + (right - left) / 2;
        sortInPlace(arr, left, mid);
        sortInPlace(arr, mid + 1, right);
        mergeInPlace(arr, left, mid, right);
    }
    
    private static void mergeInPlace(int[] arr, int left, int mid, int right) {
        int i = left, j = mid + 1;
        while (i <= mid && j <= right) {
            if (arr[i] <= arr[j]) {
                i++;
            } else {
                int value = arr[j];
                // 将 arr[i..j-1] 向右移动一位
                System.arraycopy(arr, i, arr, i + 1, j - i);
                arr[i] = value;
                i++;
                mid++;
                j++;
            }
        }
    }
}

效率分析

指标 最好情况 最坏情况 平均情况
时间复杂度 O(n log n) O(n log n) O(n log n)
空间复杂度 O(n) O(n) O(n)
比较次数 ≈ n log n / 2 ≈ n log n ≈ n log n
移动次数 ≈ n log n ≈ n log n ≈ n log n

归并排序是稳定的排序算法。它的时间复杂度在所有情况下都是 O(n log n),性能非常稳定。代价是需要 O(n) 的额外空间。归并排序特别适合链表排序,因为链表可以在 O(1) 时间内完成合并,且不需要额外空间。

应用场景

  • 需要稳定排序的场景
  • 链表排序
  • 外部排序(数据量超过内存容量)
  • 并行排序(归并排序天然适合并行化)
  • Java Arrays.sort() 对对象数组的默认算法(TimSort 的基础)
  • 多路归并