一、算法思想
希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破O(n2)的第一批算法之一。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
简单插入排序很循规蹈矩,不管数组分布是怎么样的,依然一步一步的对元素进行比较,移动,插入,比如[5,4,3,2,1,0]这种倒序序列,数组末端的0要回到首位置很是费劲,比较和移动元素均需n-1次。而希尔排序在数组中采用跳跃式分组的策略,通过某个增量将数组元素划分为若干组,然后分组进行插入排序,随后逐步缩小增量,继续按组进行插入排序操作,直至增量为1。希尔排序通过这种策略使得整个数组在初始阶段达到从宏观上看基本有序,小的基本在前,大的基本在后。然后缩小增量,到增量为1时,其实多数情况下只需微调即可,不会涉及过多的数据移动。
二、算法流程
我们选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2...1},称为增量序列。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处我们做示例使用希尔增量。
三、算法实现
1 //希尔排序
2 static class ShellSort implements Sort {
3 @Override
4 public String sortName() {
5 return "希尔排序";
6 }
7 @Override
8 public Comparable[] sort(Comparable[] data) {
9 int gap = 1;
10 //确定初始增量
11 while(gap < data.length / 2) {
12 gap = 2 * gap + 1;
13 }
14 //逐步二分缩小排序增量
15 while(gap >= 1) {
16 //从中间位置开始向后逐步分组
17 for(int i = gap; i < data.length; i++) {
18 //组内进行插入排序
19 for(int j = i; j >= gap; j -= gap) {
20 if(Sort.greater(data[j - gap], data[j])) {
21 Sort.swap(data, j - gap, j);
22 } else {
23 break;
24 }
25 }
26 }
27 //缩小gap
28 gap >>>= 1;
29 }
30 return data;
31 }
32 }
四、算法分析
复杂度
希尔排序的时间复杂度受增量序列影响,不同的增量序列会有不同时间复杂度:N 为数组长度,K 为当前增量
- 当增量序列 时,时间复杂度为
- 当增量序列 时,时间复杂度为
当数组长度为 N,增量序列为 时,希尔排序的时间复杂度 计算如下:
- 因为增量为 时,子数组个数为 (假设 ),每个子数组的元素个数为 ,每个子数组都进行一次插入排序,时间复杂度为 ,所以每个增量所需时间复杂度为
- 因为 ,所以 的最大值为 ,即会有 个增量,进行 次插入排序
- 所以,整个希尔排序的时间复杂度为 ,最后解得
稳定性
希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小, 插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比O(n^2)好一些。由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。