动画:一篇文章快速学会归并排序

内容介绍

归并排序简介

咱们之前读书时,学校会举行运动会,运动会上有不少比赛项目,好比跳远比赛。当参加跳远比赛人数比较多时,一般会将全部参赛选手分红多组,每组的同窗比赛,并按成绩进行排名,最后将全部组的学生成绩汇总获得全部学生的排名。java

上面案例中的全部学生跳远排名就是归并排序的思想。归并排序是一个典型的基于分治的算法。归并一词的中文含义就是合并、并入的意思,归并排序分红两个步骤:1.拆分,2.合并。算法

归并排序的思想

归并排序思想,将原数据序列分红大小相等的两个子序列,继续划分子序列,直到子序列有序时,将划分的有序子序列合并成大的有序序列,最终合并成一个有序序列。 编程

归并排序动画演示

归并排序分析

通常没有特殊要求排序算法都是升序排序,小的在前,大的在后。 数组由{7, 3, 1, 9, 5, 2, 8, 6} 这8个无序元素组成。数组

归并排序分红两个步骤:1.拆分,2.合并。微信

  1. 拆分 当咱们要排序由{7, 3, 1, 9, 5, 2, 8, 6}这样一个数组的时候,归并排序法首先将这个数组分红两半。 而后想办法对左边的数组进行排序,右边的数组进行排序,而后再将它们归并起来。很显然,如今左边和右边两个数组依然是无序的,接着再拆分,将左边的数组分红两半,将右边的数组分红两半,效果以下:

如今被拆分的每一个子数组长度是2,每一个子数组依然是无序的,接着拆分,效果以下:性能

拆分后的效果如上图,通过此次拆分后,每一个子数组的长度被为1,咱们认为长度为1的子数组是有序的。不须要再进行拆分了。到此拆分就结束了。优化

  1. 合并。 到如今每一个子数组长度为1,是有序的子数组,须要将两个长度为1有序的子数组合并成一个长度为2的有序数组。

动画效果以下: 动画

到如今每一个子数组长度为2,是有序的子数组,须要将两个长度为2有序的子数组合并成一个长度为4的有序数组。code

动画效果以下: blog

到如今每一个子数组长度为4,是有序的子数组,须要将两个长度为4有序的子数组合并成一个长度为8的有序数组。

动画效果以下:

归并排序合并时的细节

归并排序再进行将有序子数组合并成大一点有序数组时,须要对比左右两个子数组哪一个元素小取哪一个元素。

  1. 左边数组的元素大于右边数组的元素,取右边的小元素。
  2. 右边数组的元素大于等于左边数组的元素,取左边的小元素。

以上两种状况是常见状况,还有两种特殊状况须要注意:

  1. 左边的所有是小值提早取完了,剩下右边的直接赋值
  2. 右边的所有是小值提早取完了,剩下左边的直接赋值

归并排序代码编写

代码以下:

public class MergeSortTest2 {
    public static void main(String[] args) {
        int[] arr = new int[] {7, 3, 1, 9, 5, 2, 8, 6};
        mergeSort(arr);
    }

    // 归并排序的方法
    public static void mergeSort(int[] arr) {
        // 使用一个额外的数组,容量和要排序的数组容量同样大
        int[] aux = new int[arr.length];
        mergePass(arr, 0, arr.length - 1, aux);
        // mergePass2(arr, 0, arr.length - 1, aux);
    }

    // 对arr数组的[low, hight]索引元素进行归并排序
    private static void mergePass(int[] arr, int low, int high, int[] aux) {
        // 递归的终止条件,当拆分后,小索引和大索引是相同的,也就是一个元素的时候就终止拆分
        if (low >= high)
            return;

        // 将一个数组拆分为两个数组
        int mid = (low + high) / 2;
        mergePass(arr, low, mid, aux);
        mergePass(arr, mid + 1, high, aux);
        // 对拆分后的这两个数组进行排序
        merge(arr, low, mid, high, aux);
    }

