对希尔排序的理解——如何从插入排序进化到希尔排序?

在学习希尔排序的过程中,发现很多博客只在讲希尔排序是什么,没有解释希尔排序是怎么设计的,为什么要使用增量。
在开始前,我们要先强调一下,希尔排序的时间复杂度并不固定,它依赖于增量序列的选择。在最坏的情况下,希尔排序的时间复杂度为O(n^2),但是对于某些特定的增量序列,其时间复杂度可以降低到O(n^1.5)或更低,时间复杂度的计算详见参考资料一。

希尔排序介绍

希尔排序通过将数组分成多个小组,对每组分别进行插入排序,然后逐步减少分组的数量(缩小增量),最终让所有数据处于“基本有序”的状态,在最后一轮分组中(当分组间隔为 1 时),直接使用插入排序完成最终的排序。

1.1 初始分组:大间隔排序

假设我们有一个数组 [8, 6, 3, 7, 2, 5, 1, 4],长度为 8,使用初始间隔gap = n /2 = 4:
按照间隔为 4,将数组分为 4 组:

  • 第 1 组:[8, 2](索引 0 和 4)
  • 第 2 组:[6, 5](索引 1 和 5)
  • 第 3 组:[3, 1](索引 2 和 6)
  • 第 4 组:[7, 4](索引 3 和 7)
    对每组分别进行插入排序:
  • 第 1 组:[8, 2] → 排序后 [2, 8]
  • 第 2 组:[6, 5] → 排序后 [5, 6]
  • 第 3 组:[3, 1] → 排序后 [1, 3]
  • 第 4 组:[7, 4] → 排序后 [4, 7]
    排序后的数组:[2, 5, 1, 4, 8, 6, 3, 7]。
    通过这种大间隔分组和排序,数组中元素已经开始向正确的位置移动,较大的元素逐渐向后移动,较小的元素逐渐向前移动。

1.2 减小排序间隔

gap=gap/2=2,重新分组:

  • 第 1 组:[2, 1, 8, 3](索引 0、2、4、6)
  • 第 2 组:[5, 4, 6, 7](索引 1、3、5、7)
    对每组分别进行插入排序:
  • 第 1 组:[2, 1, 8, 3] → 排序后 [1, 2, 3, 8]
  • 第 2 组:[5, 4, 6, 7] → 排序后 [4, 5, 6, 7]
    排序后的数组:[1, 4, 2, 5, 3, 6, 8, 7]。

1.3 直至间隔为1进行插入排序

最后一轮,将间隔缩小到gap = 1,即对整个数组进行插入排序:

  • 当前数组:[1, 4, 2, 5, 3, 6, 8, 7]
    直接进行插入排序,最终得到有序数组:[1, 2, 3, 4, 5, 6, 7, 8]。

为什么使用希尔排序

我们已经学习了插入排序,插入排序的思想就是将未排序的数插入到一个有序集合,直到完成排序。插入排序的时间复杂度是O(n^2),每一个未排序的数要和有序集合中的数依次比较确定自己的相对位置。那么,一个自然的想法就是,如果在进行插入排序之前,数据已经是大致排好的,那么插入排序的内循环就能减少,时间复杂度也能减少,由此我们产生了分组的想法。如何分组是一个问题,我们可以像二分查找那样分成左右两个子分组,但是这会带来一个问题,左右两侧的元素永远不会互相比较,那么我们确定了分组的第一个必要前提:分组是会交叉的,尽可能使得每一个元素都有和其它元素比较的机会

其次,对于插入排序而言,较小的元素尽量在前面可以减少最后插入排序时比较的次数,且尽量在组内元素较少时就将最近和最远的元素分在一组,这样能减少比较次数。因此,另一个分组原则便是:组内元素要尽量包含最近和最远(靠后)的元素。

在以上两个原则的驱动下,有了我们现在看到的希尔排序的增量设计,例如最简单的gap = gap /2,亦或者是最流行的gap = gap / 3,以上就是我的个人理解。

代码讲解

template<typename T>
void shell_sort(T array[], int length) {
    int h = 1;
    while (h < length / 3) {
        h = 3 * h + 1;
    }
    while (h >= 1) {
        for (int i = h; i < length; i++) {
            for (int j = i; j >= h && array[j] < array[j - h]; j -= h) {
                std::swap(array[j], array[j - h]);
            }
        }
        h = h / 3;
    }
}
  • 第一层循环:增量逐渐缩小,分组数量也在逐渐减小。
  • 第二层循环:每一次确定好增量之后,从第一个分组的第二个元素开始进行插入排序,此后每一个元素都与自己分组内的元素进行插入排序。
  • 第三层循环:当前元素array[j]是未排序的元素,当前元素的前一个元素array[j-h]是已经排序的集合,当前元素开始依次比较,比前面小则交换。

时间复杂度

时间复杂度很难计算,如果想了解怎么计算的可以看参考资料一:内循环和外循环的时间复杂度拆解开。

参考资料

posted @ 2024-12-19 14:49  ZCry  阅读(113)  评论(0)    收藏  举报