思想导引
希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法,它是基于简单插入排序的改进版本,也称为递减增量排序算法。
在食用本文之前可以先简单了解一下插入排序的原理。
我们都知道插入排序的时间复杂度是O(n2)O(n^2)O(n2),比如对于一个长度为8的序列,它的时间复杂度对于插入排序来说就是O(82=64)O(8^2=64)O(82=64)。
但是需要注意的是实际上这是插入排序的最坏时间复杂度,如果该序列的原本顺序就已经很好时,比如12345678
,插入排序就会达到一个最好的时间复杂度即O(n)O(n)O(n)。因此当一个序列原本的顺序越工整时,它所需要的时间复杂度就会越小,接近于O(n)O(n)O(n)。
而如果将该序列先分为两组长度都为4的序列,然后分别排序,则时间复杂度可以变为O(42+42=32)O(4^2 + 4^2=32)O(42+42=32),接下来再对整个长度为8的序列进入插入排序,根据前面所讲,此时的时间复杂度绝对是小于O(82=64)O(8^2=64)O(82=64),甚至接近于O(n)O(n)O(n),因此总的时间复杂度加起来是可以小于O(n2)O(n^2)O(n2)的,注意是可以小于,如何确保能够小于还需要考虑以下问题。
首先要考虑,怎么分组呢?一种简单粗暴的方式就是切片分组,比如将这里第1个到第4个的元素分为一组,将第5个到第8个分为另外一组,这样的效果其实并不怎么好,比如如果某个后面的切片分组比前面的某个分组整体元素的值都要小,后面对整个序列插入排序的时候需要“挪动”的元素也越多,时间复杂度会急剧上升,具体解释略。而希尔采取的是增量分组,即1357为一组,2468为一组,这样做的好处就是搜索范围大,随机性更好。
其次就是每次分组的组数问题,组数在希尔排序中等于步长,也就是后面程序中的gap。是分成8组,然后4组,2组,1组这样依次除以2的方式,还是分成9,3,1依次除以3的方式呢?这篇博客中提到依次除以2的方式似乎跟简单插入排序的时间复杂度一样了,而依次除以3的方式似乎是可以的。因此如何优化增量序列也是希尔排序中的一个重点,具体这里就不展开了。
算法概要
网上也有比较有趣的图解示例,个人觉得维基百科上的就已经比较好理解了,摘抄如下。
假设有这样一组数[ 13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10 ],如果我们以步长为5开始进行排序,我们可以通过将这列表放在有5列的表中来更好地描述算法,这样他们就应该看起来是这样:
13 14 94 33 82
25 59 94 65 23
45 27 73 25 39
10
然后我们对每列进行排序:
10 14 73 25 23
13 27 94 33 39
25 59 94 65 82
45
将上述四行数字,依序接在一起时我们得到:[ 10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45 ].这时10已经移至正确位置了,然后再以3为步长进行排序:
10 14 73
25 23 13
27 94 33
39 25 59
94 65 82
45
排序之后变为:
10 14 13
25 23 33
27 25 59
39 65 73
45 94 82
94
最后以1步长进行排序就可以了。
程序描述
采用python语言描述如下:
ef shell_sort(arr):
'''希尔排序
'''
import math
gap = 1 # 步长
while(gap < len(arr)/3):
gap = gap*3+1
while gap > 0:
# 从第一组的第二个元素开始插入排序
for i in range(gap, len(arr)):
temp = arr[i]
j = i-gap
while j >= 0 and arr[j] > temp:
arr[j+gap] = arr[j]
j -= gap
arr[j+gap] = temp
# 将组数减少为3倍
gap = math.floor(gap/3)
return arr
需要注意的是这里的初始步长是待排序数组长度的三分之一。
另外程序中体现的并不是像上面一样的对每列分别插入排序,
13 14 94 33 82
25 59 94 65 23
45 27 73 25 39
10
即先插入第一列的25,45,10,然后插入第二列的59,27。为了实现的方便,这里程序是依次按照25,59,94,65,…即原本的数组顺序依次插入的。
Refs
十大经典排序算法(动图演示)
github
图解排序算法(二)之希尔排序
维基百科