从本质上讲,希尔排序是插入排序的升级版
插入排序的平均时间复杂度为O(n2),这个排序算法并不复杂,但是并不是一个高效的排序算法
我们可以针对于插入排序的两个特点进行优化:
1. 在大多数元素已经有序的情况下,插入排序的工作量较小
如果一个数组中的大部分元素已经有序,那么数组中的元素自然不需要频繁的进行比较和交换
2. 在元素数量较小的情况下,插入排序的工作量较小
插入排序的工作量和 n 的平方成正比,如果 n 较小,那么排序的工作量自然要小得多
初识希尔排序
希尔排序的思想就是:对原数组进行一些"预处理",使原数组大部分元素变得有序
如下原数组:
1. 让元素两两一组,同组两个元素之间的跨度,都是数组总长度的一半,也就是 4
如图所示,5 和 9 一组,8 和 2 一组,6 和 1 一组, 3 和 7 一组,接下来对每一组排序,排序方法直接使用插入排序,由于每一组元素数量很少,只有两个,所以插入排序的工作量很少,每组排序完成后的数组如下:
这样一来,数组整体的有序性得到显著提高,这样的做法可以理解为对原数组的"粗略调整"
2. 进一步缩小分组跨度,重复以上工作,把跨度缩小到原本的一半,也就是跨度为 2,重新对元素进行分组
如上所示,5、1、9、6 一组,2、3、8、7 一组,再次对每组进行插入排序,完成后原数组如下:
3. 进一步把分组跨度缩小,让跨度为 1,也就等同于直接做插入排序,经过一系列粗略调整,直接插入排序的工作量减少了很多,排序结果如下:
整个排序过程如下:
像这样初步分组进行初调,再直接进行插入排序的思想,就是希尔排序,根据该算法的发明者,计算机科学家 Donald Shell 的名字所命名
上边示例所使用的分组跨度(4,2,1),被称为希尔排序的增量,增量的选择有很多种,这里使用的逐步折半的方法,是在 Donald Shell 在发明希尔算法时提出的一种朴素方法,被称为希尔增量
代码实现
function sort(arr: Array<number>) {
let d = arr.length;
while(d > 1) {
d = Math.floor(d / 2);
for (let x = 0; x < d; x++) {
for (let index = x + d; index < arr.length; index+=d) {
let temp = arr[index];
let j = index - d;
for (; j >= 0 && arr[j] > temp; j-= d) {
arr[index] = arr[j];
}
arr[j+ d] = temp;
}
}
}
return arr
}
function main() {
const arr = [5, 3, 9, 12, 6, 1, 7, 2, 4, 11, 8, 10];
console.log(sort(arr))
}
存在问题及优化
问题:
希尔排序利用分组粗调的方式,减少了直接插入排序的工作量,使得算法平均复杂度低于O(n2)
但是在某些极端的情况下,希尔排序甚至比直接插入排序O(n2)更慢
示例:
例如这个数组,如果照搬之前的思路,无论是以 4 为增量,还是以 2 为增量,每组内部的元素都没有任何交换,直到把增量缩减为 1,数组才会按照直接插入排序的方式进行调整,对于这样的数组,希尔排序非但没有减少直接排序的工作量,反而白白增加了分组操作的成本
解决:
为了保证分组粗调没有盲区,每一轮的增量需要彼此"互质",也就是没有除了 1 之外的公约数
于是人们提出了很多增量方式,其中最具代表性的为 Hibbard增量 和 Sedgewick增量
Hibbard增量序列如下:
1,3,,7,15....
通项公式:2^k + 1
Sedgewick增量序列如下:
1,5,19,41....
通项公式:9*4^k - 9*2^k + 1 或者 4^k - 3*2^k + 1
希尔排序为不稳定排序:
示例如下:
原始数组:
按照希尔增量分组,第一轮粗调增量为 4,绿色 5 会和 4 交换,排到橙色 5 后边
第二轮(增量为2):
最终排序结果
摘要总结公众号:程序员小灰 文章