常考排序算法之归并排序

Posted 飞人01_01

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了常考排序算法之归并排序相关的知识,希望对你有一定的参考价值。

前几天,我发布了一篇快速排序的帖子,今天我们还是围绕着排序这个话题,继续讲解第二热度的归并排序。归并的递归版本非常的简单,非递归版本的可能稍微要难一点点,但是,不要怕,这都是小问题。我们直接开干!!!

前期文章

八大排序算法总结

深入理解快速排序以及优化方式

一、递归版本

归并排序的思想:将给定的数据,等分为两部分,分别递归调用函数,使这两部分有序。然后再将这左右两部分的数据进行合并,使其有序。这就是归并。

如上图,递归调用绿色部分,使其有序后,再调用蓝色部分,使其有序后;再将二者进行合并,且还是有序的合并。

所以,在传入进来的数组,首先就是进行mid的计算,然后调用左部分,再调用右部分。最后调用merge方法进行合并。

//伪代码
public void mergeSort(int[] array, int start, int end) {
    int mid = start + (end - start) / 2; //计算中间值
    mergeSort(array, start, mid); //左部分
    mergeSort(array, mid + 1, end); //右部分
    调用merge方法进行合并
}

大致框架知道后,现在就是merge方法的实现了,假设我们需要将给定的数据都排升序。则有如下思路:

  • 首先开辟一个数组,大小为end - start + 1,用于临时存储已经有序的数据。

  • 然后开始拿取左部分和右部分的数据,进行比较。

    • 假设左部分的值小,左部分的值就先放入临时数组,然后取出下一个数值
    • 假设右部分的值小,右部分的值就先放入临时数组,然后取出下一个数值
  • 直到左部分遍历结束,或者右部分遍历结束,循环停止

上面3点过后,循环停止下来,肯定是因为左部分的数据遍历完了,或者是右部分的数据遍历完了。此时肯定有一部分的值还没有放入临时数组,所以还需将没放入临时数组的所有数据,都放入进去。

最后将临时数组的所有数据,都放回array原数组即可。

public void merge(int[] array, int left, int mid, int right) {
    int[] tmp = new int[right - left + 1];
    int p1 = left; //指向左部分数组的下标
    int p2 = mid + 1; //指向右部分数组的下标
    int index = 0; //指向临时数组的下标
    while (p1 <= mid && p2 <= right) {
        if (array[p1] <= array[p2]) {
           tmp[index] = array[p1]; 
            p1++;
        } else {
            tmp[index] = array[p2];
            p2++;
        }
        index++; //下标后移
    }
    
    while (p1 <= mid) { //左部分的数据还没遍历完
        tmp[index] = array[p1];
        index++;
        p1++;
    }
    while (p2 <= right) { //右部分的数据还没遍历完
        tmp[index] = array[p2];
        index++;
        p2++;
    }
    
    //将临时数组的所有数据,放回array数组
    for (int i = left; i <= right; i++) {
        array[i] = tmp[i];
    }
}

以上的代码,就是merge方法,还算是比较简单的,就是谁的数值更小,谁就先放入临时数组即可。

主方法代码:

public void mergeSort(int[] array, int start, int end) {
    if (start >= end) return;
    
    int mid = start + (end - start) / 2;
    mergeSort(array, start, mid);
    mergeSort(array, mid + 1, end);
    merge(array, start, mid, end);//调用合并方法
}

二、非递归版本

递归版本的代码,是从顶至下进行拆分,比如:待排序的数组的长度是10个,我们可以拆分为5个数据一组,使其有序;而这5个数据,我们还可以拆分为2个数据和3个数据为……,一直往下拆分。

而非递归版本的代码,是从底向上。也就是说,第一次,我们拆分为左部分一个数据,右部分一个数据,然后合并;然后再以下一组,左部分一个数据,右部分一个数据,再合并……如下图:

如上图,我们先让每一个小组进行有序后,再调用下一趟,使组的数据量扩大一倍即可。

public void mergeSortNoRecur(int[] array) {
    int N = array.length;
    int leftPart = 1; //左部分的元素个数
    while (leftPart < N) {
        int start = 0; //左边界值
        while (start < N) {
            int mid = start + leftPart - 1; //中间值
            int end = mid + leftPart; //右边界值
            if (mid > N) break; //中间值,超过数组长度,直接跳出

            int newEnd = Math.min(end, N); //新右边界值,与N取最小值。作为这一组的右边界
            merge(array, start, mid, newEnd); //调用merge方法

            start = end + 1; //进入下一组
        }
        leftPart *= 2; //扩大每一个的数据量
    }
}

以上代码,就是非递归版本的代码,最外层的while循环是躺数,里面的while循环是用于合并这一趟中各个小组的数据。

归并的时间复杂度是O(N logN),空间复杂度是O(N)。

时间复杂度的推导:每次将数组进行二分,实则拆分下来就是一颗二叉树,高度就会logN,再对每个节点进行合并,即就是O(N logN)。空间复杂度的推导:每个合并的时候,都会开辟一个数组,而则这个数组的长度最大的时候,就是递归版本代码中,最后一次的合并操作,也就是第一次拆分为左右部分的时候。即就是O(N)。

好啦,本期更新就到此结束啦!我们下期见!!!

以上是关于常考排序算法之归并排序的主要内容,如果未能解决你的问题,请参考以下文章

4-2 Python面试常考算法

排序之归并排序的算法思想及实现代码

排序算法之归并排序

重学数据结构和算法之归并排序快速排序

排序算法之归并排序(Java)

排序算法之归并排序(Java)