【小小前端】前端排序算法第二期(绕人的希尔排序)

930 阅读5分钟

希尔排序(Shell Sort)

上回说到,三大基本排序冒泡排序、选择排序和插入排序。

其中插入排序又叫直接插入排序,其核心思想是通过构建有序序列,对未排序序列中选出首位数据,从已排序序列从后向前扫描,找到相应位置并插入。直接插入排序对小规模数据或基本有序数据十分高效。

希尔排序,1959年由Donald Shell发明,他是第一个突破O(n²)的排序算法。希尔排序是直接插入排序的改进版(你问我为什么不和直接插入排序写在一起?博主太笨了,希尔排序研究了两天才理解其中绕来绕去的循环--)。

希尔排序将序列分割成若干小序列(逻辑上分组),对每一个小序列进行插入排序,此时每一个小序列数据量小,插入排序的效率也提高了。

算法描述

  • 选择一个增量序列,t1,t2....tk,其中ti>tj,tk = 1;
  • 按增量序列个数k,对序列进行k次排序;
  • 每次排序,根据增量ti,将待排序序列分成若干子序列,分别对子序列进行直接插入排序。当增量为tk也就是1时,进行最后一次排序,此时子序列为排序序列本身。

动图演示

(好吧_(¦3」∠)_,这个图太抽象了,得多看12345678遍)

代码实现

先来看另一篇博客的运算图:

该图按下标距离为4进行分组,arr[0]和arr[4]为一组,arr[1]和arr[5]为一组,这里的下标距离4就被称为增量

对四个子序列进行插入排序之后:

此时四个子序列都是有序的,数组变为:

然后缩小增量为上个增量的一半:2,继续划分分组,此时,每个分组元素个数多了,但是,数组变的部分有序了,插入排序效率同样比较高

最后设置增量为上一个增量的一半:1,则整个数组被分为一组,此时,整个数组已经接近有序了:

如果看到这里你还不懂的话。。。。。。那你跟我一样笨,继续再看12345678遍就好了O(∩_∩)O。

    let arr = [3, 45, 16, 8, 65, 15, 36, 22, 19, 1, 96, 12, 56, 12, 45];
    let len = arr.length;
    let willInsertValue; 
    let gap = len; // 定义增量
    // 动态定义增量序列,每一次增量变为上次一半,最后一次的gap为1
    while(gap>0&&(gap = Math.trunc(gap/2))){
        // 对每个分组进行插入排序,为什么i开始是gap,因为插入排序默认第一位是已排序序列,arr[gap]是第一个分组第二位
        for(let i = gap;i<len;i++){
            // 待进行插入的值为a[i]
            willInsertValue = arr[i];
            // 按组进行插入,这里比较绕人,前面说了,只是逻辑上的分组,实际上还是一个序列,这里按组插入的时候是交叉
            // 进行插入
            let j = i - gap;
            // 下面就是个直接插入排序,只不过每次移位的时候下标差值为gap
            while(j>=0&&arr[j]>willInsertValue){
                arr[j+gap] = arr[j];
                j -= gap
            }
            arr[j+gap] = willInsertValue
        }
    }

输出结果为:

分析一下复杂度:

空间复杂度依然是O(1)

Shell排序的执行时间依赖于增量序列。

好的增量序列的共同特征:

  • 最后一个增量必须为1;
  • 应该尽量避免序列中的值(尤其是相邻的值)互为倍数的情况。

这样来看的话,上述栗子选择的增量1,2,4这样的其实并不是很好,使用这种增量序列时间复杂度最坏为O(n平方)。

Hibbard提出了另一个增量序列{1,3,7,...,2^k-1},这种序列的时间复杂度(最坏情形)为O(n^1.5)

Sedgewick提出了几种增量序列,其最坏情形运行时间为O(n^1.3),其中最好的一个序列是{1,5,19,41,109,...}

对于结果来说,使用哪种增量都没有影响,只要最后一次的增量变为1即可。上述栗子增量不能大于待排序序列的长度,否则gap为0,无法进行排序。

希尔排序又叫缩小增量排序。

最后说一下稳定性,由于希尔排序是交叉跳跃排序,所以是不稳定的排序。

总结

个人感觉在学习希尔排序的时候,难点在于最后交叉跳跃进行插入排序,前面说了是在逻辑上进行分组,思维完全被分组限制住了,总想着排序的时候也是按组排序,其实是以大序列的顺序对子序列进行跳跃式排序。

适用场景

希尔排序是对直接插入排序的一种优化,可以用于大型的数组,希尔排序比插入排序和选择排序要快的多,并且数组越大,优势越大。

参考

下期预告

【小小前端】前端排序算法第三期(不简单选择排序-堆排序)