希尔排序(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,无法进行排序。
希尔排序又叫缩小增量排序。
最后说一下稳定性,由于希尔排序是交叉跳跃排序,所以是不稳定的排序。
总结
个人感觉在学习希尔排序的时候,难点在于最后交叉跳跃进行插入排序,前面说了是在逻辑上进行分组,思维完全被分组限制住了,总想着排序的时候也是按组排序,其实是以大序列的顺序对子序列进行跳跃式排序。
适用场景
希尔排序是对直接插入排序的一种优化,可以用于大型的数组,希尔排序比插入排序和选择排序要快的多,并且数组越大,优势越大。