希尔排序

希尔排序

希尔排序算法既有着悠久的历史,同时也仍然不失活力。该算法的别致之处在于它不再是将输入视作为一个一维的序列,而是将其视作为一个二维的矩阵,并且试图对矩阵的每一列分别进行排序。如果矩阵当前的宽度为w,那么我们就将所有这w列各自的排序总称为w-sorting。

实际上每一次Shellsort排序的过程都是由若干个宽度不同的W-sorting构成的,如果矩阵的每一列都已经过排序我们就称之为w-ordered。

实际上矩阵最开始比较宽w比较大,此后Shellsort会逐步的压缩矩阵使之越来越高越来越窄。每压缩一次都随机执行一趟对应的w-sorting,从而使之变成W-ordered。我们也可以通过这样的一组图来说明这一过程

比如这可能就是最初的那个矩阵相应的比较宽比较矮,那么在执行完对应的Wk-sorting之后,Shellsort会对这个矩阵进行重组

使之成为一个相对更窄、但同时更高的矩阵。接下来对应于新的这个宽度,W_{k}减1也会做一趟逐列的排序。而在此之后,Shellsort又会对它进行重组,使之变成这样一个更加的窄更加高的矩阵。这个过程将持续的进行下去,总之矩阵会变得越来越高越来越窄,直到最终变成只有一列。同样的对最后这个矩阵,我们也需要来做一次对应的W-sorting,只不过此时的W等于1,所以我们也称之为one-sorting。可以看到整个Shellsort的过程,使用了一系列的宽度,也就是W_{k}减1,以及一直到W_{3}W_{2}W_{1}

这些宽度和在一起构成了所谓的步长序列step sequence。当然这些矩阵宽度被使用的次序恰好与它们在序列中的次序相反。然而无论如何,它们都必须是逐个单调递减的。没错在算法的执行过程中,我们所采用的矩阵宽度会逐步的递减。所以希尔排序算法也称作为递减增量法。

请注意我们这里的步长序列h,实际上除了我们刚才所说的单调性以及它的首项必须为一,我们对它暂时还没有更多的要求。

是的,这种序列有很多种可能的选择,采用不同的步长序列,Shellsort的性能也会有所不同。实际上Shellsort只是一个框架,你采用什么样的步长序列,就会得到什么样的算法。从这个意义上讲Shellsort就像一个播放机,你往里头放入什么样的CD,它就会播放什么样的音乐。因此我们宁愿说Shellsort是一个算法,不如说它是一类算法。

我们注意到既然任何步长序列,都要求首项W_{1}等于1,所以任何Shellsort都是以one-sorting结束的。而任何一次这样的one-sorting其实也就相当于全局的排序。因此最终的输出结果,也必然是正确的排序序列。因此这个算法的正确性是毫无疑问的。当然至此你可能会有一个疑问:既然无论如何最终都要做一次one-sorting,那么此前的这些排序又有什么意义呢?

是的,这正是Shellsort的奥妙所在。不过现在回答这个问题还为时尚早,接下来我们不妨通过一个具体的实例首先来切实的感受一下希尔排序的执行过程。

实例

考察这个由13个整数所构成的待排序序列,采用希尔排序算法,我们首先将矩阵的宽度取作8。

于是我们按照不超过8个元素为准则,将整个序列分为两段,而每一段都对应于矩阵的一行,这样我们就得到了一个宽度为8的矩阵。

接下来我们对这个矩阵逐列排序,很容易验证每一列的排序结果,最后三列是退化的情况,直接得到排序的结果。至此我们已经完成了对应于宽度8的一趟sorting。

在转换为新的矩阵之前,我们需要将它重新复原为一个线性的序列。具体来说与刚才构成矩阵的操作完全相反,我们这时候需要将矩阵的每一行逐个取出,并依次串接,从而构成一个线性序列。

我们接下来采用的矩阵宽度为五,因此我们接下来要以五为单位将此序列切分为若干段,而每一段都将作为新矩阵的一行。接下来我们依然需要逐列排序

