排序算法中的归并排序(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
/
2
n/2
n/2 个长度为2的有序子文件;若n为奇数,则最后一个子文件轮空(不参与归并)。故本趟归并完成后,前
l
o
g
(
n
)
log(n)
log(n) 个有序子文件长度为2,但最后一个子文件长度仍为1;第2趟归并则是将第1趟归并所得到的
l
o
g
(
n
)
log(n)
log(n) 个有序的子文件两两归并,如此反复,直到最后得到一个长度为n的有序文件为止。
上述的每次归并操作,均是将两个有序的子文件合并成一个有序的子文件,故称其为"二路归并排序"。类似地有
k
(
k
>
2
)
k(k>2)
k(k>2) 路归并排序。
(2) 一趟归并算法
分析:在某趟归并中,设各子文件长度为length(最后一个子文件的长度可能小于length),则归并前R[1…n]中共有 n / l e n g t h n/length n/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的文件,需进行
l
o
g
(
n
)
log(n)
log(n) 趟二路归并,每趟归并的时间为
O
(
n
)
O(n)
O(n),故其时间复杂度无论是在最好情况下还是在最坏情况下均是
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)。
4、空间复杂度
需要一个辅助向量来暂存两有序子文件归并的结果,故其辅助空间复杂度为O(n),显然它不是就地排序。
注意:
若用单链表做存储结构,很容易给出就地的归并排序。