算法|归并排序

前面讲过的几种排序算法,选择排序,冒泡排序,插入排序,希尔排序,这些排序可以归为一类(算法实现请移步这里).

因为他们是逐个元素依次比较和交换,其中插入排序对于有序元素有一定的优势,希尔排序是制造比较优势.这些排序算法都是内外循环的方式,一直到数据结束.

而归并排序使用递归的方式,将数组分成无数个小的子数组,对子数组进行排序,然后依次将他们归并起来,从而达到对整个数组排序的目的.

将两个有序的数组归并起来,从而组成更大的有序数组.从这个概念衍生出来的递归排序思想:归并排序.要将一个数组排序,可以递归的将这个数组的分成两半,直道他们在最小单位内变成有序时(通常最小单位为1),将数组合并起来.

总体的流程可以用下面的图来理解 地址:


  1. 先将左半边数组进行排序,
  2. 再将右半边元素进行排序,
  3. 最后将左边和右边的有序数组进行合并结果

看上面的图,我们很容易的想到,我们需要三个数组,左半边有序数组,右半边有序数组,然后将左右两半数组合并到一个更大的有序数组中去,但上面的图只是显示了一个抽象的流程.实际中我们需要很多次归并,所以需要更多的数组,这显然会创建大量的数组,为了避免这种情况,我们可以一次性的分配和原数组大小相等的另外一个辅助数组.在合并的时候,先把原数组拷贝到辅助数组,然后利用辅助数组进行合并. 合并的方法为 

merge(int[] a, int low, int mid, int height)

通过merge 方法,有效的将a[low]...a[mid]之间的元素和 a[mid+1]...a[height]归并成一个有序数组,并存放在a[low]...a[height]中,其中归并的过程中,会将原数组拷贝到辅助数组中,然后利用辅助数组,进行排序后,将结果归并到原数组:

private static void merge(int[] a, int low, int mid, int height) {
        int i = low;
        int j = mid + 1;


        //copy the a to auxiliary
        for (int k = low; k <= height; k++) {
            auxiliary[k] = a[k];
        }

        //merge left and right
        for (int k = low; k <= height; k++) {
            if (i > mid) {
                a[k] = auxiliary[j++];
            } else if (j > height) {
                a[k] = auxiliary[i++];
            } else if (a[i] >= a[j]) {
                a[k] = auxiliary[j++];
            } else {
                a[k] = auxiliary[i++];
            }
        }
    }

这个方法的简单描述为:

先将原数组元素从low...height范围拷贝到auxiliary(auxiliary是一次性分配的),然后利用下面四个规则将左右两边的数组元素进行重新排序:

如果左半边元素用尽(i>mid),则使用右半边的元素.

如果有右半边的元素用尽(j>height),则使用左半边的元素.

如果左半边的当前元素比右半边的当前元素小,则使用左半边的当前元素.

如果右半边的当前元素比坐半边的当前元素小,则使用右半边的当前元素.

而且每次选定了元素,不管是左边(i++)还是右边(j++),索引都会增加,i,j分别代表两个数组中的元素的索引,大家可以仔细想象下,排序过程中,他们的位置依次增加,向后移动查找元素的模拟场景

为了加深理解理解,看看下面这幅图:


根据上面我描述的过程,可以看到,排序过程中不断的在aus[0]...a[4]和 a[5]...a[9]之间比较元素大小,总是将a[i](左半边数组)和a[j](右半边数组)之间的较小元素重新摆放在a[k]的位置上,直到数组结束

归并排序的思路就是,如果能将两个子数组排序,就能递归的将两个子数组排序归并成一个更大的有序数组.

