四大排序之希尔排序

1.1 思路

希尔排序是一种创新的排序算法,它的名字来源于它的发明者Donald Shell,1959年,希尔排序算法诞生了。

  • 在简单排序算法诞生后的很长一段时间内,人们不断尝试发明各种各样的排序算法,但是当时的排序算法的时间复杂度都是O(N²),看起来很难超越。
  • 当时计算机学术界充满了"排序算法不可能突破O(N²)"的声音,这与人类100米短跑不可能突破10秒大关的想法一样。
  • 但是,终于有一天,一位科学家发布了一种超越O(N²)的新排序算法,并以Shell的名字命名,以纪念这个历史里程碑。
  • 随后,还出现了许多可以超越O(N²)的排序算法,希尔排序作为其中之一,也是一种重要的排序算法。

希尔排序(Shell sort)是插入排序的一种改进版本,通过使用更大的步长来减少待排序列表中的元素,并最终通过使用步长为 1 的插入排序将数据有序化。

希尔排序的时间复杂度较为复杂,但是一般被认为是O(n^(3/2))或者O(n^(4/3))。

与其他排序算法相比,希尔排序具有较快的初始速度,但其最坏时间复杂度与插入排序相同。

1.2 希尔排序流程

回顾插入排序:

  • 由于希尔排序基于插入排序, 所以有必须回顾一下前面的插入排序.
  • 我们设想一下, 在插入排序执行到一半的时候, 标记符左边这部分数据项都是排好序的, 而标识符右边的数据项是没有排序的.
  • 这个时候, 取出指向的那个数据项, 把它存储在一个临时变量中, 接着, 从刚刚移除的位置左边第一个单元开始, 每次把有序的数据项向右移动一个单元, 直到存储在临时变量中的数据项可以成功插入.

插入排序的问题:

  • 假设一个很小的数据项在很靠近右端的位置上, 这里本来应该是较大的数据项的位置.
  • 把这个小数据项移动到左边的正确位置, 所有的中间数据项都必须向右移动一位.
  • 如果每个步骤对数据项都进行N次复制, 平均下来是移动N/2, N个元素就是 N*N/2 = N²/2.
  • 所以我们通常认为插入排序的效率是O(N²)
  • 如果有某种方式, 不需要一个个移动所有中间的数据项, 就能把较小的数据项移动到左边, 那么这个算法的执行效率就会有很大的改进.

希尔排序的做法:

  • 比如下面的数字, 81, 94, 11, 96, 12, 35, 17, 95, 28, 58, 41, 75, 15.
  • 我们先让间隔为5, 进行排序. (35, 81), (94, 17), (11, 95), (96, 28), (12, 58), (35, 41), (17, 75), (95, 15)
  • 排序后的新序列, 一定可以让数字离自己的正确位置更近一步.
  • 我们再让间隔位3, 进行排序. (35, 28, 75, 58, 95), (17, 12, 15, 81), (11, 41, 96, 94)
  • 排序后的新序列, 一定可以让数字离自己的正确位置又近了一步.
  • 最后, 我们让间隔为1, 也就是正确的插入排序. 这个时候数字都离自己的位置更近, 那么需要复制的次数一定会减少很多.

选择合适的增量:

  • 在希尔排序的原稿中, 他建议的初始间距是N / 2, 简单的把每趟排序分成两半.
  • 也就是说, 对于N = 100的数组, 增量间隔序列为: 50, 25, 12, 6, 3, 1.
  • 这个方法的好处是不需要在开始排序前为找合适的增量而进行任何的计算.
  • 我们先按照这个增量来实现我们的代码.

1.3 希尔排序代码

// TypeScript代码实现希尔排序
function shellSort(arr: number[]) {
  // 定义增量, 每次分组, 增量为数组长度的一半
  let gap = Math.floor(arr.length / 2);
  while (gap > 0) {
    // 按组进行排序
    for (let i = gap; i < arr.length; i++) {
      // 获取当前元素
      let current = arr[i];
      let j = i;
      // 将相邻元素比较, 满足条件就后移
      while (j >= gap && arr[j - gap] > current) {
        arr[j] = arr[j - gap];
        j -= gap;
      }
      // 将当前元素插入合适的位置
      arr[j] = current;
    }
    // 每次递减增量, 直到为1
    gap = Math.floor(gap / 2);
  }
  return arr;
}

整个代码实现了快速排序的算法流程:

  • heapSort 函数是堆排序的主体函数,使用大根堆实现从小到大的排序
  • adjustHeap 函数是堆调整函数,用来调整大根堆,以保证堆顶的数是整个堆中最大的数整个代码实现了希尔排序的算法流程:
  • 整段代码为一个函数,函数名为"shellSort",参数为待排序的数组"arr"。
  • 从最外层循环开始,设置gap的值为数组长度的一半,每次循环缩小gap的值,直到gap的值为1。
  • 在gap的控制下,进行插入排序,在gap的间隔内进行循环,在循环内部,比较相邻两个数的大小,若左边数大于右边数,则交换位置。
  • 整段代码最后一行,返回排好序的数组。

1.4 希尔排序的时间复杂度

希尔排序的效率

  • 希尔排序的效率很增量是有关系的.
  • 但是, 它的效率证明非常困难, 甚至某些增量的效率到目前依然没有被证明出来.
  • 但是经过统计, 希尔排序使用原始增量, 最坏的情况下时间复杂度为O(N²), 通常情况下都要好于O(N²)

Hibbard 增量序列

  • 增量的算法为2^k - 1. 也就是为1 3 5 7...等等.
  • 这种增量的最坏复杂度为O(N^3/2), 猜想的平均复杂度为O(N^5/4), 目前尚未被证明.

Sedgewick增量序列

  • {1, 5, 19, 41, 109, … }, 该序列中的项或者是94^i - 9*2^i + 1或者是4^i - 32^i + 1
  • 这种增量的最坏复杂度为O(N^4/3), 平均复杂度为O(N^7/6), 但是均未被证明.

总之, 我们使用希尔排序大多数情况下效率都高于简单排序, 甚至在合适的增量和N的情况下, 还好好于快速排序.

1.5 希尔排序小结

希尔排序是一种改进版的插入排序,从历史的角度来看,它是一种非常非常重要的排序算法,因为它解除了人们对原有排序的固有认知。

但是现在已经有很多更加优秀的排序算法:归并排序、快速排序等,所以从实际的应用角度来说,希尔排序已经使用的非常非常少了。

因为,我们只需要了解其核心思想即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值