排序算法(四)希尔排序

265 阅读5分钟

本文首发于我的个人博客

Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情

希尔排序是针对插入排序的一种改进。希尔排序主要是在开始排序之前,将数据进行粗略的排序,从而使得算法的平均时间复杂度低于 O(n2n^2)。但是在一些极端情况下,时间复杂度仍是 O(n2n^2),且比直接插入排序还要慢,是一种不稳定的排序方法。

核心思想

  • 首先设置一个增量 gap,并将数组按此 gap 进行分割。这里的 gap 可以理解为跨度,即根据 gap 设置分组,每组内的元素之间的下标距离为 gap 值。
    • 如:数组长度为20,gap 设为5,则将整个数组分割为5组,每组四个,组内每个元素的下标相差5。
  • 之后对每个区间内的数进行组内排序,这个排序使用插入排序即可。
  • 重复上面两步,并且每次重复时,步长是不断缩小的,直到步长为1。

示例

希尔排序的示例如下所示,示例信息来源于漫画:什么是希尔排序?

如何对原始数组进行预处理呢?聪明的科学家想到了一种分组排序的方法,以此对数组进行一定的“粗略调整”。

image.png 图片来源于漫画:什么是希尔排序?

所谓分组,就是让元素两两一组,同组两个元素之间的跨度,都是数组总长度的一半,也就是跨度为4。

image 图片来源于漫画:什么是希尔排序?

如图所示,元素5和元素9一组,元素8和元素2一组,元素6和元素1一组,元素3和元素7一组,一共4组。

接下来,我们让每组元素进行独立排序,排序方式用直接插入排序即可。由于每一组的元素数量很少,只有两个,所以插入排序的工作量很少。每组排序完成后的数组如下:

image.png (图片来源于漫画:什么是希尔排序?)

这样一来,仅仅经过几次简单的交换,数组整体的有序程度得到了显著提高,使得后续再进行直接插入排序的工作量大大减少。这种做法,可以理解为对原始数组的“粗略调整”。

但是这样还不算完,我们可以进一步缩小分组跨度,重复上述工作。把跨度缩小为原先的一半,也就是跨度为2,重新对元素进行分组:

image.png (图片来源于漫画:什么是希尔排序?)

如图所示,元素5,1,9,6一组,元素2,3,8,7一组,一共两组。

接下来,我们继续让每组元素进行独立排序,排序方式用直接插入排序即可。每组排序完成后的数组如下:

image.png (图片来源于漫画:什么是希尔排序?)

此时,数组的有序程度进一步提高,为后续将要进行的排序铺平了道路。

最后,我们把分组跨度进一步减小,让跨度为1,也就等同于做直接插入排序。经过之前的一系列粗略调整,直接插入排序的工作量减少了很多,排序结果如下:

image.png (图片来源于漫画:什么是希尔排序?)

让我们重新梳理一下分组排序的整个过程:

image.png (图片来源于漫画:什么是希尔排序?)

希尔排序的优化

希尔增量能高效率完成的关键点在于分组,即如何设置增量。所以,即使要对希尔排序进行优化,也都是集中在增量上。

设置增量的一个关键点就是互质。互质指得是元素互相置换,如果进行了多轮,都只有很少的元素进行了互质,实际上相当于希尔排序的关键点,分组进行粗略排序,没有起作用,效率并没有提高。

所以对增量优化的核心在于如何提高互质率,一个比较好的增量有如下要求:

  • 最后的增量一定是1。
  • 增量序列尽可能不互为倍数,尤其是相邻的。比如2, 4, 8, 16... 这种的增量。

增量设置方法

  • 希尔增量:是一种逐步折半的增量方法,即第一次的增量为 数组长度 / 2,之后每次的增量都是上一次的增量 /2, 一直到增量为1时,为止。虽然这个是常用的,并且是原装的,但是并不是一种好选择。

    • 时间复杂度:最好的空间复杂度为𝑂(𝑛^1$$^.$$^3),最坏的时间复杂度为𝑂(𝑛2𝑛^2)。
  • Hibbard增量:取法为 𝐷𝑘=2k^k−1。

    • 使用方法:需要首先计算出能够使用的 Hibbard 增量。k的初始值始终为1,k 不断自增,每次自增根据 k 计算增量。并且最后的增量值要小于数组长度的一半。最后依次从最后一位增量向前开始使用增量。
      • 如:长度为20,依次计算生成的增量分别是1, 3, 7,使用增量时则按7, 3, 1的顺序来使用。即第一次增量为7,第二次为3,第三次为1。
    • 时间复杂度:最坏时间复杂度为𝑂(n^3$$^/$$^2);平均时间复杂度约为𝑂(n^5$$^/$$^4)
  • Knuth 增量:取法为 hi = ( 3i^i − 1 ) / 2。

    • 使用方法:同Hibbard增量。也是k的初始值始终为1,k 不断自增,每次自增根据 k 计算增量并取对应的值。
    • 简化版公式为:初始增量为1,每次计算设上次的增量为x。直接使用 3x + 1 即可。
      • 如:第一个增量是1,第二个为 3 * 1 + 1,第三个是 3 * 4 + 1,第四个是3 * 13 + 1,依次类推。
    • 时间复杂度:𝑂(n^3$$^/$$^2)。

Code

function sortArray(nums: number[]): number[] {
  const len: number = nums.length;
  let gapArray: number[] = [1];

  while (1) {
    const gap: number = 3 * gapArray[gapArray.length - 1] + 1;
    if ( gap < len / 2 ) {
      gapArray.push( gap );
    }else {
      break
    }
  }
  
  for (let gapIdx: number = gapArray.length - 1; gapIdx >= 0; -- gapIdx) {
    const gap: number = gapArray[gapIdx];
    let startIdx: number = 0
    for (let idx = startIdx + gap; idx < len; idx += gap ) {

      let nowIdx: number = idx;
      while (nowIdx - gap >= 0 && nums[nowIdx] < nums[nowIdx - gap]) {
        const tmp: number = nums[nowIdx];
        nums[nowIdx] = nums[nowIdx - gap];
        nums[nowIdx - gap] = tmp;
        nowIdx = nowIdx - gap
      }
      ++ startIdx
    }
  }

  return nums
};

复杂度分析

时间复杂度:𝑂(n^3$$^/$$^2)。

空间复杂度:O( 1 )。