R[j+d]=R[0];//插入R[i]到正确的位置上
}//endif
}//ShellPass
voidShellSort(SeqListR)
{
intincrement=n;//增量初值,不妨设n>0
do{
increment=increment/3+1;//求下一增量
ShellPass(R,increment);//一趟增量为increment的Shell插入排序
}while(increment>1)
}//ShellSort
注意:
当增量d=1时,ShellPass和InsertSort基本一致,只是由于没有哨兵而在内循环中增加了一个循环判定条件"j>0",以防下标越界。
2.设监视哨的shell排序算法
算法分析
1.增量序列的选择
Shell排序的执行时间依赖于增量序列。
好的增量序列的共同特征:
①最后一个增量必须为1;
②应该尽量避免序列中的值(尤其是相邻的值)互为倍数的情况。
有人通过大量的实验,给出了目前较好的结果:
当n较大时,比较和移动的次数约在nl.25到1.6n1.25之间。
2.Shell排序的时间性能优于直接插入排序
希尔排序的时间性能优于直接插入排序的原因:
①当文件初态基本有序时直接插入排序所需的比较和移动次数均较少。
②当n值较小时,n和n2的差别也较小,即直接插入排序的最好时间复杂度O(n)和最坏时间复杂度0(n2)差别不大。
③在希尔排序开始时增量较大,分组较多,每组的记录数目少,故各组内直接插入较快,后来增量di逐渐缩小,分组数逐渐减少,而各组的记录数目逐渐增多,但由于已经按di-1作为距离排过序,使文件较接近于有序状态,所以新的一趟排序过程也较快。
因此,希尔排序在效率上较直接插人排序有较大的改进。
3.稳定性
希尔排序是不稳定的。
参见上述实例,该例中两个相同关键字49在排序前后的相对次序发生了变化。
五.堆排序
1、堆排序定义
n个关键字序列Kl,K2,…,Kn称为堆,当且仅当该序列满足如下性质(简称为堆性质):
(1)ki≤K2i且ki≤K2i+1或
(2)Ki≥K2i且ki≥K2i+1(1≤i≤)
若将此序列所存储的向量R[1..n]看做是一棵完全二叉树的存储结构,则堆实质上是满足如下性质的完全二叉树:
树中任一非叶结点的关键字均不大于(或不小于)其左右孩子(若存在)结点的关键字。
【例】关键字序列(10,15,56,25,30,70)和(70,56,30,25,15,10)分别满足堆性质
(1)和
(2),故它们均是堆,其对应的完全二叉树分别如小根堆示例和大根堆示例所示。
2、大根堆和小根堆
根结点(亦称为堆顶)的关键字是堆里所有结点关键字中最小者的堆称为小根堆。
根结点(亦称为堆顶)的关键字是堆里所有结点关键字中最大者,称为大根堆。
注意:
①堆中任一子树亦是堆。
②以上讨论的堆实际上是二叉堆(BinaryHeap),类似地可定义k叉堆。
3、堆排序特点
堆排序(HeapSort)是一树形选择排序。
堆排序的特点是:
在排序过程中,将R[l..n]看成是一棵完全二叉树的顺序存储结构,利用完全二叉树中双亲结点和孩子结点之间的内在关系【参见二叉树的顺序存储结构】,在当前无序区中选择关键字最大(或最小)的记录。
4、堆排序与直接选择排序的区别
直接选择排序中,为了从R[1..n]中选出关键字最小的记录,必须进行n-1次比较,然后在R[2..n]中选出关键字最小的记录,又需要做n-2次比较。
事实上,后面的n-2次比较中,有许多比较可能在前面的n-1次比较中已经做过,但由于前一趟排序时未保留这些比较结果,所以后一趟排序时又重复执行了这些比较操作。
堆排序可通过树形结构保存部分比较结果,可减少比较次数。
5、堆排序
堆排序利用了大根堆(或小根堆)堆顶记录的关键字最大(或最小)这一特征,使得在当前无序区中选取最大(或最小)关键字的记录变得简单。
(1)用大根堆排序的基本思想
①先将初始文件R[1..n]建成一个大根堆,此堆为初始的无序区
②再将关键字最大的记录R[1](即堆顶)和无序区的最后一个记录R[n]交换,由此得到新的无序区R[1..n-1]和有序区R[n],且满足R[1..n-1].keys≤R[n].key
③由于交换后新的根R[1]可能违反堆性质,故应将当前无序区R[1..n-1]调整为堆。
然后再次将R[1..n-1]中关键字最大的记录R[1]和该区间的最后一个记录R[n-1]交换,由此得到新的无序区R[1..n-2]和有序区R[n-1..n],且仍满足关系R[1..n-2].keys≤R[n-1..n].keys,同样要将R[1..n-2]调整为堆。
……
直到无序区只有一个元素为止。
(2)大根堆排序算法的基本操作:
①初始化操作:
将R[1..n]构造为初始堆;
②每一趟排序的基本操作:
将当前无序区的堆顶记录R[1]和该区间的最后一个记录交换,然后将新的无序区调整为堆(亦称重建堆)。
注意:
①只需做n-1趟排序,选出较大的n-1个关键字即可以使得文件递增有序。
②用小根堆排序与利用大根堆类似,只不过其排序结果是递减有序的。
堆排序和直接选择排序相反:
在任何时刻,堆排序中无序区总是在有序区之前,且有序区是在原向量的尾部由后往前逐步扩大至整个向量为止。
(3)堆排序的算法:
voidHeapSort(SeqIAstR)
{//对R[1..n]进行堆排序,不妨用R[0]做暂存单元
inti;
BuildHeap(R);//将R[1-n]建成初始堆
for(i=n;i>1;i--){//对当前无序区R[1..i]进行堆排序,共做n-1趟。
R[0]=R[1];R[1]=R[i];R[i]=R[0];//将堆顶和堆中最后一个记录交换
Heapify(R,1,i-1);//将R[1..i-1]重新调整为堆,仅有R[1]可能违反堆性质
}//endfor
}//HeapSort
(4)BuildHeap和Heapify函数的实现
因为构造初始堆必须使用到调整堆的操作,先讨论Heapify的实现。
①Heapify函数思想方法
每趟排序开始前R[l..i]是以R[1]为根的堆,在R[1]与R[i]交换后,新的无序区R[1..i-1]中只有R[1]的值发生了变化,故除R[1]可能违反堆性质外,其余任何结点为根的子树均是堆。
因此,当被调整区间是R[low..high]时,只须调整以R[low]为根的树即可。
"筛选法"调整堆
R[low]的左、右子树(若存在)均已是堆,这两棵子树的根R[2low]和R[2low+1]分别是各自子树中关键字最大的结点。
若R[low].key不小于这两个孩子结点的关键字,则R[low]未违反堆性质,以R[low]为根的树已是堆,无须调整;否则必须将R[low]和它的两个孩子结点中关键字较大者进行交换,即R[low]与R[large](R[large].key=max(R[2low].key,R[2low+1].key))交换。
交换后又可能使结点R[large]违反堆性质,同样由于该结点的两棵子树(若存在)仍然是堆,故可重复上述的调整过程,对以R[large]为根的树进行调整。
此过程直至当前被调整的结点已满足堆性质,或者该结点已是叶子为止。
上述过程就象过筛子一样,把较小的关键字逐层筛下去,而将较大的关键字逐层选上来。
因此,有人将此方法称为"筛选法"。
②BuildHeap的实现
要将初始文件R[l..n]调整为一个大根堆,就必须将它所对应的完全二叉树中以每一结点为根的子树都调整为堆。
显然只有一个结点的树是堆,而在完全二叉树中,所有序号的结点都是叶子,因此以这些结点为根的子树均已是堆。
这样,我们只需依次将以序号为,-1,…,1的结点作为根的子树都调整为堆即可。
具体算法【参见教材】。
5、大根堆排序实例
对于关键字序列(42,13,24,91,23,16,05,88),在建堆过程中完全二叉树及其存储结构的变化情况参见。
6、算法分析
堆排序的时间,主要由建立初始堆和反复重建堆这两部分的时间开销构成,它们均是通过调用Heapify实现的。
堆排序的最坏时间复杂度为O(nlgn)。
堆排序的平均性能较接近于最坏性能。
由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。
堆排序是就地排序,辅助空间为O
(1),
它是不稳定的排序方法。
六.快速排序
快速排序的基本思路是:
首先我们选择一个中间值middle(程序中我们可使用数组中间值),把比中间值小的放在其左边,比中间值大的放在其右边。
由于这个排序算法较复杂,我们先给出其进行一次排序的程序框架(从各类数据结构教材中可得):
voidQuickSort(int*pData,intleft,intright)
{
inti,j;
intmiddle,iTemp;
i=left;
j=right;
middle=pData[(left+right)/2];//求中间值
do
{
while((pData[i] i++;
while((pData[j]>middle)&&(j>left))//从右扫描小于中值的数
j--;
if(i<=j)//找到了一对值
{
//交换
iTemp=pData[i];
pData[i]=pData[j];
pData[j]=iTemp;
i++;
j--;
}
}while(i<=j);//如果两边扫描的下标交错,就停止(完成一次)
//当左边部分有值(left if(left QuickSort(pData,left,j);
//当右边部分有值(right>i),递归右半边
if(right>i)
QuickSort(pData,i,right);
}
对于n个成员,快速排序法的比较次数大约为n*logn次,交换次数大约为(n*logn)/6次。
如果n为100,冒泡法需要进行4950次比较,而快速排序法仅需要200次,快速排序法的效率的确很高。
快速排序法的性能与中间值的选定关系密切,如果每一次选择的中间值都是最大值(或最小值),该算法的速度就会大大下降。
快速排序算法最坏情况下的时间复杂度为O(n2),而平均时间复杂度为O(n*logn)。
七.合并排序
說明
之前所介紹的排序法都是在同一個陣列中的排序,考慮今日有兩筆或兩筆以上的資料,它可能是不同陣列中的資料,或是不同檔案中的資料,如何為它們進行排序?
解法
可以使用合併排序法,合併排序法基本是將兩筆已排序的資料合併並進行排序,如果所讀入的資料尚未排序,可以先利用其它的排序方式來處理這兩筆資料,然後再將排序好的這兩筆資料合併。
有人問道,如果兩筆資料本身就無排序順序,何不將所有的資料讀入,再一次進行排序?
排序的精神是儘量利用資料已排序的部份,來加快排序的效率,小筆資料的排序較為快速,如果小筆資料排序完成之後,再合併處理時,因為兩筆資料都有排序了,所有在合併排序時會比單純讀入所有的資料再一次排序來的有效率。
那麼可不可以直接使用合併排序法本身來處理整個排序的動作?
而不動用到其它的排序方式?
答案是肯定的,只要將所有的數字不斷的分為兩個等分,直到最後剩一個數字為止,然後再反過來不斷的合併,就如下圖所示:
不過基本上分割又會花去額外的時間,不如使用其它較好的排序法來排序小筆資料,再使用合併排序來的有效率。
下面這個程式範例,我們使用快速排序法來處理小筆資料排序,然後再使用合併排序法處理合併的動作。
例子
C
#include
#include
#include
#defineMAX110
#defineMAX210
#defineSWAP(x,y){intt;t=x;x=y;y=t;}
intpartition(int[],int,int);
voidquicksort(int[],int,int);
voidmergesort(int[],int,int[],int,int[]);
intmain(void){
intnumber1[MAX1]={0};
intnumber2[MAX1]={0};
intnumber3[MAX1+MAX2]={0};
inti,num;
srand(time(NULL));
printf("排序前:
");
printf("\nnumber1[]:
");
for(i=0;inumber1[i]=rand()%100;
printf("%d",number1[i]);
}
printf("\nnumber2[]:
");
for(i=0;inumber2[i]=rand()%100;
printf("%d",number2[i]);
}
//先排序兩筆資料
quicksort(number1,0,MAX1-1);
quicksort(number2,0,MAX2-1);
printf("\n排序後:
");
printf("\nnumber1[]:
");
for(i=0;iprintf("%d",number1[i]);
printf("\nnumber2[]:
");
for(i=0;iprintf("%d",number2[i]);
//合併排序
mergesort(number1,MAX1,number2,MAX2,number3);
printf("\n合併後:
");
for(i=0;iprintf("%d",number3[i]);
printf("\n");
return0;
}
intpartition(intnumber[],intleft,intright){
inti,j,s;
s=number[right];
i=left-1;
for(j=left;jif(number[j]<=s){
i++;
SWAP(number[i],number[j]);
}
}
SWAP(number[i+1],number[right]);
returni+1;
}
voidquicksort(intnumber[],intleft,intright){
intq;
if(leftq=partition(number,left,right);
quicksort(number,left,q-1);
quicksort(number,q+1,right);
}
}
voidmergesort(intnumber1[],intM,intnumber2[],
intN,intnumber3[]){
inti=0,j=0,k=0;
while(i