排序算法中的归并排序(Merge Sort)是利用"归并"技术来进行排序。归并是指将若干个已排序的子文件合并成一个有序的文件。

一、实现原理

1、算法基本思路

设两个有序的子文件(相当于输入堆)放在同一向量中相邻的位置上:R[low..m],R[m+1..high],先将它们合并到一个局部的暂存向量R1(相当于输出堆)中,待合并完成后将R1复制回R[low..high]中。

(1)合并过程

合并过程中,设置i,j和p三个指针,其初值分别指向这三个记录区的起始位置。合并时依次比较R[i]和R[j]的关键字,取关键字较小的记录复制到R1[p]中,然后将被复制记录的指针i或j加1,以及指向复制位置的指针p加1。
重复这一过程直至两个输入的子文件有一个已全部复制完毕(不妨称其为空),此时将另一非空的子文件中剩余记录依次复制到R1中即可。
最后,将结果赋值到R[]中。

(2)动态申请R1

实现时,R1是动态申请的,因为申请的空间可能很大,故须加入申请空间是否成功的处理。

二、2种实现方法

自底向上自顶向下方法。

1. 自底向上方法(虽然效率较高,但可读性较差)

(1) 自底向上的基本思想

自底向上的基本思想是:第1趟归并排序时,将待排序的文件R[1..n]看作是n个长度为1的有序子文件,将这些子文件两两归并,若n为偶数,则得到 n/2n/2 个长度为2的有序子文件;若n为奇数,则最后一个子文件轮空(不参与归并)。故本趟归并完成后,前 log(n)log(n) 个有序子文件长度为2,但最后一个子文件长度仍为1;第2趟归并则是将第1趟归并所得到的 log(n)log(n) 个有序的子文件两两归并,如此反复,直到最后得到一个长度为n的有序文件为止。
上述的每次归并操作,均是将两个有序的子文件合并成一个有序的子文件,故称其为"二路归并排序"。类似地有 k(k>2)k(k>2) 路归并排序。

(2) 一趟归并算法

分析:在某趟归并中,设各子文件长度为length(最后一个子文件的长度可能小于length),则归并前R[1..n]中共有 n/lengthn/length 个有序的子文件:R[1..length],R[length+1..2length],…

注意:调用归并操作将相邻的一对子文件进行归并时,必须对子文件的个数可能是奇数、以及最后一个子文件的长度小于length这两种特殊情况进行特殊处理:
① 若子文件个数为奇数,则最后一个子文件无须和其它子文件归并(即本趟轮空);
② 若子文件个数为偶数,则要注意最后一对子文件中后一子文件的区间上界是n。

举个栗子:对于数组 a[n] = {63, 95, 84, 46, 18, 24, 27, 31, 46},采用自底向上方法归并排序过程如下图所示。

具体实现如下:

int min(int x, int y) 
{
    return x < y ? x : y;
}
void merge_sort(int arr[], int len) 
{
    int *a = arr;
    int *b = (int *) malloc(len * sizeof(int));
    int seg, start;
    for (seg = 1; seg < len; seg += seg) {
        for (start = 0; start < len; start += seg * 2) 
        {
            int low = start, mid = min(start + seg, len), high = min(start + seg * 2, len);
            int k = low;
            int start1 = low, end1 = mid;
            int start2 = mid, end2 = high;
            while (start1 < end1 && start2 < end2)
                b[k++] = a[start1] < a[start2] ? a[start1++] : a[start2++];
            while (start1 < end1)
                b[k++] = a[start1++];
            while (start2 < end2)
                b[k++] = a[start2++];
        }
        int *temp = a;
        a = b;
        b = temp;
    }
    if (a != arr) 
    {
        for (int i = 0; i < len; i++)
            b[i] = a[i];
        b = a;
    }
    free(b);
}

2. 自顶向下的方法

采用分治法进行自顶向下的算法设计,形式更为简洁。

(1)分治法的三个步骤

设归并排序的当前区间是R[low..high],分治法的三个步骤是:
①分解:将当前区间一分为二,即求分裂点:mid = (low+high)/2;
②求解:递归地对两个子区间R[low..mid]和R[mid+1..high]进行归并排序;
③组合:将已排序的两个子区间R[low..mid]和R[mid+1..high]归并为一个有序的区间R[low..high]
递归的终结条件:子区间长度为1(一个记录自然是有序的啦)。

具体实现如下:

void merge_sort(int arr[], int low, int high)
{
    if(low < high)
    {
        int mid = (low+high)/2;
        merge_sort(arr, low, mid);   //左半区排序
        merge_sort(arr, mid+1, high); //右半区排序
        merge_sort(arr, low, mid, high);//左右半区合并
    }
}

三、分析

1、稳定性
归并排序是一种稳定的排序。

2、存储结构要求
可用顺序存储结构。也易于在链表上实现。

3、时间复杂度
对长度为n的文件,需进行 log(n)log(n) 趟二路归并,每趟归并的时间为 O(n)O(n),故其时间复杂度无论是在最好情况下还是在最坏情况下均是 O(nlogn)O(nlogn)

4、空间复杂度
需要一个辅助向量来暂存两有序子文件归并的结果,故其辅助空间复杂度为O(n),显然它不是就地排序。

注意
若用单链表做存储结构,很容易给出就地的归并排序。