如何更好地理解希尔排序算法
Posted Python算法之旅
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何更好地理解希尔排序算法相关的知识,希望对你有一定的参考价值。
希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破O(n^2)的第一批算法之一。
为什么希尔排序算法的效率会比简单插入排序高?如何理解希尔排序的算法思想?要解决这两个问题,需要先理解简单插入排序算法和“跳跃式插入”的含义。
首先我们来看简单插入排序的算法思想:从第2个元素开始,每趟将一个待排序的元素,按其大小插入到前面已排序序列的适当位置上去,直到元素全部插入为止。对应VB 代码如下(之所以写成这种形式是为了避免数组下标越界):
For i = 2 To n
t = a(i): j = i
Do While j > 1
If a(j - 1) > t Then
a(j) = a(j - 1)
j = j - 1
Else
Exit Do
End If
Loop
a(j) = t
Next i
接下来我们再解决一个看似不相干的问题:分别对数组a的奇数和偶数位置的元素进行冒泡排序,即采用“跳跃式冒泡”的方法,每次跳跃的步长为2,将数组分成2个子序列,分别对这2个子序列进行排序。
例如,考虑对数组a = [6, 3, 5, 4, 1, 2, 8, 7]进行跳跃式冒泡排序,排序后的数组a = [1, 2, 5, 3, 6, 4, 8, 7]。
对应代码如下:
For i = 1 To (n - 1) \ 2 '冒泡趟数
For j = n To i * 2 + 1 Step -1
If a(j) < a(j - 2) Then '跳跃式交换,跳跃距离为2
t = a(j): a(j) = a(j - 2): a(j - 2) = t
End If
Next j
Next i
除了使用跳跃式冒泡排序算法,我们也可以使用跳跃式插入排序算法来实现相同的功能,对应代码如下:
For i = 3 To n
t = a(i): j = i
Do While j > 2
If a(j - 2) > t Then '跳跃式移动,跳跃距离为2
a(j) = a(j - 2)
j = j - 2
Else
Exit Do
End If
Loop
a(j) = t
Next i
进一步思考:我们可以将步长设置为m,即把数组分成m个子序列,分别对这m个子序列进行插入排序。例如,考虑对数组a = [6, 3, 5, 4, 1, 2, 8, 7]进行跳跃式插入排序。
当m=1时,排序后的数组a = [1, 2, 3, 4, 5, 6, 7, 8];
当m=2时,排序后的数组a = [1, 2, 5, 3, 6, 4, 8, 7];
当m=3时,排序后的数组a = [4, 1, 2, 6, 3, 5, 8, 7];
当m=4时,排序后的数组a = [1, 2, 5, 4, 6, 3, 8, 7];
当m=5时,排序后的数组a = [2, 3, 5, 4, 1, 6, 8, 7]。
实现该功能的代码如下:
For i = m + 1 To n '将相隔为m的元素组成一个子序列,分别对各组进行插入排序
t = a(i): j = i
Do While j > m
If a(j - m) > t Then '跳跃式移动,跳跃距离为m
a(j) = a(j - m)
j = j - m
Else
Exit Do
End If
Loop
a(j) = t
Next i
看到这里,相信您已经明白什么是“跳跃式插入”了。
实践表明插入排序在对“基本有序”的数据操作时,效率非常高,都快赶上线性排序的效率了(所谓“基本有序”是指待排序的数组逆序数比较少)。但进行插入操作时,每次只能将数据移动一位,难免出现大量重复移动。如果将元素尽可能快地移动到它“该去”的地方,肯定会减少重复移动的次数,提高效率。
我们可以先把全部元素分组排序,将所有距离为步长m的元素放在同一个组中,通过“跳跃式移动”的方法,能让元素每次移动一大步,即步长m>1,大大提高了移动的效率。一趟排序下来,虽然同组的元素没有挨在一起,各组元素相互隔开,但是由于每一组都已经各自排好序了,所以整个序列的“有序性”还是变好了。
我们来看一个例子:
图1是原始序列,序列长度n=8,我们先取步长m=n\2=4,把序列分成4组(容易看出来,分组数量和步长相等)。横线下方的序号表示每个元素的下标,用1-8表示;矩形中的数字表示元素大小,矩形颜色相同表示它们在同一组,此时分组情况为(1,5),(2,6),(3,7),(4,8)。
我们对同组的元素进行简单插入排序,结果如图2所示。一趟排序下来,虽然同组的元素没有挨在一起,各组元素相互隔开,但是由于每一组都已经各自排好序了,所以整个序列看上去还是比以前要“有序”些,即逆序数要少一些。
接下来我们取更小的步长m=2,把序列分成2组,此时分组情况为(1,3,5,7),(2,4,6,8)。各元素位置和分组情况如图3所示:
我们对同组的元素进行简单插入排序,结果如图4所示。很明显,现在逆序数又少了很多,整个序列变得更加“有序”了。
同样的,我们继续减少步长,取m=1,把序列分成1组,此时各元素位置和分组情况如图5所示:
现在步长m=1,就是普通的插入排序。现在整个序列已经是“基本有序”了,直接插入排序也能高效完成。排序结果如图6所示:
通过上面的例子我们可以看到,将相隔一定步长的元素组成一个子序列,分别对他们进行插入排序,可以实现跳跃式的移动,使得排序的效率提高。经过一轮分组排序后,虽然整个序列还是无序的,但每个相互隔开的子序列已经变得有序,总的逆序数减少,整个序列变得更加“有序”了。每完成一轮分组排序后,我们就将步长减半,继续分组排序,直至步长m=1,就变成普通的插入排序了。由于此时整个序列已经“基本有序”了,直接插入排序也能高效完成。
这种另类的插入排序算法就是传说中的“希尔排序”。用VB语言实现代码如下:
m = n \ 2
Do While m > 0
For i = m + 1 To n '将相隔为m的元素组成一个子序列,分别对各组进行插入排序
t = a(i): j = i
Do While j > m '跳跃式移动,跳跃距离为m
If a(j - m) > t Then
a(j) = a(j - m)
j = j - m
Else
Exit Do
End If
Loop
a(j) = t
Next i
m = m \ 2 '步长m每次减半
Loop
希尔排序的关键在于不能将元素随便分组后各自排序,而是将相隔一定步长的元素组成一个子序列,实现跳跃式的移动,使得排序的效率提高。
步长的选择是希尔排序的重要部分。前面的算法中,我们让步长m每次减半,这是最简单的方法,但不是唯一的方法。事实上只要满足让步长逐渐减小,最终步长为1的规则,无论采用何种递减规律都可以。算法最开始以某个步长进行排序,然后会继续以更小的步长排序,最终算法以步长为1排序。当步长为1时,算法变为插入排序,这就保证了数据一定会被排序。
Donald Shell 最初建议选择步长m=n\2,然后让步长m每次减半,直到步长m=1。虽然这样取可以比O(n^2)类的算法(插入排序)更好,但仍然有减少平均时间和最差时间的余地。可能希尔排序最重要的地方在于当用较小步长排序后,以前用的较大步长仍然是有序的。比如,如果一个数列以步长5进行了排序然后再以步长3进行排序,那么该数列不仅是以步长3有序,而且是以步长5有序。如果不是这样,那么算法在迭代过程中会打乱以前的顺序,那就不会以如此短的时间完成排序了。
已知的最好步长序列是由Sedgewick提出的 (1, 5, 19, 41, 109,...),该序列的项来自 9 * 4^i - 9 * 2^i + 1 和 4^i - 3 * 2^i + 1 这两个算式。这项研究也表明在希尔排序中比较是最主要的操作,而不是交换。用这种步长序列的希尔排序比插入排序和堆排序都要快,甚至在小数组中比快速排序还快,但是在涉及大量数据时希尔排序还是比快速排序慢。
另一个在大数组中表现优异的步长序列是(斐波那契数列除去0和1将剩余的数以黄金分区比的两倍的幂进行运算得到的数列):(1, 9, 34,182, 836, 4025, 19001, 90358, 428481, 2034035, 9651787, 45806244, 217378076,1031612713, …)。
需要本文word版的,可以加入“选考VB算法解析”知识星球参与讨论和下载文件,“选考VB算法解析”知识星球汇集了数量众多的同好,更多有趣的话题在这里讨论,更多有用的资料在这里分享。
我们专注选考VB算法,感兴趣就一起来!
相关优秀文章:
以上是关于如何更好地理解希尔排序算法的主要内容,如果未能解决你的问题,请参考以下文章