如何更好地理解希尔排序算法

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算法,感兴趣就一起来!


相关优秀文章:

以上是关于如何更好地理解希尔排序算法的主要内容,如果未能解决你的问题,请参考以下文章

五分钟学会一个高难度算法:希尔排序

计算机算法——希尔排序

排序算法专题之希尔排序

C++ 不知算法系列之聊聊希尔归并排序算法中的分治哲学

理解插入排序,希尔排序,选择排序的算法原理

14-看图理解数据结构与算法系列(希尔排序)