此时我们不妨稍作停留来观察一下这两个中间结果。虽然我们现在还不能精确的度量,但是我们依然能够隐约而切实的感觉到整个序列的有序性是在不断的改善。那么接下来还会继续改善吗?我们不妨继续下去。

再接下来的一步,我们将矩阵的宽度取作三也就是说我们将以每三个元素为单位将整个序列分割成若干段,这些段也就分别构成了新矩阵的各行。这个新矩阵共有三列,我们接下来依然需要对这个新矩阵中的三列分别排序。

至此你也不妨再次的大致体会一下,经过刚才的这样一趟逐列排序。整个序列的有序性又向前有所改进。为了再进一步的提高

整个序列的有序性,我们接下来将要采用的矩阵宽度取作2,也就是说我们要以两个元素为单位,将整个序列切分成若干段。而且同样的每一段都构成新矩阵的一行。此后我们又可以分别针对这两列各做一次排序,不难验证排序的结果分别是这样。

请再次的体会一下,经过这样的一趟逐列排序,整个序列的有序性又有所改善。与所有的步长序列一样,我们最终也是终止于W1等于1,也就是说我们要将此时的线性序列完整的视作为一列,并且对它进行排序。在经过了以上的各步之后,不出意外,我们的确得到了原序列的一个排序结果。

shell序列

我们在这里结识的第一个步长序列就出自于希尔排序的发明者Shell本人之手。我们接下来就会看到这个序列存在很多缺点。尽管从某种意义上来看它非常优美。因为我们注意到它的每一项都整齐划一的是2的k次方的形式,也就是说每一项都是前一项的两倍。那么这个序列的缺点就集中体现在它在最坏情况下可能会导致Ω(n^{2})的运行时间。

为此我们可以构造这样一个具体的实例.我们首先来考察两个整数区间也就是 [ 0,2^{n-1} ) 以及 [ 2^{n-1}2^{n} ) 。

其中包含的整数都是2^{n-1}个,只不过在数值上前者更小,而后者更大。

接下来我们将这两组整数分别的打乱次序并相应的构成两个字序列A以及B。然后我们按照ABAB交错的形式将它们会合为一个完整的序列。比如对于n=4而言,这就是一个可能的生成序列。

可以看到它是有两个规模都为8的子序列交错构成的,子序列A中的元素都被安置在秩为奇数的位置,而对称的子序列B中的元素都被安放在秩为偶数的位置。现在我们假设就采用希尔的序列来对它进行排序。

我们考察算法的倒数第二步也就是以二为间隔的那轮排序刚刚结束的时候,我们可以断言此时序列的组成必然是这样:

也就是说原先来自于子序列A中的那些元素依然占据着秩为奇数的位置,而且这八个元素的相对次序已经是完全有序的。同时对称的原先来自于子序列B中的那些元素也必然仍就占据着秩为偶数的那些位置。而且仅就这八个元素而言,它们之间的相对次序也已经是有序的。这两个字序列在这个时刻的有序性并不难理解:在刚刚过去的这一论排序中,这两个子序列恰好各自就是独立成为一列。因此所谓的2-sorting,其实就是对这两列分别进行排序。所以它们的结果自然应该是各自有序的。

当然刚才我们所指出的另一个现象更会引发我们的好奇,也就是说无论我们此前经历过多少趟的排序交换,来自于子序列A和子序列B中的元素始终都是分别占据着秩为奇数和偶数的位置,二者泾渭分明没有任何的元素互换。为此我们需要反观希尔序列,我们注意到在这个序列中除了第一项其余各项都是偶数。没错,偶数!这就意味着在这些项所对应的每一个重组的二维矩阵中同属一列的元素,或者都来自于集合A,或者都来自于B,自然不会发生A与B之间的元素互换了。因此直到执行完2-sorting之后,这两个序列必然都是井水不犯河水,互补相扰。

然而这恰恰正是问题所在!对于这样的序列,在接下来的最后一趟排序,也就是one-sorting中,我们必然需要付出高昂的代价。因为在这个序列中依然包含着大量的逆序对。我们不妨只统计B中的元素所参与构成的逆序对:首先是全局最大的15,它与其后的7就构成了一个逆序对。接下来再考察次大的14,不难看出它与6和7构成了两个逆序对。再接下来是13,它与5 6 7 总共构成了三个逆序对。以下类推元素12,将与4 5 6 7总共构成四个逆序对。

