前面我们讲了堆排序,因为它用到了完全二叉树,充分利用了完全二叉树的深度是|log2n|+1的特性,所以效率比较高。不过堆结构的设计本身是比较复杂的,老实说,能想出这样的结构就挺不容易,有没有更直接简单的办法利用完全二叉树来排序,当然有。
高考一本、二本、专科分数线是如何划分:
简单地说,如果各高校本科专业在某省高三理科学生中计划招收1万名,那么将全省参加高考的理科学生分数倒排序,第1万名的总分数就是当年本科生的分数线(现实可能会比这复杂,这里简化之)。也就是说,即使你是你们班级第一、甚至年级第一名,如果你没有上分数线,则说明你的成绩排不到全省前1万名,你也就基本失去了当年上本科的机会。
换句话说,所谓的全省排名,其实也就是每个市、每个县、每个学校、每个班级的排名合并后再排名得到的。
要比较两个学生的成绩高低是很容易的,比如甲比乙分数低,丙比丁分数低。那么我们也就可以很容易得到甲乙丙丁合并后的成绩排名,同样的,戊己庚辛的排名也容易得到,由于他们两组分别有序了,把他们八个学生成绩合并有序也是很容易做到的了,继续下去……最终完成全省学生的成绩排名,此时高考状元也就诞生了。
为了更清晰地说清楚这里的思想,我们将本是无序的数组序列{16,7,13,10,9,15,3,2,5,8,12,1,11,4,6,14},通过两两合并排序后再合并,最终获得了一个有序的数组。注意仔细观察它的形状,你会发现,它像极了一棵倒置的完全二叉树,通常涉及到完全二叉树结构的排序算法,效率一般都不低的——这就是我们要讲的归并排序法。
归并排序算法
"归并"一词的中文含义就是合并、并入的意思,而在数据结构中的定义是将两个或两个以上的有序表组合成一个新的有序表。
归并排序(Merging Sort)就是利用归并的思想实现的排序方法。它的原理是假设初始序列含有n个记录,则可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到|n/2|(|x|表示不小于x的最小整数)个长度为2或1的有序子序列;再两两归并,……,如此重复,直至得到一个长度为n的有序序列为止,这种排序方法称为2路归并排序。
好了,有了对归并排序的初步认识后,我们来看代码。
/* 对顺序表L作归并排序 */
void MergeSort(SqList *L)
{
MSort(L->r, L->r, 1, L->length);
}
一句代码,别奇怪,它只是调用了另一个函数而已。为了与前面的排序算法统一,我们用了同样的参数定义SqList *L,由于我们要讲解的归并排序实现需要用到递归调用,因此我们外封装了一个函数。假设现在要对数组{50,10,90,30,70,40,80,60,20}进行排序,L.length=9,我现来看看MSort的实现。
/* 将SR[s..t]归并排序为TR1[s..t] */
void MSort(int SR[], int TR1[], int s, int t)
{
int m;
int TR2[MAXSIZE + 1];
if (s == t)TR1[s] = SR[s];
else
{
/* 将SR[s..t]平分为SR[s..m]和SR[m+1..t] */
m = (s + t) / 2;
/* 递归将SR[s..m]归并为有序的TR2[s..m] */
MSort(SR, TR2, s, m);
/* 递归将SR[m+1..t]归并为有序TR2[m+1..t] */
MSort(SR, TR2, m + 1, t);
/* 将TR2[s..m]和TR2[m+1..t] */
/* 归并到TR1[s..t] */
Merge(TR2,TR1, s, m, t);
}}
这段代码是归并排序算法中的归并操作的递归实现。
函数接收一个原始序列SR,一个用于存放结果的序列TR1,以及两个参数s和t,表示对原始序列SR的s到t位置进行归并排序。
首先判断s和t是否相等,如果相等,则说明只有一个元素,直接将该元素放入TR1中。否则,说明有多个元素需要进行归并排序。
接下来,将SR的s到t位置平分为SR的s到m位置和SR的m+1到t位置,其中m为s和t的中点。
然后,使用递归调用MSort函数,对SR的s到m位置和SR的m+1到t位置分别进行归并排序,将结果分别存放在TR2的s到m位置和TR2的m+1到t位置。
最后,调用Merge函数,将TR2的s到m位置和TR2的m+1到t位置的元素归并到TR1的s到t位置。
通过不断递归调用MSort函数,将原始序列不断分割为更小的子序列,并对子序列进行归并排序,最终得到了完整的有序序列TR1。
综上所述,这段代码实现了归并排序算法的归并操作,通过递归地将序列平分为更小的子序列,并将归并排序的结果合并起来,最终得到了完整的有序序列。
现在我们来看看Merge函数的代码是如何实现的。
/* 将有序的SR[i..m]和SR[m+1..n]归并为有序的TR[i..n] */
void Merge(int SR[], int TR[], int i, int m, int n)
{
int j, k, l;
/* 将SR中记录由小到大归并入TR */
for (j = m + 1, k = i; i <= m && j <= n; k++)
{
if (SR[i] < SR[j])
TR[k] = SR[i++];
else
TR[k] = SR[j++];
}
if (i <= m)
{
for (l = 0; l <= m - i; l++)
/* 将剩余的SR[i..m]复制到TR */
TR[k + l]=SR[i + l];
}
if (j<=n)
{
for (l = 0; l <= n - j; l++)
/* 将剩余的SR[j..n]复制到TR */
TR[k + l] = SR[j + l];
}}
这段代码实现了归并排序算法中的归并操作。
函数接收一个有序序列SR,一个用于存放结果的序列TR,以及三个参数i、m和n,表示对有序序列SR的i到m位置和m+1到n位置进行归并。
首先,通过循环遍历i到m位置和m+1到n位置的元素,将较小的元素先放入TR中。循环条件是i小于等于m并且j小于等于n。
在循环内部,通过比较SR[i]和SR[j]的大小,将较小的元素放入TR中的当前位置k,并将相应的指针i或j递增1,指向下一个要比较的元素。同时,k也递增1,指向TR中的下一个位置。
如果i小于等于m,说明SR[i..m]中仍有剩余元素未放入TR中,需要将剩余的部分复制到TR中。通过一个循环将剩余的SR[i..m]复制到TR中的相应位置。
同样地,如果j小于等于n,说明SR[j..n]中仍有剩余元素未放入TR中,也通过一个循环将剩余的SR[j..n]复制到TR中的相应位置。
通过这个归并操作,将有序的SR[i..m]和SR[m+1..n]归并为有序的TR[i..n]。
综上所述,这段代码实现了归并排序算法中的归并操作,通过比较两个有序序列的元素大小,并将较小的元素放入结果数组中,最终得到了完整的有序序列TR。
归并排序复杂度分析
我们来分析一下归并排序的时间复杂度,一趟归并需要将SR[1]~SR[n]中相邻的长度为h的有序序列进行两两归并。
并将结果放到TR1[1]~TR1[n]中,这需要将待排序序列中的所有记录扫描一遍,因此耗费O(n)时间,而由完全二叉树的深度可知,整个归并排序需要进行次,因此,总的时间复杂度为O(nlogn),而且这是归并排序算法中最好、最坏、平均的时间性能。
由于归并排序在归并过程中需要与原始记录序列同样数量的存储空间存放归并结果以及递归时深度为log2n的栈空间,因此空间复杂度为O(n+logn)。
另外,对代码进行仔细研究,发现Merge函数中有if(SR[i]<SR[j])语句,这就说明它需要两两比较,不存在跳跃,因此归并排序是一种稳定的排序算法。
也就是说,归并排序是一种比较占用内存,但却效率高且稳定的算法。