现在给出递归排序算法的一个实现:

    static int[] nums = new int[]{
            1, 15, 3, 78,
            34, 23, 46, 2,
            8, 34, 57, 50,
            200, 123, 66, 43,
            33, 100, 356, 21};

    static int[] auxiliary = new int[nums.length];

    public static void main(String[] args) {


        System.out.println("before Merge sort " + Arrays.toString(nums));
//        topToBottomSort(nums, 0, nums.length - 1);
        bottomToTopSort(nums);

        System.out.println("after Merge sort " + Arrays.toString(nums));
    }

    public static void topToBottomSort(int[] nums, int low, int height) {
        if (low >= height) return;

        int mid = low + (height - low) / 2;

        topToBottomSort(nums, low, mid);// sort left
        topToBottomSort(nums, mid + 1, height);//sort right

        merge(nums, low, mid, height); // merge left and right
    }

看方法名称topToBottomSort,叫自顶向下排序,从方法实现上来看,排序时总是先将数组分成更小的数组排序,总是将a[low]....a[height]分成 a[low]....a[mid]和 a[mid+1].....a[height],递归地将他们进行排序,最后通过merge将他们归并成最终的结果

归并的过程为:



上面两幅图是归并排序的轨迹图,可以看出,

要将a[0]...a[15]排序,先要对a[0]...a[7]排序,

要对a[0]....a[7]排序,要先对a[0]...a[3]排序,

要对a[0]...a[3]排序,要先对a[0]...a[1]排序,

要对a[0]...a[1]排序,要先对a[0]和a[1]进行排序,这个时候,a[0],a[1]已经不能再分隔,作为单个元素来看,他们已经长度为1的有序数组,可以直接进行合并

a[0]和a[1]归并后,紧接着就是a[2]和a[3]合并,然后是a[4]...a[7]的排序,同理也是一样的排序流程,将他们递归的分割到只有一个元素的数组时,就可以开始归并排序,和上面的流程是一样的,依次类推,最后归并到a[0]...a[15]

这种递归排序方式化整为零,递归的将排序行为分成更小数组的排序行为,最后通过很多极小的数组的排序结果归并成更大数组,从而实现对整个数组的排序.这是"分治思想"在排序算法上的经典应用.我们将要解决的问题分成许多个小问题的解,然后将小问题的所有解组合起来,最终就构成这个问题的解.

还有另外一个排序思路,就是与上面的过程完全反过来,叫自底向上排序.

就是先向这个数组分成很多个极小的数组(长度为1),然后先两两合并,再四四合并,载八八合并.....直道数组结束.

排序的实现为:

    public static void bottomToTopSort(int[] nums) {
        final int N = nums.length;
        for (int size = 1; size < N; size = size + size) {//the size is length of sub array
            for (int low = 0; low < N - size; low += size + size) {
                merge(nums, low, low + size - 1, Math.min(low + size + size - 1, N - 1));
            }
        }
    }

    private static void merge(int[] a, int low, int mid, int height) {
        int i = low;
        int j = mid + 1;


        //copy the a to auxiliary
        for (int k = low; k <= height; k++) {
            auxiliary[k] = a[k];
        }

        //merge left and right
        for (int k = low; k <= height; k++) {
            if (i > mid) {
                a[k] = auxiliary[j++];
            } else if (j > height) {
                a[k] = auxiliary[i++];
            } else if (a[i] >= a[j]) {
                a[k] = auxiliary[j++];
            } else {
                a[k] = auxiliary[i++];
            }
        }
    }

用下面这幅图来加深排序过程的理解:


先分成长度为1的子数组,两两合并,然后将size 加倍,也就是四四合并,然后再加倍,八八合并,直到数组结束!

前面我们做过实际的 算法|算法性能实测(选择|插入|希尔) ,其中希尔排序是性能最好的,排序 100w随机数据的时间为193ms.

现在将数组规模再扩大16倍时(千万级别),16777216 (1<<24) 个时,排序的时间为: 4663ms,

而归并排序算法在这个数据规模上的排序时间为:1425ms,又快了2倍不止.

下一篇会接着介绍,快速排序,与归并排序的排序思想类似,也是"分治思想"的经典应用之一.

快速排序的排序性能某些时候比归并排序还要快,非常值得学习!

算法实现请移步这里

下一篇:快速排序




阅读更多
上一篇算法|算法性能实测(选择|插入|希尔)
下一篇算法|快速排序
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