    // 将arr[low, mid]和arr[mid+1, high]两部分进行归并
    private static void merge(int[] arr, int low, int mid, int high, int[] aux) {
        // 临时数组的内容就是两个待排序数组的内容,排好序的内容会放到arr中
        for (int k = low; k <= high; k++) {
            aux[k] = arr[k];
        }

        // i是左边数组的索引
        // j是右边数组的索引
        int i = low;
        int j = mid + 1;

        // 遍历左边和右边的小数组合并到一个大数组中
        for (int k = low; k <= high; k++) {
            if (i > mid) {
                // 左边的所有是小值提早取完了,剩下右边的直接赋值
                arr[k] = aux[j];
                j++;
            } else if (j > high) {
                // 右边的所有是小值提早取完了,剩下左边的直接赋值
                arr[k] = aux[i];
                i++;
            } else if (aux[i] > aux[j]) {
                // 左边数组的元素大于右边数组的元素,取右边的小元素
                arr[k] = aux[j];
                j++;
            } else { // aux[left] <= aux[right]
                // 右边数组的元素大于等于左边数组的元素,取左边的小元素
                arr[k] = aux[i];
                i++;
            }
        }
    }
}

归并排序代码优化1

归并排序代码优化1,小数据规模时改用插入排序。

若是待排序的数据量很大,当子数组拆分的比较小时,能够改为插入排序,由于插入排序的小数据规模时效率很高,这种优化方式能够改进大多数递归排序算法的性能。

优化后代码以下:

public class MergeSortTest2 {
    public static void main(String[] args) {
        int[] arr = new int[] {7, 3, 1, 9, 5, 2, 8, 6};
        mergeSort(arr);
        System.out.println(Arrays.toString(arr));
    }

    public static void mergeSort(int[] arr) {
        // 使用一个额外的数组,容量和要排序的数组容量同样大
        int[] aux = new int[arr.length];
        mergePass2(arr, 0, arr.length - 1, aux);
    }

    // 优化: 对小规模子数组使用插入排序
    // 对arr数组的[low, hight]索引元素进行归并排序
    private static void mergePass2(int[] arr, int low, int high, int[] aux) {
        if (low >= high) return;
        // 对小规模子数组使用插入排序
        if (high - low <= 15) {
            insertionSort(arr, low, high);
        }

        // 将一个数组拆分为两个数组进行排序
        int mid = (low + high) / 2;
        mergePass2(arr, low, mid, aux);
        mergePass2(arr, mid + 1, high, aux);
        // 拆分完成后须要对这两个数组进行排序
        merge(arr, low, mid, high, aux);
    }

    // 对数组指定索引范围的元素使用插入排序
    public static void insertionSort(int[] arr, int low, int high) {
        for (int i = low + 1; i < high; i++) {
            int e = arr[i]; // 获得当前这个要插入的元素
            int j;
            for (j = i; j > 0 && arr[j-1] > e; j--) {
                arr[j] = arr[j-1];
            }
            arr[j] = e;
        }
    }

    // 将arr[low, mid]和arr[mid+1, high]两部分进行归并
    private static void merge(int[] arr, int low, int mid, int high, int[] aux) {
        // 临时数组的内容就是两个待排序数组的内容,排好序的内容会放到arr中
        for (int k = low; k <= high; k++) {
            aux[k] = arr[k];
        }

        // i是左边数组的索引
        // j是右边数组的索引
        int i = low;
        int j = mid + 1;

        // 遍历左边和右边的小数组合并到一个大数组中
        for (int k = low; k <= high; k++) {
            if (i > mid) {
                // 左边的所有是小值提早取完了,剩下右边的直接赋值
                arr[k] = aux[j];
                j++;
            } else if (j > high) {
                // 右边的所有是小值提早取完了,剩下左边的直接赋值
                arr[k] = aux[i];
                i++;
            } else if (aux[i] > aux[j]) {
                // 左边数组的元素大于右边数组的元素,取右边的小元素
                arr[k] = aux[j];
                j++;
            } else { // aux[left] <= aux[right]
                // 右边数组的元素大于等于左边数组的元素,取左边的小元素
                arr[k] = aux[i];
                i++;
            }
        }
    }
}

