小学生图解排序算法:④希尔排序

希尔排序

含义:希尔排序(Shell Sort)是插入排序的一种。也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因DL.Shell于1959年提出而得名。

原理: 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。

                                                        ——来源于百度百科

算法特点

以从小到大排列为例。

希尔排序算法使用直接插入排序算法来排序,但其改进之处在于,直接插入排序算法是由插入值与其前面的记录依次比对,直到终止条件出现(两个终止条件见前一篇:直接插入排序算法),其每次往前移的步长为1。

希尔排序则以一定步长为增量对要排序的数据源进行分组,在各分组内采用直接插入排序算法对各组进行排序,排序后再对数据源进行更小步长的分组排序,不断重复直到步长为1时,再次采用直接插入排序即可完成排序。


图解算法

假定有数组a[n],长度n=15。如果是采用直接插入排序算法,那么直接把第2-15位依次与插入位前1、2、3……位对比插入到合适位置即可。如果对数组从小到大排列,而一个较小的数在末尾,则该数要与前面一一比对很多次才行。显然,这种效率太低。

那么,希尔算法怎么做呢?以一个例子来说明。对图片中长度n=15的随机无序数组进行从小到大排序。

如果图片被缩小,请点击鼠标右键—在新标签中打开图片

图解说明

  • 第一局中,步长间隔gap= n / f = 7(n=15为数组长度,f=2 为折叠基数,其含义在文末说明,此环节先略过不作说明),使源数组的元素分组成了7组,各组分别采用直接插入排序算法进行排序。参与构成各组的下标分别为:

    1. 第1组:a[0], a[7], a[14];
    2. 第2组:a[1], a[8];
    3. 第3组:a[2], a[9];
    4. 第k组:a[k], a[k+7];

    在这步,各位应当已经看出了希尔排序的优势:它通过设定步长间隔,使得记录值可以通过较少的比对次数进行大范围的移位。例如例子中的a[14]=02,只需和a[0] a[7]比对,即将该值移到了首位。如果是整个数组不采用希尔,采用直接插入法,则其要从a[13]开始一一往前比对,这样的话比对次数可是多了不少。类似的还有3组、4组和7组中的大数,只比较了1次,就向后大幅度移动了。

    在第一局各小组内部比较和移位后,各小组中的小数放在了前面,大数移到了后面。而对于整个数组,我们观察发现,前段基本上是小数,后段基本为大数,有序性较佳,当然,还有少量数是无序的。

  • 第二局中,步长间隔gap为3,和第一局类似,分小组后进行组内排序,各元素移到合适位置后,整个数组的有序性更进一步。

  • 第三局中,步长间隔gap为1了,也就是所有元素参与排序,显然就是直接插入排序算法喽。因为经过前两局排序,数组基本有序,此时再使用直接插入排序算法,会比对随机无序的数组使用直接插入排序算法效率强上不少,原始数据量越多,提升越明显。

核心代码
上文多次提及希尔算法与直接插入排序算法的密切关系,显然可以猜到希尔的代码肯定与直接插入排序算法极其相似。因此在这里,我们不再像前三篇文章一样,事无巨细地分析各循环条件的临界点了。根据以上图解和说明,直接上关键代码。

public static void shellSort(int[] a){
   int i, j, temp, gap;
   int f = 2;//折叠基数f的取值有要求,见下文
   for(gap = a.length / f; gap > 0; gap /= f){
      for(i = gap; i < a.length; i++){
         temp = a[i];
         for(j = i - gap; j >= 0 && temp < a[j]; j -= gap){
            a[j + gap] = a[j];
         }
         a[j + gap] = temp;
      }
   }
}

折叠基数选择

每次计算的步长与折叠基数密切相关。如果选择错误,可能会导致希尔算法不起作用,或者实现不了希尔算法本该拥有的效率。

原则:最终步长间隔应该为1(这样才能保证在最后能够来一次直接插入排序,使全部排序完成,否则可能部分记录未能排序到正确位置)

如何根据上面的最终步长原则,选择折叠基数呢?
假定数据源长度为n,折叠数为f,每次的步长gap则为:

第一次 gap = n / f ;
第二次 gap = gap / f ;
第三次 gap = gap / f ;
……
如果出现 gap = 1,则该折叠数 f 可行;如果直到gap = 0,都没有出现gap=1,则该折叠数 f 不可行。

例如长度n=15,折叠数 f = 2,则:
第一次 gap = 15 / 2 = 7;
第二次 gap = 7 / 2 = 3 ;
第三次 gap = 3 / 2 = 1 ;
出现 gap = 1;说明折叠数 2 可行。

又如长度n=15,折叠数 f = 4,则:
第一次 gap = 15 / 4 = 3;
第二次 gap = 3 / 4 = 0 。
出现 gap = 0之前,没有出现gap = 1,说明折叠数 4 不可行。

另:折叠基数 f = 2对所有数组都可行。你猜为什么?


算法稳定性

前一篇说到直接插入排序算法是稳定的排序算法,而此文多处表示希尔排序与其密切相关,那么,希尔排序算法的稳定性如何呢?

其实在图解示例中,出现过2个相同的元素,分别是02和26。不过这次我并没有特别标注它们的前后相对位置在排序前和排序完成后是否有明显变化。

我们从希尔算法的特点出发来分析一下。

当n个相同的元素出现在数据源中时,通过一定的步长对数据源分组。相同的元素并不能保证在同一个小组中,当它们出现在不同的小组中时,便会各自与其小组中的元素比对及移位,则它们在源数据中的前后相对位置,可能会发生变化。因此,希尔排序算法是不稳定的算法。


说明

本文为个人学习笔记,如有细节错误或描述歧义,请留言告知,谢谢!
本文首发于博客专栏:http://Windows9.Win/shell_sort_algorithm

你可能感兴趣的:(算法Algorithm)