归并排序(Merge Sort)

标签

稳定排序、非原地排序、比较排序

基本思想

归并排序属于比较类非线性时间排序,号称比较类排序中性能最佳,在数据中应用中较广。

归并排序是分治策略的一个典型的应用。所谓分治策略(Divide and Conquer),即将问题(divide)成一些小的问题以期递归求解,而治(conquer)的阶段则将分的阶段得到的各答案“融合”在一起,即分而治之。落实到归并排序中,就是将整个序列先递归划分为子序列(子序列起始长度为1),然后将相邻的子序列两两合并,合并的过程即是有序化的过程。如此重复,子序列间不断合并得到更长的有序子序列,直到整个序列合并完成,则此时序列完全有序。

拆分阶段

拆分可以用递归实现,也可以用迭代。

递归的话,自上而下。先拆分,大事化小的思想。目的是将长度为$n$的待排序序列:$a_0a_1\dots a_{n - 1}$,先一分为二,拆成$a_0a_1\dots a_{\frac{n - 1}{2} - 1}$和$a_{\frac{n - 1}{2}}\dots a_{n - 1}$两个子序列,再分别将这两个子序列继续一分为二,如此直到子序列长度为1,即完成拆分,进入合并阶段。

而迭代的话,自下而上。直接从最小子序列(长度为1)开始有序化合并,逐步往上,直至整个序列合并完成有序化。

递归 vs. 迭代

这两种归并算法虽然实现方式不同,但也有共同之处
1. 无论是基于递归还是迭代的归并排序, 它们调用的核心方法Merge都是相同的:完成一次合并。即两个已经有序的数组序列合并成一个更大的有序数组序列;
2. 趋势一致,合并序列的长度都是从小(一个元素)到大(整个序列)增长的。
 

合并阶段

显然,合并阶段需要申请额外的临时存储空间(数组)

如上图所示,每次合并的过程即是对两个已有序子序列进行合并再排序的过程。

通常我们使用两个游标分别指示两个子序列当前待排序元素的位置,另有一个游标指示临时数组的当前元素存储位置。三个游标的初始位置均在头部。实际操作时,每次对两个子序列中游标指示位置的元素进行比较,将较小者(升序排列)存入临时数组中(游标指示位置),存储元素对应的两个游标均后移一位。如此操作,直至子序列之一的游标到达末部,则将另一个子序列中剩余元素直接依次全部存入临时数组中,即可。

如此,临时数组中存储的即是两个子序列合并后的有序序列,再将其复制目标数组中,一次合并排序就完成了。

算法描述

  • 步骤1:把长度为$n$的输入序列分成两个长度为$n/2$的子序列;
  • 步骤2:对这两个子序列分别采用归并排序;
  • 步骤3:将两个排序好的子序列合并成一个最终的排序序列。

动图演示

时间复杂度

归并排序一共需要进行$O(\log n)$层归并操作,每层的归并操作的总时间复杂度是$O(n)$,因此总体的时间复杂度为$O(n\log n)$。

最坏,最好和平均时间复杂度都是$Θ(n\log n)$。

空间复杂度

和其他排序有所不同,为了实现归并操作,每次合并都需要开辟额外的空间来临时保存合并后的排序结果,总共需要开辟$n$个元素的空间,所以归并排序的空间复杂度为$O(n)$

算法示例(C)

 根据实现思路不同,总的来说,归并排序的代码实现也分为递归和迭代两类。递归式自顶向下,迭代是自底向上。

下面先讲讲两类方法的共同部分,也是核心调用——有序化合并函数Merge

Merge函数

背景

Merge的前提是两个子序列已有序。

Merge函数的输入为一个待排序数组a[low...high],则它实际上由两个已有序的子序列:a[low...mid]和a[mid+1...high]组成。为方便描述,我们分别称之为a1和a2,则目标是a1,a2的合并有序化。

实现步骤

