本文首发于我的个人博客
Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情。
希尔排序是针对插入排序的一种改进。希尔排序主要是在开始排序之前,将数据进行粗略的排序,从而使得算法的平均时间复杂度低于 O()。但是在一些极端情况下,时间复杂度仍是 O(),且比直接插入排序还要慢,是一种不稳定的排序方法。
核心思想
- 首先设置一个增量 gap,并将数组按此 gap 进行分割。这里的 gap 可以理解为跨度,即根据 gap 设置分组,每组内的元素之间的下标距离为 gap 值。
- 如:数组长度为20,gap 设为5,则将整个数组分割为5组,每组四个,组内每个元素的下标相差5。
- 之后对每个区间内的数进行组内排序,这个排序使用插入排序即可。
- 重复上面两步,并且每次重复时,步长是不断缩小的,直到步长为1。
示例
希尔排序的示例如下所示,示例信息来源于漫画:什么是希尔排序?:
如何对原始数组进行预处理呢?聪明的科学家想到了一种分组排序的方法,以此对数组进行一定的“粗略调整”。
图片来源于漫画:什么是希尔排序?
所谓分组,就是让元素两两一组,同组两个元素之间的跨度,都是数组总长度的一半,也就是跨度为4。
图片来源于漫画:什么是希尔排序?
如图所示,元素5和元素9一组,元素8和元素2一组,元素6和元素1一组,元素3和元素7一组,一共4组。
接下来,我们让每组元素进行独立排序,排序方式用直接插入排序即可。由于每一组的元素数量很少,只有两个,所以插入排序的工作量很少。每组排序完成后的数组如下:
(图片来源于漫画:什么是希尔排序?)
这样一来,仅仅经过几次简单的交换,数组整体的有序程度得到了显著提高,使得后续再进行直接插入排序的工作量大大减少。这种做法,可以理解为对原始数组的“粗略调整”。
但是这样还不算完,我们可以进一步缩小分组跨度,重复上述工作。把跨度缩小为原先的一半,也就是跨度为2,重新对元素进行分组:
(图片来源于漫画:什么是希尔排序?)
如图所示,元素5,1,9,6一组,元素2,3,8,7一组,一共两组。
接下来,我们继续让每组元素进行独立排序,排序方式用直接插入排序即可。每组排序完成后的数组如下:
(图片来源于漫画:什么是希尔排序?)
此时,数组的有序程度进一步提高,为后续将要进行的排序铺平了道路。
最后,我们把分组跨度进一步减小,让跨度为1,也就等同于做直接插入排序。经过之前的一系列粗略调整,直接插入排序的工作量减少了很多,排序结果如下:
(图片来源于漫画:什么是希尔排序?)
让我们重新梳理一下分组排序的整个过程:
(图片来源于漫画:什么是希尔排序?)
希尔排序的优化
希尔增量能高效率完成的关键点在于分组,即如何设置增量。所以,即使要对希尔排序进行优化,也都是集中在增量上。
设置增量的一个关键点就是互质。互质指得是元素互相置换,如果进行了多轮,都只有很少的元素进行了互质,实际上相当于希尔排序的关键点,分组进行粗略排序,没有起作用,效率并没有提高。
所以对增量优化的核心在于如何提高互质率,一个比较好的增量有如下要求:
- 最后的增量一定是1。
- 增量序列尽可能不互为倍数,尤其是相邻的。比如
2, 4, 8, 16...这种的增量。
增量设置方法
-
希尔增量:是一种逐步折半的增量方法,即第一次的增量为
数组长度 / 2,之后每次的增量都是上一次的增量 /2, 一直到增量为1时,为止。虽然这个是常用的,并且是原装的,但是并不是一种好选择。- 时间复杂度:最好的空间复杂度为𝑂(𝑛^1$$^.$$^3),最坏的时间复杂度为𝑂()。
-
Hibbard增量:取法为 𝐷𝑘=2−1。
- 使用方法:需要首先计算出能够使用的 Hibbard 增量。k的初始值始终为1,k 不断自增,每次自增根据 k 计算增量。并且最后的增量值要小于数组长度的一半。最后依次从最后一位增量向前开始使用增量。
- 如:长度为20,依次计算生成的增量分别是
1, 3, 7,使用增量时则按7, 3, 1的顺序来使用。即第一次增量为7,第二次为3,第三次为1。
- 如:长度为20,依次计算生成的增量分别是
- 时间复杂度:最坏时间复杂度为𝑂(n^3$$^/$$^2);平均时间复杂度约为𝑂(n^5$$^/$$^4)
- 使用方法:需要首先计算出能够使用的 Hibbard 增量。k的初始值始终为1,k 不断自增,每次自增根据 k 计算增量。并且最后的增量值要小于数组长度的一半。最后依次从最后一位增量向前开始使用增量。
-
Knuth 增量:取法为 hi = ( 3 − 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 )。