归并排序代码优化2

归并排序使用递归进行排序,在代码编写和阅读上比较清晰,容易理解,可是当待排数据量很大时,递归会形成时间和空间上的性能损耗,而且可能会形成栈溢出。咱们排序追求的就是效率,能够将递归转化成迭代,也就是自底向上归并排序。从而改善归并排序的性能。

自底向上归并排序能够分为两个过程:

  1. 合并排序:从子序列长度为1开始,进行两两合并排序,获得2倍长度的有序大序列。
  2. 循环:子序列长度从1开始,循环让子序列长度是原先的两倍,不断进行合并排序。

自底向上归并排序动画效果以下:

自底向上归并排序代码以下:

public class BottomUpMergeSortTest2 {
    public static void main(String[] args) {
        int[] arr = new int[] {7, 3, 1, 9, 5, 2, 8, 6};
        mergeSortBottomUp(arr);
        System.out.println(Arrays.toString(arr));
    }

    // 不使用递归,自底向上的归并排序
    public static void mergeSortBottomUp(int[] arr) {
        // 使用一个额外的数组,容量和要排序的数组容量同样大
        int[] aux = new int[arr.length];

        // 在for循环中对数组进行拆分
        // size为子数组的长度
        for (int size = 1; size < arr.length; size += size) { // size = 1, 2, 4
            for (int low = 0; low < arr.length -size; low += size+size) {
                // 优化: 若是左边子数组的最后一个元素大于右边子数组的最小值说明须要排序
                if (arr[low+size-1] > arr[low+size]) {
                    merge(arr, low, low + size -1, Math.min(low + size + size - 1, arr.length - 1), aux);
                }
            }
        }
    }

    // 将arr[low, mid]和arr[mid+1, high]两部分进行归并
    private static void merge(int[] arr, int low, int mid, int high, int[] aux) {
        // 临时数组的内容就是两个待排序数组的内容,排好序的内容会放到arr中
        for (int k = low; k <= high; k++) {
            aux[k] = arr[k];
        }

        // i是左边数组的索引
        // j是右边数组的索引
        int i = low;
        int j = mid + 1;

        // 遍历左边和右边的小数组合并到一个大数组中
        for (int k = low; k <= high; k++) {
            if (i > mid) {
                // 左边的所有是小值提早取完了,剩下右边的直接赋值
                arr[k] = aux[j];
                j++;
            } else if (j > high) {
                // 右边的所有是小值提早取完了,剩下左边的直接赋值
                arr[k] = aux[i];
                i++;
            } else if (aux[i] > aux[j]) {
                // 左边数组的元素大于右边数组的元素,取右边的小元素
                arr[k] = aux[j];
                j++;
            } else { // aux[left] <= aux[right]
                // 右边数组的元素大于等于左边数组的元素,取左边的小元素
                arr[k] = aux[i];
                i++;
            }
        }
    }
}

自底向上归并排序,避免了递归时深度为log2n的栈空间,额外使用了aux数组和原数组同样大小的空间,所以空间复杂度为0(n),而且避免了递归在时间性能上有必定的提高,因此使用归并排序时,优先考虑非递归方法。

归并排序复杂度

一张图看懂归并排序时间复杂度,以下图:

归并排序会将数据规模为n的数据拆分红

每次比较n次,所以总的时间复杂度为

归并排序空间复杂度复杂度:由于归并排序过程当中须要使用一个和原始数组相同大小的辅助数组,因此归并排序的空间复杂度为O(n)。

总结

  1. 归并排序思想,将原数据序列分红大小相等的两个子序列,继续划分子序列,直到子序列有序时,将划分的有序子序列合并成大的有序序列,最终合并成一个有序序列。归并排序分红两个步骤:1.拆分,2.合并。
  2. 归并排序代码优化1,小数据规模时改用插入排序。
  3. 自底向上归并排序,避免递归时间和空间上的额外消耗。

原创文章和动画制做真心不易,您的点赞就是最大的支持! 想了解更多文章请关注微信公众号:表哥动画学编程

相关文章
相关标签/搜索