1. 申请一个和原数组a长度相同的拷贝数组aux,并将原数组a中的所有元素依次拷贝进数组aux中,即aux[low...high]和a[low...high]一模一样;
2. 与原数组中的a1和a2相对应,我们从aux1和aux2的头部元素开始,比较两个子序列的元素大小。较小的元素放入原数组a中(若a[0]已被占则放在a[1]...依次类推),并取得较小元素所在子序列的下一个元素, 继续同另一个子序列中之前那个较大的元素比较。因为前提是aux1和aux2都是有序的,所以通过这种方法我们能得到更长的有序序列;
3. 如果aux的两个子序列的其中之一的所有元素都已比较完了, 则可直接将另一个子序列中剩下的元素,全部放入原数组a的剩余位置。
 
步骤2和3的实现方法
  • 设置两个游标用于元素比较(在aux中进行):变量 i 和 j ,分别代表左游标和右游标,开始时分别指向aux[low]和aux[mid + 1];
  • 设置游标变量 k 用于指示在a中放置元素的位置(在a中进行),k在开始时候指向a[low];
  • 总体上来说 i, j, k 的趋势都是从左向右移动的;
步骤2和3的图示解说
结合上面的步骤2,比较 i 和 j 当前指示的aux中的元素的大小,取得其中比较大的那个元素(例如上图中的 i ),将其放入数组a中,此时 i++ ,左游标右移。同时 k++ , k 游标也向右移动。
结合上面的步骤4,在 i 和 j 都向右移动的过程中。如上图这种情况,因为 j 当前指示的元素(图中位置)大于左半边即a[low...mid]的所有元素,导致 i 不断增加(右移)且越过了边界(mid), 所以这时候就不需要比较了,只要把 j 当前指示位置到high的元素都搬到原数组中,填满原数组中剩下的位置,单次合并就完成了,在这一段过程中 j 连续加1, j 游标连续右移。同时 k 也连续加1, k 游标也连续右移,直到 j == high && k == high 为止。
 
基于上面的表述, 总结出单次合并算法Merge中最关键的4个条件判断情形:
  1. 左半边用尽(取右半边的元素)
  2. 右半边用尽(取左半边的元素)
  3. 左半边元素小于等于右半边(取左半边的元素)
  4. 右半边元素大于左半边(取右半边的元素)

代码实现(C)

void my_merge(int *a, int low, int mid, int high) {
    for (int p = low; p <= high; p++)
        aux[p] = a[p]; //aux为全局变量
    int i = low, j = mid + 1, k = low;
    while (i <= mid && j <= high) {
        if (aux[i] <= aux[j]) {
            a[k++] = aux[i++];
        } else {
            a[k++] = aux[j++];
        }
    }
    while (i <= mid) {
        a[k++] = aux[i++];
    }
    while (j <= high) {
        a[k++] = aux[j++];
    }
    return;
}

递归方法(自顶向下)

图解Merge函数调用

归并排序中,递归方法形成了一系列有层次、有先后调用顺序的Merge,  如下图左边的写入编号的Merge函数列表(这里是按照字母排序,A最小,Z最大):
 

从上到下,是各个Merge的先后调用顺序。1最先调用,15最后调用。

从右到左, 递归栈由深到浅。例如1, 2, 4, 5的递归深度是相同的, 而3比它们浅一个层次。
merge_sort_up_to_down(a, low, mid); //1
merge_sort_up_to_down(a, mid + 1, high); //2
my_merge(a, low, mid, high); //3

首先,在第一层递归的时候,先进入的是第一行的merge_sort方法,然后紧接着又进入了第二层递归的第一行merge_sort方法, 如此继续,由(a, low,mid)的参数列表可知其递归的趋势是一直向左移动的,直到最后一层递归,所以最先执行第一行merge_sort的对象是a[0]和a[1](上图编号1),然后执行的是最后一层递归的第二行代码,这时候merge_sort的对象是a[2]和a[3](上图编号2),再然后执行第三行,对已经有序的a[0]、a[1]和a[2]、a[3]进行my_merge(上图编号3)。之后返回上一层执行第二行,同样的递归向下再返回,再执行第三行,再往上返回一层。如此,递归的深度不断变浅, 直到最后对整个数组的左右两半进行my_merge,完成最终的有序化合并。

