# 排序 - 归并排序 (Merge Sort)

# 归并排序介绍

根据具体的实现,归并排序包括 "从上往下" 和 "从下往上"2 种方式。

# 从下往上的归并排序

将待排序的数列分成若干个长度为 1 的子数列,然后将这些数列两两合并;得到若干个长度为 2 的有序数列,再将这些数列两两合并;得到若干个长度为 4 的有序数列,再将它们两两合并;直接合并成一个数列为止。这样就得到了我们想要的排序结果。

# 从上往下的归并排序

它与 "从下往上" 在排序上是反方向的。它基本包括 3 步:

  • 分解 – 将当前区间一分为二,即求分裂点 mid = (low + high)/2;
  • 求解 – 递归地对两个子区间 a [low…mid] 和 a [mid+1…high] 进行归并排序。递归的终结条件是子区间长度为 1。
  • 合并 – 将已排序的两个子区间 a [low…mid] 和 a [mid+1…high] 归并为一个有序的区间 a [low…high]。

image-20230126224017610.png

# 归并排序实现

# 从上往下的归并排序

从上往下的归并排序采用了递归的方式实现。它的原理非常简单,如下图:image-20230126224105662.png

通过 "从上往下的归并排序" 来对数组 {90,80,70,10,20,50,60,40} 进行排序时:

  • 将数组 {90,80,70,10,20,50,60,40} 看作由两个有序的子数组 {90,80,70,10} 和 {20,50,60,40} 组成。对两个有序子树组进行排序即可。
    • 将子数组 {90,80,70,10} 看作由两个有序的子数组 {90,80} 和 {70,10} 组成。
    • 将子数组 {20,50,60,40} 看作由两个有序的子数组 {20,50} 和 {60,40} 组成。
      • 将子数组 {90,80} 看作由两个有序的子数组 {90} 和 {80} 组成。
      • 将子数组 {70,10} 看作由两个有序的子数组 {70} 和 {10} 组成。
      • 将子数组 {20,50} 看作由两个有序的子数组 {20} 和 {50} 组成。
      • 将子数组 {60,40} 看作由两个有序的子数组 {60} 和 {40} 组成。

# 从下往上的归并排序

从下往上的归并排序的思想正好与 "从下往上的归并排序" 相反。如下图:

image-20230126224540323.png

通过 "从下往上的归并排序" 来对数组 {90,80,70,10,20,50,60,40} 进行排序时:

  • 将数组 {90,80,70,10,20,50,60,40} 看作由 8 个有序的子数组 {90},{80},{70},{10},{20},{50},{60} 和 {40} 组成。
  • 将这 8 个有序的子数列两两合并。得到 4 个有序的子树列 {80,90},{10,70},{20,50} 和 {40,60}。
  • 将这 4 个有序的子数列两两合并。得到 2 个有序的子树列 {10,70,80,90} 和 {20,40,50,60}。
  • 将这 2 个有序的子数列两两合并。得到 1 个有序的子树列 {90,80,70,10,20,50,60,40}。

# 排序原理:

1. 尽可能的一组数据拆分成两个元素相等的子组,并对每一个子组继续拆分,直到拆分后的每个子组的元素个数是 1 为止。

2. 将相邻的两个子组进行合并成一个有序的大组;

3. 不断的重复步骤 2,直到最终只有一个组为止。

# 归并排序 API 设计

类名 mergeSort
构造方法 Merge ():创建 Merge 对象
成员方法 1.public static void sort (int [] a):对数组内的元素进行排序 2.private static void sort (int [] a, int lo, int hi):对数组 a 中从索引 lo 到索引 hi 之间的元素进 3.private static void merge (int [] a, int lo, int mid, int hi): 从索引 lo 到所以 mid 为一个子组,从索引 mid+1 到索引 hi 为另一个子组,把数组 a 中的这两个子组的数据合并成一个有序的大组(从从索引 lo 到索引 hi)4.private static boolean less (int v,int w): 判断 v 是否小于 w
成员变量 1.private static int [] assist:完成归并操作需要的辅助数组

image-20230126225716589.png

image-20230126225744548.png

image-20230126225805681.png

# 归并排序的时间复杂度和稳定性

# 归并排序时间复杂度

归并排序的时间复杂度是 O (N*lgN)。

假设被排序的数列中有 N 个数。遍历一趟的时间复杂度是 O (N),需要遍历多少次呢?归并排序的形式就是一棵二叉树,它需要遍历的次数就是二叉树的深度,而根据完全二叉树的可以得出它的时间复杂度是 O (N*lgN)。

# 归并排序稳定性

归并排序是稳定的算法,它满足稳定算法的定义。

算法稳定性 – 假设在数列中存在 a [i]=a [j],若在排序之前,a [i] 在 a [j] 前面;并且排序之后,a [i] 仍然在 a [j] 前面。则这个排序算法是稳定的!

代码实现:

package com.w.winterVacation.sort;
/**
 * 归并排序
 *
 * @author wspstart
 * @create 2023-01-26 17:05
 */
public class MergeSort {
    // 归并所需要的辅助数组
    private static int[] assist;
    /*
        比较 v 是否小于 w
     */
    private static boolean less(int v, int w) {
        return v <= w;
    }
    /*
    对数组中 arr 的元素进行排序
     */
    public static void sort(int[] arr) {
        // 1、初始化辅助数组 assist;
        assist = new int[arr.length];
        // 2、定义一个 left 变量和 right 变量,分别记录数组中最小的索引和最大的索引
        int left = 0;
        int right = arr.length - 1;
        // 3、调用 sort 重载方法完成数组 arr 中,从索引 left 到索引 right 的元素的排序
        sort(arr, left, right);
    }
    /*
    对数组 arr 中从 left 到 right 中的元素排序
     */
    private static void sort(int[] arr, int left, int right) {
        // 对传入的数据进行安全性校验
        if (right <= left) {
            return;
        }
        // 对数据进行分组
        int mid = left + ((right - left) >> 1);
        // 对每一组数据进行排序
        sort(arr, left, mid);
        sort(arr, mid + 1, right);
        merge(arr, left, right, mid);
    }
    /**
     * 归并,归并的过程中需要对数据进行排序
     *
     * @param arr   排序的原始数组
     * @param left  左边有序序列的初始索引
     * @param right 右边索引
     * @param mid   中间索引
     */
    private static void merge(int[] arr, int left, int right, int mid) {
        // 定义三个指针
        int i = left; // 表示 assist 数组中已有的有效数据的索引
        int p1 = left; // 左子组的第一个位置
        int p2 = mid + 1; // 右子组的第一个位置
        // 遍历,移动 p1 指针和 p2 指针,比较对应的索引处的值,找出小的那个,放到辅助数组的对应索引处
        while (p1 <= mid && p2 <= right) {
            // 比较对应索引处的值
            if (less(arr[p1], arr[p2])) {
                assist[i++] = arr[p1++];
            } else {
                assist[i++] = arr[p2++];
            }
        }
        // 出了第一个循环之后,两个子组中的某一个子组中的元素已经遍历完成了。
        // 遍历,如果 p2 的指针没有走完,那么顺序移动 p2 指针,把对应的元素放到辅助数组的对应索引处 (下面两个循环只会走一个)
        while (p1 <= mid) {
            assist[i++] = arr[p1++];
        }
        while (p2 <= right) {
            assist[i++] = arr[p2++];
        }
        // 把辅助数组中的元素拷贝到原数组中
        for (int index = left; index <= right; index++) {
            arr[index] = assist[index];
        }
    }
}