前言
归并排序使用了分治思想,本篇文章将会通过这种排序让读者大概体会什么是分治思想。
归并排序
归并排序是完全符合分治思想的一种排序方法。
首先,对于待排序的数组 A[ n ],将其分为两个子列 A[ 0..n/2 ],A[ n/2+1..n-1 ];
然后,对这两个子列分别进行归并排序,然后再根据大小顺序合并在一起。
这两个过程一个是“分”,一个是“治”。
看个例子:
若是对上面这个数组进行排序,首先将其 “分” 为两个子序列:
对这两个子序列排序,又需要将它们分别分解,逐层分解,最后得到这样的一张图:
每一行都是上一行的子问题,当分解为单个元素时,这已经是最小子问题了,这个时候才开始 “治”,也就是逐层合并子列。
如果你看懂下图,那基本上就明白归并排序的大致思路了。
就像上面这个过程一样,当子列被拆分到单个元素时开始合并子列, 并逐层向上按顺序合并子列。
下面一个问题是,如何实现合并两个排好序的子列?这里我们以递增子列为例。
这里我们可以用两个指针来解决这个问题。两个指针分别指向两个子列的首项,然后进行比较,每次取较小的那个元素,然后所在子列的指针后移,如果其中一个子列排完,那么剩下的直接加入到数组当中。
对于合并子列,我们可以写出这样的伪代码:
MERGE( A, p, q, r ) //A为数组,p为左子列的首项,r为右子列的末尾,q为左子列的末尾
1 n1 = p - q + 1 n2 = r - q //获取两个子列的长度
2 let L[ 0..n1 ] and R[ 0..n2 ] be new Arrays //创建两个子列空间
3 for i = 0 to n1-1 L[ i ] = A[ p + i ] //拷贝赋值
4 for i = 0 to n2-1 R[ i ] = A[ q + i + 1 ]
5 L[ n1 ] = R[ n2 ] = ∞ //设置哨兵
6 i = j = 0 //设置指针
7 for k = p to r
8 if L[ i ] <= R[ j ] A[ k ] = L[ i ], i++
9 else A[ k ] = R[ j ], j++
在伪代码中,我们发现,在创建左右子列时,分别多创建了一个位置,并在第5行代码中设置为无穷大。我们称这两个位置的元素为哨兵。
为什么要设置哨兵呢?
如果没有哨兵的话,在比较左右子列的每个元素时,就要判断哪个子列的元素首先全部纳入数组中,再把另外子列的剩余元素加入队列完成排序。在两个子列的尾部都加入哨兵的话,就会简单很多,因为哨兵的值为无穷大,两个子列的指针永远不会越界,并且当一个子列全部排入数组后,另一个子列的所有元素肯定小于哨兵的值,就可以顺序加入数组,从而减少了指针判定的操作。
我们已经给出了归并子列的算法,再加上前面讲述的分治的思想,你应该能够自己设计出归并排序的伪代码了:
MERGE-SORT( A, p, r ) //A为数组,p为首项,r为末尾
1 if q < r
2 q = ( p + r ) / 2
3 MERGE-SORT( A, p, q )
4 MERGE-SORT( A, q + 1, r )
5 MERGE( A, p, q, r)
我们用递归的办法体现了分治思想,当排序数组 A[ n ] 时,我们调用 MERGE-SORT( A, 0, n-1 ) 进行排序,代码的第3、4行又将其拆分为两个子问题,递归地求解,当子问题全部排好序后,再用MERGE进行归并。
C/C++代码如下:
void Merge(int* a, int p, int q, int r) {
//求两个子序列的长度
int n1 = q - p + 1; //n1为a[p]到a[q]
int n2 = r - q; //n2为a[q+1]到a[r]
int* L, * R; //构造两个子序列
L = (int*)malloc(sizeof(int) * (n1+1)); //分配空间
R = (int*)malloc(sizeof(int) * (n2+1)); //这里+1是给最后一位留空
L[n1] = INT_MAX; //最后一位设为最大值
R[n2] = INT_MAX;
int i, j;
for (i = 0; i < n1; i++) { //分别拷贝两个序列信息
L[i] = a[p+i]; //从a[p]到a[q]
}
for (i = 0; i < n2; i++) {
R[i] = a[q+i+1]; //从a[q+1]到a[r]
}
i = 0; //从两个子列的第一项开始
j = 0;
for (int k = p; k <= r; k++) { //重新排序
if (L[i] <= R[j]) { //选择小的放
a[k] = L[i];
i = i + 1;
}
else { //这里体现最后一位INT_MAX的作用
a[k] = R[j]; //当一个子列全部排完后,就把另一个子列复制过来
j = j + 1;
}
}
return;
}
void Merge_Sorting(int* a, int p, int r) { //利用递归方法进行排序
int q; //创建中间项
if (p < r) {
q = (p + r) / 2;
Merge_Sorting(a, p, q); //归并左子列
Merge_Sorting(a, q + 1, r); //归并右子列
Merge(a, p, q, r); //将排好序的左右子列归并
}
return;
}
本文是《算法导论》的学习笔记,如有错误,希望大佬在评论区指正!