排序算法:归并排序

前言

这篇文章本该发表于2018年4月份末,在 排序算法:快速排序 之后,但是不知道什么原因,这篇文章忘了在CSDN上发表,今天在看博客的时候突然发现,因此补上。

 

概述

之前介绍过排序算法中效率很高的“快速排序”。本文将介绍另一个高效的排序算法——“归并排序”。

 

基本思想

归并排序的主要思想是分治法。主要过程是:

  1. 将n个元素从中间切开,分成两部分。(左边可能比右边多1个数)

  2. 将步骤1分成的两部分,再分别进行递归分解。直到所有部分的元素个数都为1。

  3. 从最底层开始逐步合并两个排好序的数列。

 

思考

考虑一个问题,如何将两个有序数列合并成一个有序数列?

很简单,由于两个数列都已经有序,我们只需从两个数列的低位轮番拿出各自最小的数来PK就就行了,输的一方为小值,将这个值放入临时数列,然后输的一方继续拿出一个值来PK,直至有一方没有元素后,将另一方的所有元素依次接在临时数列后面即可。此时,临时数列为两个数列的有序合并。归并排序中的归并就是利用这种思想。对应的代码如下:

/**
 * 合并两个有序数列
 * array[first]~array[mid]为第一组
 * array[mid+1]~array[last]为第二组
 * temp[]为存放两组比较结果的临时数组
 */
private static void mergeArray(int array[], int first, int mid, int last, int temp[]) {
    int i = first, j = mid + 1; // i为第一组的起点, j为第二组的起点
    int m = mid, n = last; // m为第一组的终点, n为第二组的终点
    int k = 0; // k用于指向temp数组当前放到哪个位置
    while (i <= m && j <= n) { // 将两个有序序列循环比较, 填入数组temp
        if (array[i] <= array[j])
            temp[k++] = array[i++];
        else
            temp[k++] = array[j++];
    }
    while (i <= m) { // 如果比较完毕, 第一组还有数剩下, 则全部填入temp
        temp[k++] = array[i++];
    }
    while (j <= n) {// 如果比较完毕, 第二组还有数剩下, 则全部填入temp
        temp[k++] = array[j++];
    }
    for (i = 0; i < k; i++) {// 将排好序的数填回到array数组的对应位置
        array[first + i] = temp[i];
    }
}

 

例子

下面通过一个例子来看看归并排序是怎么工作的,原数组如下。

第一步:分解

首先将数组分解成两部分,即19、15、37为一组,12、25为一组,为了区分,我们起个名字叫“第一层”,如下图:

第二步:分解

继续分解,19、15为一组,37为一组,12为一组,25为一组,这四组为“第二层”,如下图:

第三步:分解

继续分解,此时只剩下19、15这一组可以分解,分解成19、15,这两组为“第三层”,如下图:

第四步:归并

由于所有组都已经分解成只有1个元素,开始进行归并,从“高层”开始归并,即先归并“第三层”,比较“第三层”两组元素,19 < 15,因此将15排在19前面,由于已经没有元素,结束此次归并,如下图:

第五步:归并

继续归并,此次归并“第二层”,这一层有4个组,进行两两比较。首先,比较15、19和37:15 < 37,所以15放第一个位置,接着比较19和37,19 < 37,所以19放第二个位置,此时第一组15、19已经没有元素,于是将37填入15和19之后。接着比较:12和25:12 < 25,所以12放第一个位置,由于第一组12已经没有元素,于是将25填入12之后。归并的结果如下:

第六步:归并

继续归并,此次归并“第一层”,这一组有2个组,第一组:15、19、37,第二组:12、25。同样的,取两组的第1个数比较:15 > 12,所以12放第1个位置;接着取第二组的第2个数比较:15 < 25,所以15放第2个位置;接着取第一组的第2个数比较:19 < 25,所以19放第3个位置;接着取第一组的第3个数比较:37 > 25,所以25放第4个位置;由于第二组已经没有元素,所以37自然归入第5个位置。此时,归并结束,最终数组如下。

整个例子的完整过程图如下:

 

完整过程

用一张动图来展示整个归并排序的过程。图中,开始每个柱子的颜色都不同,代表所有数字被分解成了各自一组,红色的柱子代表归并后的数列。

 

整合

合并的代码在开头已经给过了,分解的代码较简单,就是递归调用归并方法,最后调用合并方法,最终代码如下。

public class MergeSort {
    public static void mergeSort(int[] array) {
        if (array == null || array.length == 0)
            return;
        int[] temp = new int[array.length];
        mergeSort(array, 0, array.length - 1, temp);

    }
    // 归并
    private static void mergeSort(int array[], int first, int last, int temp[]) {
        if (first < last) {
            int mid = (first + last) / 2;
            mergeSort(array, first, mid, temp); // 递归归并左边元素
            mergeSort(array, mid + 1, last, temp); // 递归归并右边元素
            mergeArray(array, first, mid, last, temp); // 再将二个有序数列合并
        }
    }

    /**
     * 合并两个有序数列
     * array[first]~array[mid]为第一组
     * array[mid+1]~array[last]为第二组
     * temp[]为存放两组比较结果的临时数组
     */
    private static void mergeArray(int array[], int first, int mid, int last, int temp[]) {
        int i = first, j = mid + 1; // i为第一组的起点, j为第二组的起点
        int m = mid, n = last; // m为第一组的终点, n为第二组的终点
        int k = 0; // k用于指向temp数组当前放到哪个位置
        while (i <= m && j <= n) { // 将两个有序序列循环比较, 填入数组temp
            if (array[i] <= array[j])
                temp[k++] = array[i++];
            else
                temp[k++] = array[j++];
        }
        while (i <= m) { // 如果比较完毕, 第一组还有数剩下, 则全部填入temp
            temp[k++] = array[i++];
        }
        while (j <= n) {// 如果比较完毕, 第二组还有数剩下, 则全部填入temp
            temp[k++] = array[j++];
        }
        for (i = 0; i < k; i++) {// 将排好序的数填回到array数组的对应位置
            array[first + i] = temp[i];
        }
    }
}

 

 

时间复杂度

归并排序的时间复杂度为O(nlogn),推导过程较复杂,在此不多赘述。

 

使用场景

从代码中可以看出,归并排序需要一个跟待排序数组同等空间的临时数组,因此,使用归并排序时需要考虑是否有空间上的限制。如果没有空间上的限制,归并排序是一个不错的选择。在本人的电脑测试,100万的随机数字,归并排序大约耗时150毫秒。

 

相关文章

排序算法:快速排序

排序算法:选择排序

排序算法:插入排序

排序算法:冒泡排序

 

程序员囧辉 CSDN认证博客专家 Spring MySQL 分布式
当你的才华还撑不起你的野心的时候,你就应该静下心来学习,愿你在我这里能有所收获。公众号:程序员囧辉,分享个人在Java学习路上的一些学习、思考、经验和总结。
©️2020 CSDN 皮肤主题: 数字20 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值