数据结构与算法(十九)希尔排序

希尔排序(Shell Sort)是插入排序方式的一种,又称“缩小增量排序”,是直接插入排序算法的一种更高效的改进版本。

直接插入排序,它的效率在某些时候是很高的,比如,数据集本身就是基本有序的,只需要少量的插入操作,就可以完成整个数据集的排序工作,此时直接插入很高效;还有就是数据量比较少时,直接插入的优势也比较明显。可问题在于,这两个条件本身就过于苛刻,现实中数据量少或者基本有序都属于特殊情况。

因此,希尔排序的基本思想就是:将需要排序的序列划分为若干个较小的序列,对这些序列进行直接插入排序,通过这样的操作可使需要排序的数列基本有序,最后再使用一次直接插入排序。

怎么划分子序列?假设我们现在有一个序列arr[10] = {0,9,1,5,8,3,7,4,6,2}。

首先试一下分组划分子序列:我们将序列分出三组{9,1,5}、{8,3,7}和{4,6,2},然后排序为{1,5,9}、{3,7,8}、{2,4,6},再合并成1,5,9,3,7,8,2,4,6},显然这个序列并不满足基本有序的条件。

因此使用跳跃法分割子序列:将相距某个‘增量'的数据组成一个子序列。这样才能保证在子序列内分别进行直接插入排序后得到的结果是基本有序而不是局部有序。

对于希尔排序而言,增量是重中之重,可究竟应该选择怎样的增量才是最好目前还是一个数学难题,迄今为止还没有人找到一种最好的增量序列,不过希尔给出一个N/k的增量序列(简单常用但不算好),N为序列长度,k的取值范围一般在2~6之间,当然最好还是根据具体情况进行选择增量序列。而当k=N时,步长为1,希尔排序就是直接插入排序。

如果看刚开始看不懂希尔排序,就忽略increment的定义和设置,然后把increment=1带进去看,会发现和直接插入排序一模一样。

/*希尔排序算法*/
void Shell_Sort(int *arr, int len) 
{
    int increment = len;             /*增量或步长*/
    while (increment>1)
    {                                /*增量的取值是一门学问,这里使用原始希尔增量N/k*/
        increment = increment/2;     /*k=2,每次增量折半,直到为1为止*/
        for (int i=increment+1;i<=len;i++)   /*i指向增量右边的位置,可以防止数组越界*/
        {                                    
            if (arr[i-increment] > arr[i]) { /*增量左边的数据大于增量右边的数据*/       
                arr[0] = arr[i];     /*哨兵存储要插入的值,不然后移时会覆盖*/
                int flag = 0;        /*flag记录要插入的位置*/
                for (int j=i-increment;arr[j]>arr[0] && j>0;flag=j,j-=increment)
                    arr[j+increment] = arr[j];/*交换*/
                arr[flag] = arr[0];
            }
        }
    }
} 

我们先计算增量increment=9/2=4,然后看一下等待排序的序列,根据增量在脑海将序列进行分组:{9, 3, 2}、{1, 7}、{5, 4}、{8, 6},这将有助于我们理解。

首先进行increment=4的第一次循环,i=increment+1=4+1=5,a[i-increment=1]=9 大于 a[i=5]=3,所以要9要后移,3要前移。

这一步相当于直接插入排序在处理序列{9, 3, 2}中的第二个元素3。

继续进行increment=4的第二次循环,a[i-increment=2]=1 小于 a[i=6]=7,所以不需要移动。

这一步相当于直接插入排序在处理序列{1, 7}中的第二个元素7,因为序列只有两个元素所以这个序列已经排序完毕。

继续进行increment=4的第三次循环,a[i-increment=3]=5 大于 a[i=7]=4,所以5要后移,4要前移。

这一步相当于直接插入排序在处理序列{5, 4}中的第二个元素4,因为序列只有两个元素所以这个序列已经排序完毕。

继续进行increment=4的第四次循环,a[i-increment=4]=8 大于 a[i=8]=6,所以8要后移,6要前移。

这一步相当于直接插入排序在处理序列{8, 6}中的第二个元素6,因为序列只有两个元素所以这个序列已经排序完毕。

继续进行increment=4的第五次循环,a[i-increment=5]=9 大于 a[i=9]=2,所以9要后移,2要前移。此时还未结束,2还要继续前移。

因为这一步相当于直接插入排序在处理序列{9, 3, 2}中的第三个元素2,2还要与3比较并前移,才算这个序列排序完毕。

此时increment=4的的循环进行完毕,得到了下图这样基本有序的的序列。这就是希尔排序的精华所在,将关键字小的数据跳跃式的前移,而不是直接插入排序的一步一步地往前“挪”,所以希尔排序的一轮循环等于直接插入排序的很多轮循环,减少了大量的比较移动次数。

接着就是increment=4/2=2,开始新一轮的循环,重复上述的步骤,这里不再详细解释。

希尔排序的时间复杂度分析是一个复杂的问题,涉及到一些数学上尚未解决的难题。

所以关于希尔排序的时间复杂度,我只能给出一个答案:最好情况为O(n^1.3),最坏情况肯定是O(n²),平均情况为O(n㏒n)。

注:希尔排序经常涉及到arr[i-increment]操作,其中increment为希尔排序的当前增量或步长,这种随机访问元素的操作不适合链式存储结构,因为每次遍历都会浪费大量时间,极大降低算法的效率,不管是单链表还是双向链表。

补充实测:在数据量千万级,k=2效率最低;k=4效率最高,速度比k=2快1s;k=3、5、6之间相互差0.1s左右,但都比k=2快0.5s。

猜你喜欢

转载自blog.csdn.net/qq_36557133/article/details/90322951