完整代码(C)

#include <stdio.h>
#include <stdlib.h>

int *aux;

void my_merge(int *a, int low, int mid, int high) {
    for (int p = low; p <= high; p++)
        aux[p] = a[p];
    int i = low, j = mid + 1, k = low;
    while (i <= mid && j <= high) {
        if (aux[i] <= aux[j]) {
                a[k++] = aux[i++];
        } else {
            a[k++] = aux[j++];
        }
    }
    while (i <= mid) {
        a[k++] = aux[i++];
    }
    while (j <= high) {
        a[k++] = aux[j++];
    }
    return;
}

void merge_sort_up_to_down(int *a, int low, int high) {
    if (low >= high) return;
    int mid = low + (high - low) / 2;
    merge_sort_up_to_down(a, low, mid);
    merge_sort_up_to_down(a, mid + 1, high);
    my_merge(a, low, mid, high);
    return;
}

void merge_sort(int *a, int len) {
    aux = (int *)malloc(sizeof(int) * len);
    merge_sort_up_to_down(a, 0, len - 1);
    free(aux);
    return ;
}

int main() {
    /* 归并排序(升序)*/
    int num[10] = {5, 1, 8, 4, 7, 2, 3, 9, 0, 6};
    int length = sizeof(num) / sizeof(num[0]);
    merge_sort(num, length);
    for (int i = 0; i < length; i++) 
        printf("%d ", num[i]);
    return 0;
}

递归的三种优化思路

优化一. 小规模数组使用插入排序

用不同的方法处理小规模问题能改进大多数递归算法的性能,因为递归会使小规模问题中方法调用太过频繁,所以改进对它们的处理方法就能改进整个算法。 因为插入排序非常简单, 因此一般来说在小数组上比归并排序更快。 这种优化能使归并排序的运行时间缩短10%到15%;

快速排序的递归优化思路与之类似,对于小规模数组,在归并和快速排序算法中都可以替换成插入排序,以提升效率。

具体做法就是把递归停止的判断语句 if (low >= high) return; 改成

if (low + M>=high) { // 数组长度小于M的时候
    insert_sort(a, low, high); // 切换到插入排序
    return;
}

优化二:Merge函数可跳过情况判断

Merge函数的功能是对a[low...high]中的a[low...mid]和a[mid...high]进行有序化合并。因为a[low...mid]和a[mid...high]本来就是有序的,而如果判断出a[mid]<=a[mid+1]的话, 那么直接就可以确保a[low]<a[low+1]...<a[mid]<=a[mid+1]<a[mid+2]...< a[high]已经有序,从而可跳过接下来的Merge过程。
void merge_sort_up_to_down(int *a, int low, int high) {
    // 终止递归的条件
    if (low >= high) return;
    int mid = low + (high - low) / 2;
    merge_sort_up_to_down(a, low, mid);
    merge_sort_up_to_down(a, mid + 1, high);
    if(a[mid] <= a[mid+1]) return; // 避免不必要的合并调用
    merge(a, low, mid, high);  // 单次合并
  }

优化三:去除原数组序列到辅助数组的拷贝

每次调用Merge方法时候,我们都把a对应的序列拷贝到辅助数组aux中来:

for (int p = low; p <= high; p++)
    aux[p] = a[p];
 
实际上,这个拷贝的过程可以去掉。实现方式是,在递归调用的每个层次都进行输入数组和输出数组的角色交换,从而确保输入数组的有序性(即子序列分别有序)
void merge_sort_up_to_down(int *a, int *aux, int low, int high) {
    if (low >= high) return;
    int mid = low + (high - low) / 2;
    merge_sort_up_to_down(aux, a, low, mid); //每层进行角色交换
    merge_sort_up_to_down(aux, a, mid + 1, high);
    my_merge(a, aux, low, mid, high);
    return;
}

void merge_sort(int *a, int len) {
    int *aux = (int *)malloc(sizeof(int) * len);
    for (int p = 0; p < len; p++) //只拷贝递归前的这一次
        aux[p] = a[p];
    merge_sort_up_to_down(a, aux, 0, len - 1);
    free(aux);
    return ;
}

【注意:因为最外部的 merge_sort 和 my_merge 的参数顺序是相同的, 所以,无论递归过程中拷贝数组和原数组的角色如何替换,对最后一次调用的my_merge而言(将整个数组左右半边合为有序的操作),最终被排为有序的一定是原数组,而不是拷贝数组!】 

完整优化代码(C)

#include <stdio.h>
#include <stdlib.h>

int *aux;

void my_merge(int *a, int *aux, int low, int mid, int high) {
    int i = low, j = mid + 1, k = low;
    while (i <= mid && j <= high) {
        if (aux[i] <= aux[j]) {
            a[k++] = aux[i++];
        } else {
            a[k++] = aux[j++];
        }
    }
    while (i <= mid) {
        a[k++] = aux[i++];
    }
    while (j <= high) {
        a[k++] = aux[j++];
    }
    return;
}

void merge_sort_up_to_down(int *a, int *aux, int low, int high) {
    if (low + M>=high) { // 优化一:数组长度小于M的时候
        insert_sort(a, low, high); // 切换到插入排序
        return;
    }
    int mid = low + (high - low) / 2;
    merge_sort_up_to_down(aux, a, low, mid); //优化三:去掉递归内拷贝
    merge_sort_up_to_down(aux, a, mid + 1, high);
    if(a[mid] <= a[mid+1]) return; // 优化二:避免不必要的合并调用
    my_merge(a, aux, low, mid, high);  // 单次合并
    return;
}

void merge_sort(int *a, int len) {
    aux = (int *)malloc(sizeof(int) * len);
    for (int p = 0; p < len; p++)
        aux[p] = a[p];
    merge_sort_up_to_down(a, aux, 0, len - 1);
    free(aux);
    return ;
}


int main() {
    /* 归并排序(升序) */
    int num[10] = {5, 1, 8, 4, 7, 2, 3, 9, 0, 6};
    int length = sizeof(num) / sizeof(num[0]);
    merge_sort(num, length);
    for (int i = 0; i < length; i++)
        printf("%d ", num[i]);
    return 0;
}
归并排序(递归优化)

迭代循环(自底向上)

思路比较简单:

完整代码(C)

#include <stdio.h>
#include <stdlib.h>
#include <algorithm>

int *aux;

void my_merge(int *a, int low, int mid, int high) {
    int i = low, j = mid + 1, k = low;
    for (int p = low; p <= high; p++)
        aux[p] = a[p];
    while (i <= mid && j <= high) {
        if (aux[i] <= aux[j]) {
            a[k++] = aux[i++];
        } else {
            a[k++] = aux[j++];
        }
    }
    while (i <= mid) {
        a[k++] = aux[i++];
    }
    while (j <= high) {
        a[k++] = aux[j++];
    }
    return;
}

void merge_sort_down_to_up(int *a, int length) {
    aux = (int *)malloc(sizeof(int) * length);
    for (int size =1; size < length; size = size+size){
        for(int low = 0; low< length - size; low += size+size) {
            my_merge(a, low, low + size - 1, std::min(low + size + size - 1, length - 1)); //最后一个子序列可能不够一个size
        }
    }
    return;
}

int main() {
    /* 归并排序(升序) */
    int num[10] = {5, 1, 8, 4, 7, 2, 3, 9, 0, 6};
    int length = sizeof(num) / sizeof(num[0]);
    merge_sort_down_to_up(num, length);
    for (int i = 0; i < length; i++)
        printf("%d ", num[i]);
    return 0;
}

(整理自网络)

参考资料:

https://www.cnblogs.com/chengxiao/p/6194356.html

posted @ 2020-12-08 17:53  箐茗  阅读(569)  评论(0编辑  收藏  举报