归并排序

47 阅读4分钟

在掌握了快速排序之后,学习同样基于分治法思想,但思路迥异且非常稳定的归并排序

特性归并排序快速排序
核心思想先分后治:递归地将数组对半拆分,再将有序子数组合并。分而治之:选取基准进行分区,递归排序左右区间。
时间复杂度最好、最坏、平均情况均为 O(n log n)平均 O(n log n),最坏 O(n²)
空间复杂度O(n) ,需要额外的空间来合并数组平均 O(log n)
稳定性稳定,相等元素的相对位置在排序后不变不稳定,分区过程可能打乱相等元素的顺序
效率影响性能稳定,不受输入数据特性影响受输入数据影响,最坏情况可能退化为O(n²)

🔄 归并排序的核心思想

归并排序严格遵循分治法,其过程可以清晰地分为三个步骤 :

  1. 分解:将当前需要排序的数组,从中间位置递归地拆分成两个子数组,直到每个子数组只包含一个元素(一个元素本身就是有序的)。
  2. 解决:递归地对这两个子数组进行归并排序。
  3. 合并:这是归并排序的核心步骤。将两个已经排好序的子数组合并成一个新的有序数组。

归并排序.gif

🧩 算法步骤与示例

我们通过数组 [8, 3, 5, 4, 7, 1, 9, 2]来理解归并排序的过程 :

1. 分解阶段:不断将数组对半拆分,直到每个子数组只剩一个元素。

初始数组: [8, 3, 5, 4, 7, 1, 9, 2]
第一次拆分: [8, 3, 5, 4][7, 1, 9, 2]
第二次拆分: [8, 3], [5, 4], [7, 1], [9, 2]
最终拆分: [8], [3], [5], [4], [7], [1], [9], [2] (每个子数组都已自然有序)

2. 合并阶段:从最小的子数组开始,两两合并成有序的新数组。

  • 合并 [8][3]得到 [3, 8]
  • 合并 [5][4]得到 [4, 5]
  • 合并 [3, 8][4, 5]得到 [3, 4, 5, 8]
  • 同理,合并右侧数组得到 [1, 2, 7, 9]
  • 最后,合并 [3, 4, 5, 8][1, 2, 7, 9]得到最终的有序数组 [1, 2, 3, 4, 5, 7, 8, 9]

合并两个有序子数组的过程是归并排序的关键。它通过双指针遍历两个子数组,比较指针所指元素,将较小的元素放入临时数组,直到某个子数组遍历完,再将另一个子数组的剩余部分直接追加到临时数组末尾 。

💻 Java代码实现

以下是归并排序的Java实现,包含了递归排序和合并两个有序数组的核心方法。

public class MergeSort {

    // 归并排序的入口方法
    public static void mergeSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        int[] temp = new int[arr.length]; // 创建临时数组,避免在递归中频繁创建
        mergeSort(arr, 0, arr.length - 1, temp);
    }

    // 递归排序的主方法
    private static void mergeSort(int[] arr, int left, int right, int[] temp) {
        if (left < right) {
            int mid = left + (right - left) / 2; // 计算中点,防止整数溢出
            // 递归排序左半部分
            mergeSort(arr, left, mid, temp);
            // 递归排序右半部分
            mergeSort(arr, mid + 1, right, temp);
            // 合并两个有序部分
            merge(arr, left, mid, right, temp);
        }
    }

    // 合并两个有序子数组 arr[left...mid] 和 arr[mid+1...right]
    private static void merge(int[] arr, int left, int mid, int right, int[] temp) {
        int i = left;    // 左子数组的起始索引
        int j = mid + 1; // 右子数组的起始索引
        int t = 0;       // 临时数组的当前索引

        // 比较两个子数组的元素,将较小的放入temp
        while (i <= mid && j <= right) {
            if (arr[i] <= arr[j]) {
                temp[t++] = arr[i++];
            } else {
                temp[t++] = arr[j++];
            }
        }

        // 将左子数组剩余元素拷贝到temp
        while (i <= mid) {
            temp[t++] = arr[i++];
        }

        // 将右子数组剩余元素拷贝到temp
        while (j <= right) {
            temp[t++] = arr[j++];
        }

        // 将temp数组中合并好的数据拷贝回原数组arr
        t = 0;
        while (left <= right) {
            arr[left++] = temp[t++];
        }
    }

    public static void main(String[] args) {
        int[] arr = {8, 3, 5, 4, 7, 1, 9, 2};
        System.out.println("排序前: " + java.util.Arrays.toString(arr));
        mergeSort(arr);
        System.out.println("排序后: " + java.util.Arrays.toString(arr));
    }
}

⚖️ 归并排序的优缺点与应用场景

优点

  • 稳定的时间复杂度:在任何情况下都能保持O(n log n)的高效率,性能可预测。
  • 稳定性:是稳定的排序算法,适用于需要保持相等元素初始顺序的场景。
  • 适合大数据量:特别适合处理无法一次性装入内存的大规模数据(外部排序)。

缺点

  • 空间复杂度高:需要O(n)的额外空间,在内存受限的环境下可能成为问题。
  • 递归开销:递归实现可能导致栈溢出,尽管可以采用迭代方式实现以避免。

应用场景

  • 当需要稳定的排序且对空间要求不苛刻时。
  • 用于链表排序时,归并排序只需要修改指针,空间复杂度可降为O(1)。
  • 外部排序,例如对大文件进行排序。

希望这份详细的归并排序讲解能帮助你更好地理解这个经典算法!理解了归并排序和快速排序的异同,你对分治法的应用会有更深刻的认识。