上一篇日志讲到,对于直接插入排序法,如果要做改良,可以从两个方面入手,减少元素之间的比较次数和减少元素之间的移动次数。从减少元素之间的比较次数方面,我们可以用二分查找的思想,因为在直接插入排序的“寻找插入位置”这一过程,是一个查找过程,所以可以嵌入二分查找的方法来寻找插入位置,把直接插入排序改良成二分插入排序,这是减少了序列中元素之间比较的次数。那么如果想要减少元素之间移动的次数,怎么做?
希尔排序是一种方法,又称“缩小增量排序”法。希尔排序可以减少直接插入排序中元素移动的次数,从而加快排序的进行。它是怎么做到的?看看希尔排序的思路:
- 先将待排序列按照某一“增量”,分割成若干个子序列。
- 对每一个子序列做直接插入排序。
- 缩小增量,继续把整个序列按照增量分割成若干个子序列。
- 继续对每一个子序列做直接插入排序。
…………………………………………
5.重复上述步骤,直到最后增量为1后,也就是最后一次希尔排序,变成了一次直接插入排序。
上面就是希尔排序的过程。直接插入排序元素之间移动次数多的原因是每次只交换相邻两个元素之间的位置,这样做每次只能消除一对位置错误的元素,而希尔排序每次交换的是一定间隔的元素,通过每次消除一定间隔距离的元素,来减少排序过程中元素之间的移动次数。
具体是怎么实现的,我们能通过代码来看看:
/*希尔排序方法1*/
void Shell_Sort(PtrlSqList P, int N, int Increment[])
{
int increases, j, Tmp, k, Si;
int count=0; /*排序次数的计数*/
/*获得待排序列的长度,用来决定使用增量序列的长度*/
int ListLength=GetListLength(P);
/*遍历得出需要增量序列的长度*/
for (Si=0; Increment[Si]>=ListLength; Si++);
/*完成后得到的就是只需要用到Increment[Si]和之前的元素*/
printf("\n需要用到的增量序列为:");
for (j=Si; Increment[j]>=1; j++) {
printf("%d ", Increment[j]);
}
printf("\n\n希尔插入排序过程:\n");
/*选择增量,当增量不等与1时,到下一个增量*/
for (increases=Increment[Si]; increases>=1; increases=Increment[++Si]) {
/*直接插入排序*/
for (k=increases; k<=ListLength; k++) {
Tmp=P->arr[k]; /*从序列中逐个拿出元素*/
for (j=k; j>=increases && P->arr[j-increases]>Tmp; j-=increases) {
P->arr[j]=P->arr[j-increases]; /*往后挪一位*/
}
P->arr[j]=Tmp; /*最后找到比Tmp大的数都往后挪了,留下正确的位置就插入*/
}
count++;
PrintList(P, count); /*此为输出序列函数,此处每次输出一次排序后的序列*/
printf("\n");
printf("\n");
}
}
表示增量的逐步缩小,我们用一个增量数组(第80行)来存放增量,并传入希尔排序的函数中。首先我们取得待排序列的长度,根据待排序列的长度来决定需要最大多大的增量。第90行从增量序列中第一个元素(增量最大的元素)开始,如果增量比待排序列的长度大,就不要,Si++;循环做完后,Si就是小于待排序列长度的最大增量。我们从这个增量开始,做缩小增量排序。
接着到排序过程,从增量序列中拿出第一个最大的增量赋值给increases,当increases增量不等于1也就是没到最后一次排序时,就从增量序列中继续拿出下一个缩小了的增量。
拿出一个增量后,从这个增量开始,做直接插入排序(第99行,k从increases开始到ListLength,即当作待排序列做直接插入排序),拿出序列中的第一个位置的元素赋给Tmp暂时保存,接着最内层for循环,j从k开始,即在“已排好序列”中,如果发现前一个增量位置的元素大于Tmp,就把该元素后移一个增量的位置,然后j后移一个增量。最后比Tmp大的数都后挪了一个增量位置后,找到最小的位置j,让Tmp赋值给P->arr[ j ]。
上面是第一种实现方法,还有第二种是改良“带哨兵的直接插入排序法”的希尔排序,我们来看具体怎么做:
/*希尔排序方法2*/
void Shell_Sort(PtrlSqList P, int length, int Increment[])
{
int increases, j, k, ListLength, Si;
int count=0;
ListLength=GetLength(P);
/*遍历获得需要的增量序列*/
for (Si=0; Increment[Si]>=ListLength; Si++);
printf("\n需要用到的增量序列为:");
for (j=Si; Increment[j]>=1; j++) {
printf("%d ", Increment[j]);
}
printf("\n\n希尔排序过程:\n");
for (increases=Increment[Si]; increases>=1; increases=Increment[++Si]) {
/*插入排序*/
for (j=increases; j<=ListLength; j++) {
/*如果后一个数小于前一个间隔增量的数*/
if (P->arr[j]<P->arr[j-increases]) {
P->arr[0]=P->arr[j]; /*把较小的数赋值给哨兵暂存*/
for (k=j-increases; k>0 && P->arr[0]<P->arr[k]; k-=increases) {
/*如果P->arr[0]<P->arr[k],即哨兵处元素比某一增量处的元素小的话*/
P->arr[k+increases]=P->arr[k]; /*记录后移,找出插入位置*/
}
P->arr[k+increases]=P->arr[0];
}
}
count++;
PrintList(P, count);
printf("\n\n");
}
}
前面同样是传进去增量数组Incerment,然后获取待排序列的长度并获得需要的增量序列。第108行同样从第一个最大增量开始,做插入排序,第110行i从increases开始,判断如果后一个数小于前一个间隔增量的数,就先把较小的数赋值给P->arr[ 0 ]哨兵处保存,然后最内层for循环,k从j-increases开始,即”已排好序列”中开始按增量逐一比较,如果有某一处元素比增量元素大的话,就把这个较大的元素后移一个增量的位置,最后for循环做完后,比哨兵处大的元素都后移了一个增量的位置,剩下的k+increases位置就是插入位置(因为循环每次k-=increases,当发现P->arr[0]不是小于P->arr[k]后,k要加increases才回到后一个,即正确的的插入位置)。
完整代码在个人代码云: