希尔排序(ShellSort)

来源:互联网 发布:淘宝修图师兼职 编辑:程序博客网 时间:2024/06/07 02:37

最后分析的基于比较的排序

之所以放在前面几个排序算法之后主要是因为虽然希尔排序很容易编写却很难分析,尤其是它的时间复杂度。
希尔排序思想的提出是有原因的:在那个排序还基本都是2次型(插入,选择,冒泡...)的年代,当人们经常使用
插入排序时发现有时突然插入一个很小的元素时总是“一步一步”地往前移很久,真的是让人很不爽,电脑就像瞎子一样要一个一个摸索。
因此D.L.Shell做了启发性的尝试:能不能每次不是相邻的元素来比较,而是相距较远的元素来比较,之后逐渐缩小比较的“步子”,
这样出现逆序的元素对的距离被逐渐缩小,以此来避免出现每次插入需要移动很多次的元素。

D.L.Shell自己提出的增量序列

shell的序列是“就地取材”的:第一个步长就是输入长度的一般 n / 2,之后不断整除2
/*****************************************    函数:使用shell增量的Shell排序    时间复杂度:<span style="color: rgb(51, 51, 51); font-family: arial; font-size: 13px; line-height: 20.0200004577637px;">Ω</span>(nlgn) O(n^2 / 4)*****************************************/void shellShellSort(int* a, int n){    for(int increment = n / 2; increment > 0; increment /= 2)   ///步长不断减半    {        for(int i = increment; i < n; i++)        {            int tmp = a[i]; ///把待插入元素保存到临时变量中            int destIndex = i;  ///计算插入位子            ///把第一次测试单独提出来            if(a[destIndex -= increment] > tmp)            {                do                {                    a[destIndex + increment] = a[destIndex];                }while((destIndex -= increment) >= 0 && a[destIndex] > tmp);     ///测试上一个是否是目标位置                a[destIndex + increment] = tmp;     ///最后一次测试失败使得destIndex比实际小increment            }        }    }}
关于时间复杂度的说明:对希尔排序的平均运行时间的分析是很困难的,我肯定不会啦,所以这里只分析最好和最坏情况,
而对于平均复杂度采用经过别人大量实验得出的平均值。
最好情况很容易得出:数据原本就是排好序的,用因为这里分析的增量序列都是呈几何下降的,所以都是nlgn
而希尔增量的最坏情况发生在n为2的幂时,且数据满足对于任意k长的插入序列都需要k^2 / 8次插入,比如下面的序列
{2, 6, 4, 8, 1, 5, 3, 7}

shell增量图示




shell增量为什么有时候那么慢

正如上图所示:当n为2的幂时,算法很有可能退化为n平方算法,这是因为步长均是2的幂,不同插入序列所确定的大小关系没法很好地交错(可以把大小关系理解为一张网,交错地越厉害确定的关系也就越多)。
这样一来没法保证相隔足够远的元素是有序的。所以更好的增量序列基本要求元素之间最好是互素的(起码是相邻的元素之间是互素的),
这样一来,步长的交错这一概念就转化为了数论里面数和数之间的关系。

一个比较好的增量序列:hibbard增量序列

Hibbard给出的增量序列乍看起来只是做了一点点的修改:1,3,7,...2^k - 1
但经证明发现该序列给出的最坏情况一下下降为n^1.5
首先该序列相邻的元素一定是互素的,这一点在数论里面就是数 2^i - 1 和 2^j - 1 互素的充要条件是 i 和 j 互素。
/**************************************    函数:使用hibbard增量的Shell排序    时间复杂度:<span style="color: rgb(51, 51, 51); font-family: arial; font-size: 13px; line-height: 20.0200004577637px;">Ω</span>(nlgn) O(n^1.5)**************************************/void hibbardShellSort(int* a, int n){    int powerOf2 = 1;    while(powerOf2 < n) powerOf2 <<= 1; ///乘2    for(int increment = powerOf2 - 1; increment > 0; increment = (powerOf2 >>= 1) -1)   ///步长几何递减    {        for(int i = increment; i < n; i++)        {            int tmp = a[i]; ///把待插入元素保存到临时变量中            int destIndex = i;  ///计算插入位子            ///把第一次测试单独提出来            if(a[destIndex -= increment] > tmp)            {                do                {                    a[destIndex + increment] = a[destIndex];                }while((destIndex -= increment) >= 0 && a[destIndex] > tmp);     ///测试上一个是否是目标位置                a[destIndex + increment] = tmp;     ///最后一次测试失败使得destIndex比实际小increment            }        }    }}
证明该算法的上界需要了解两点:
1.用步长b插入排序后的序列称为:b - sorted,并且这一性质不会被破坏掉,也就是后面用其它步长插入数列还是b - sorted(证明很简单,不过理解很难)
2.数论里面的定理:对正整数a 和 b有所有具有(a - 1)(b - 1) + gcd(a, b) * k形式的数都可以被表示为ma + nb,其中m和n均为正整数。(没记错的话)
所以对于互素的 2^(k + 1) - 1 和 2^(k + 2) - 1凡是大于等于8 * (2^k - 1)^2 + 4 *( 2^k - 1)的数都可以表示为两数的线性组合,对于本算法,2^(k + 1) - 1 sorted
加上 2^(k + 2) - 1 sorted就能推出步长为2^k - 1时距离大于8 * (2^k - 1)^2 + 4 *( 2^k - 1)元素这么长时一定是有序的,也就用数学语言证明了按这个序列
能让关系网很好地交错。
下面是上界的级数和,分为两部分当步长大于根号n时估值(n - increment) * increment > n^2 / increment 但两个都是上界,于是选取更紧的n^2 / increment,而当步长小于根号n时,大小相反:


注意,上面只是算法的上界,平均情况下还要小,书上说是O(n^1.25),但目前还没有被证明出来。
不过有趣的是,我实现上面两种增量序列时,发现只要n不是2的幂的话shell增量运算得更快。这么说这半天都白分析了?
肯定不是,起码知道了怎么去分析增量序列。我想这也是书里面为什么会提这两种不怎么实用的增量序列的原因了。

实际应用中的增量序列

我目前知道的比较好的两个增量序列是由一对十分有名的师徒分别提出来的:

分析什么的就算了。。。
这里实现sedgewick增量序列:
/**************************************    函数:使用sedgewick增量的Shell排序    时间复杂度:Omega(nlgn) O(n^(4/3))**************************************/static int sedgeIncre[] = {0,   ///作为哨兵                           1, 5, 19, 41, 109, 209, 505, 929, 2161, 3905, 8929, 16001,                           36289, 64769, 146305, 260609, 587521, 1045505, 2354689,                           4188161, 9427969, 16764929, 37730305, 67084289, 150958081,                           268386305, 603906049, 1073643521};       ///算到了10的9次方void sedgewickShellSort(int* a, int n){    int idx = 0;    ///当前的步长下标    while(sedgeIncre[idx] < n) idx++;    for(int increment = sedgeIncre[idx]; increment > 0; increment = sedgeIncre[--idx])   ///步长几何递减    {        for(int i = increment; i < n; i++)        {            int tmp = a[i]; ///把待插入元素保存到临时变量中            int destIndex = i;  ///计算插入位子            ///把第一次测试单独提出来            if(a[destIndex -= increment] > tmp)            {                do                {                    a[destIndex + increment] = a[destIndex];                }while((destIndex -= increment) >= 0 && a[destIndex] > tmp);     ///测试上一个是否是目标位置                a[destIndex + increment] = tmp;     ///最后一次测试失败使得destIndex比实际小increment            }        }    }}
目前sedgewick增量是已被证明上界的序列中最好的:O(n^(4/3))
并且在实际情况下被猜想为O(n^(7/6)),对于中型数据其效率是比堆排序高的,因为常数优势。

后记

本来想利用希尔排序替代插入排序来优化快速排序的,不过没想到没啥用,稍大一点的数据拼不过快速排序的nlgn+小常数,
稍小一点的数据对插入排序的最坏情况下的优化也比不过插入排序更小的常数。
还是老样子,若内容有误后有更好的想法请在下面评论,谢谢。






1 0