我想你已经看出其中的规律了。没错,B中的各元素所参与构成的逆序对数恰好构成一个算术级数,没错算术级数。我想经过这门课的学习,你现在应该有了一个直觉的反馈。是的这样一个算术级数对应的将是平方量级的运行成本,也就是说算法的效率已经退化为个与起泡排序相当了。当然在这里我们并不满足于仅仅指出希尔排序的缺点,而更重要的是,我们需要探究导致这种缺陷的根源让我们将目光再次投回到希尔序列。我们会发现与其说其中大量的元素都是偶数,不如更一般的说,其中的各项并非互素。

因此每一轮的排序都有大量的精力浪费于对前一抡排序工作的重复之上。是的,相邻项要尽可能的互素,这样我们也就拿到了打开新方法大门的钥匙。

邮资问题

假设在某个国家寄送一封平信需要五毛钱,而寄送一张明信片只需三毛五。进一步的我们假设在这个国家所发行的邮票只有面值为四分以及一毛三的两种。那么如果你需要邮寄一封平信或者明信片,你是否能够恰好用这两类邮票凑出所对应的邮资呢?

翻译成数学的语言,我们可以说,在这个国家用四分和一毛三面额的邮票所能凑出的邮资必然是4m+13n的形式,这一个由整数相乘然后累加而形成的表达式也称作线性组合linear combination。用数论的语言,以上问题可以描述位4m+13n=35是否存在自然数(非负整数)解?

当然我们也可以将以上的邮资问题推而广知。比如分别用g和h来代表两种邮票的面额,而用m和n分别代表这两类邮票所使用的数量。于是由m枚面额为g的邮票以及N枚面额为h邮票所共同构成的邮资,就可以表示为这样一个形式——也就是更一般意义上的linear combination。

不难理解通过线性组合,我们的确可以凑出不同的邮资。但是正如我们已经看到的,有些邮资却总是无法凑出。实际上我们更加关注于后一类的邮资。对于面额特定的两枚邮票,我们不妨将所有不能由他们凑出的邮资汇成一个集合并记作n(g,h)。我们最为关注的是这个集合中的最大(值),我们将其记作x(g,h)。那么对于任何一对互素的g和h,x又是多少呢?

那么对于任何一对互素的g和h,x又是多少呢?

在此我们直接引述数论中的现成结论:给定两个互素的正整数A和B,那么它们最大不能组合的数为A*B-A-B,不能组合的数的个数为num = (A - 1)*(B - 1) / 2 

数论告诉我们说,x(g,h)应该等于g减1与h减一的乘积再减一。

当然你可以化简为很多别的形式,比如说g和h的乘积再减去g和h的和。

我们不妨就刚才的实例来做一个验证:也就是说在g等于4h等于13时,按照这个定理,x应该等于4和13的乘积,再减去4和13的总和。不出意外恰好就是35,也就是刚才明信片所对应的邮资。

当然这个定理也告诉我们,从三毛六开始向上,所有的面额都是可以由这两种邮票凑出的,只要这两种邮票足够多。

定理K

回到排序问题,我们首先来引入h-sorting以及h-ordered的这两个概念。

在某个序列中如果任何一对距离为H的元素都保持前小后大的次序,我们就称它为h-ordered也就是以h为间隔是有序的。

当然作为其中的一个特例,在任何一个one ordered的序列中,根据定义任何一对相邻的元素彼此之间都是顺序的。你应该记得我们在最初介绍起泡排序算法时就指出,某个序列中只要任何相邻的元素之间都是彼此顺序的,那么必然就是整体有序的,所以我们说任何one-ordered的序列也必然是全局有序的,也就是我们排序算法最初需要输出的结果。

那么对于任何一个随机序列,如何使它变成是H-ordered的呢?实际上如下图所示,我们只需采用希尔排序的那种方法,将输入的(一维)序列在逻辑上转换为一个宽度为H的矩阵,然后分别的逐列排序。你应该记得将整个序列在逻辑上转化为一个宽度为H的矩阵,并且逐列进行排序,在希尔排序中就称为h-sorting。

由此我们可做一简洁的归纳,也就是任何一个序列在做过h-sorting之后必然是h-ordered。

你应该记得在希尔排序中,每向前迭代一步,对应的矩阵宽度都会相应的减少。比如从前一轮的g减少为后一轮的h,我们知道在经过前一轮的逐列排序之后,整个序列应该是g-ordered,而在后一轮的逐列排序之后,这个序列也自然的应该是h-ordered。

那么在整个序列达到以h为间隔有序的同时,此前以g为间隔的有序性是否能够依然得以延续呢?这个问题的答案并不是那么一目了然,所幸的是Knuth已经在他那本著名的专著中给出了正面的答案。

Knuth指出任何一个原先已是g-ordered的序列,在此后经过h-sorting之后依然保持是g-ordered。也就是说该向量既是g-有序,也是h-有序。相对于任何一个固定间隔而言的有序性在希尔排序的过程中将会不断的保持并且持续的积累下来。

逆序对

现在还是回到我们的线性组合,考察我们的待排序序列:如果它既是g-ordered,同时也是h-ordered,这样的序列也称作是g-h-ordered。实际上这样的序列也必然会以G和H之和为步长是有序的。我们可以通过这个图来简明的证明这一点:

在整个序列中考察任何一对相距g加上h的元素,我们说它们必然是顺序,因为我们可以找到居于它们中间的这样一个特定的元素,这个元素到前一元素的距离为g而到后一元素的距离为h,于是以这个中间元素为桥梁,根据不等式的单项传递性,我们自然就可以得知这一对间隔为g加h的元素也必然是顺序的。实际上这个结论也可以进一得推广到g和h的任何一个线性组合。我们仍然通过这组图来加以说明:

在这个序列中,我们考察任何一对间距为这个线性组合的元素。为了证明这一对元素之间的顺序性,我们无非是将刚才的方法推而广之。也就是说,我们需要引入更多的中间桥梁。比如这里给出的就是一种具体的方案。

我们首先以g为间隔连续的取出m个元素,在这些间隔位置上的所有元素必然是单调非降的。接下来我们在以h为间隔,连续的取出n个元素,同样的这n个元素也必然是单调非降的。于是通过所有这些单项不等号的串接,我们也就自然的证明了,这样一对元素之间的顺序性。

由以上我们可以概括出一个结论,也就是:凡是间距可以表示为线性组合的,任何一对元素必然是顺序的。简而言之,凡能表示为线性组合则必然顺序。

现在我们将注意力集中于序列中秩为i的那个元素。如果g和h是互素的而且整个序列已经是同时关于g和h有序的,那么相对于这个元素,哪些元素必然是顺序的?反过来哪些元素才有可能是逆序的?

我们说实际上可能逆序的元素不会很多。确切的讲无非就是刚才关于g和h的那个x的,也就是不能由g和h线性组合生成的那个最大的整数。你能看出其中的原因吗?

没错,对于这个区间之后的任何一个元素而言,它与 i 的距离已经超出了那个最大的预值x,所以它们之间的间隔也必然可以表示为g和h的线性组合。而我们刚刚总结过,凡是能够表示为线性组合的,也就是顺序。反过来这也就意味着,i这个元素所能参与构成的逆序对只可能出现与这样一个范围。而随着希尔排序的不断迭代,这个范围也会不断的缩小。没错,逆序列的总数会不断持续的减少。因此你能想到点什么吗?没错,插入排序。

我们知道,插入排序具有输入敏感性。它的实际运行时间将线性正比于序列中所含逆序对的总数。至此我们终于弄明白了,为什么在希尔排序的底层,我们更加倾向于使用插入排序算法。实际上按照以上的优化思路,后人针对希尔排序曾经涉及过许多更为优化的步长序列,比如:PS序列、Pratt序列以及Sedgewick序列。

 

参考:邓俊辉《数据结构/c++版》

  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值