归并排序:递归的艺术,效率的典范

249 阅读4分钟

归并排序(Merge Sort)是一种经典的分治算法,其基本思路可用一句话概括:将数组划分为两部分,分别排序后合并为一个有序数组。听起来简单,但背后的逻辑却精妙无比。归并排序通过递归的方式,将问题逐步拆解为最小单位,再通过合并操作将结果逐步整合。

从递归到合并:一步步拆解归并排序

递归的本质

递归看似复杂,其实就是通过不断压栈,层层拆解问题,再逐步回溯解决。归并排序正是利用递归的方式,将一个大问题化简为多个小问题。例如,当我们面对一个数组 [3, 5, 6, 0, 1, 2] 时,归并排序会这样运作:

  1. 拆分阶段:将数组拆分为两个子数组:左侧 [3, 5, 6] 和右侧 [0, 1, 2]。然后对两个子数组分别进行递归排序。
  2. 排序合并:将左侧和右侧排序后的结果合并为一个新的有序数组。

合并的核心

在合并阶段,我们会用到一个辅助数组以及两个指针,分别指向左右两个子数组的起始位置:

  • 比较两个指针所指向的元素,将较小的值放入辅助数组中,并移动相应指针。
  • 如果某一子数组的指针已经到达末尾,直接将另一个子数组剩余的元素拷贝到辅助数组。
  • 最后将辅助数组的内容复制回原数组。

[3, 5, 6][0, 1, 2] 为例,合并过程如下:

  1. 比较 30,将 0 放入辅助数组。
  2. 比较 31,将 1 放入辅助数组。
  3. 重复上述操作,直到所有元素合并完毕。

最终结果是一个完全有序的数组 [0, 1, 2, 3, 5, 6]

时间复杂度分析:为何是 O(n log n)?

归并排序的时间复杂度被证明为 O(n log n),这源于其两大特点:

  1. 分解层数:每次将数组分为两半,分解过程的层数为 log n(n 是数组长度)。
  2. 合并复杂度:每层的合并操作需要遍历整个数组,因此为 O(n)。

无论数组初始顺序如何(完全无序或接近有序),归并排序的时间复杂度始终是 O(n log n),这使其成为处理大规模数据的理想选择。


代码实现:一步步拆解归并排序

以下是归并排序在 Java 中的实现:

public class MergeSort {

    // 主排序函数
    public void mergeSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        mergeSortRecursive(arr, 0, arr.length - 1);
    }

    // 递归排序函数
    private void mergeSortRecursive(int[] arr, int left, int right) {
        if (left >= right) {
            return;
        }
        int mid = left + (right - left) / 2;  // 计算中点
        mergeSortRecursive(arr, left, mid);  // 排序左半部分
        mergeSortRecursive(arr, mid + 1, right);  // 排序右半部分
        merge(arr, left, mid, right);  // 合并两部分
    }

    // 合并函数
    private void merge(int[] arr, int left, int mid, int right) {
        int[] help = new int[right - left + 1];  // 辅助数组
        int i = 0, p1 = left, p2 = mid + 1;

        // 合并过程:将较小的元素放入辅助数组
        while (p1 <= mid && p2 <= right) {
            help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
        }

        // 将剩余部分加入辅助数组
        while (p1 <= mid) help[i++] = arr[p1++];
        while (p2 <= right) help[i++] = arr[p2++];

        // 将排序结果复制回原数组
        for (i = 0; i < help.length; i++) {
            arr[left + i] = help[i];
        }
    }

    public static void main(String[] args) {
        MergeSort sorter = new MergeSort();
        int[] arr = {3, 5, 6, 0, 1, 2};
        sorter.mergeSort(arr);
        System.out.println("Sorted Array: " + Arrays.toString(arr));
    }
}

归并排序的优劣势

优点

  1. 稳定性:归并排序保留了相同元素的相对顺序。
  2. 高效性:无论数组多么无序,其时间复杂度始终为 O(n log n)。
  3. 适用性强:适合排序大规模数组。

缺点

  1. 空间复杂度较高:需要额外的辅助数组,空间复杂度为 O(n)。
  2. 不适合小数组:小规模数据时,递归带来的开销可能不如直接使用插入排序。

结语

归并排序不仅是一个高效的排序算法,更是递归思想的绝佳范例。通过将问题分解到足够小的规模,递归的力量得以完美展现。希望这篇文章能帮助你更好地理解归并排序的原理与实现!