0. 原理
Shell排序是插入排序的变种,因D.L.Shell提出而得名。Shell排序对数据进行分组,然后对每一组数据进行排序,在每一组数据都有序之后再对所有分组利用插入排序进行最后一次排序,这样能减少数据交换次数,加快排序速度。
- 确定分组规则,一般根据排序数组长度每次折半,但是必须要保证最后一次分组的间隔为1
- 分完组后,就是对每一组内部进行排序
- 重复上述过程,直到进行完间隔为1(也就是插入排序)的排序。此时数组排序完成。
可能理解起来有点困难,这里给出一个演示例子:
对数组[54, 38, 65, 97, 19 13, 86 49, 56, 09]进行排序,每一趟分组规则为5, 3, 1:
- 第一趟分组:(54, 19),(38, 86),(65, 49),(97, 56),(19, 09)
- 第一趟排序:(19, 54),(38, 86),(49, 65),(56, 97),(09, 19)
- 则第一趟后结果为:[19, 38, 49, 56, 09, 54, 86, 49, 65, 97, 19]
- 第二趟分组:(19, 56, 86, 97),(38, 09, 49, 19),(49, 54, 65)
- 第二趟排序:(19, 56, 86, 97),(09, 19, 38, 49),(49, 54, 65)
- 则第二趟后结果为:[19, 09, 49, 56, 19, 54, 86, 38, 65, 97, 49]
- ……….
可以看到每一趟之后整个数组序列越来越趋向于有序。
1. 实现
@Override
public int[] sort(int[] data) {
if (data == null || data.length <= 1) {
return data;
}
if ((gap & 0x1) == 0) {
gap--;
}
for (int gap = data.length / 2; gap >= 1; gap /= 2) {
System.out.println("round gap=" + gap + " start");
for (int i = 0; i < gap; i++) {
for (int j = i + gap; j < data.length; j += gap) {
if (data[j] < data[j - gap]) {
for (int k = j; k >= 0 && k - gap >= 0; k -= gap) {
if (data[k] < data[k - gap]) {
swap(data, k, k - gap);
} else {
break;
}
}
}
}
}
System.out.println("round gap=" + gap + " finish");
}
return data;
}
实现起来有4个循环,最外层循环为分组循环,第二层为对数组进行分组的循环,第三层和第四层为模拟插入排序对分组后的数据进行排序的循环,第三和第四层可以看到和插入排序基本一样,只是j和k是根据gap(间距)来递增递减的。
有一个把循环层数减少的实现方法,但是可能没有贴出来的容易理解,具体实现也可以参考
2. 复杂度
希尔排序的时间是不稳定的,受分组的选择和数据的排列影响很大。
一个好的增量排序需要遵循以下原则:
- 最后一个增量必须为1
- 应尽量避免增量之间互为倍数,因此我在实现的时候增加了一个奇偶判断,如果为gap偶数,则减一使其成为奇数。
Shell排序的时间复杂度计算起来比较复杂,貌似也没有数学上对于Shell排序效率的结论,这里不做描述,有人通过大量的实验给出当n较大时,时间复杂度在O(n^(3/2))到O(n^(7/6))之间。
Shell排序在n特别大,也就是序列特别大并且十分无序时时间性能明显优于插入排序,原因如下:
- 大间隔的交换导致需要挪动的数据稀少,并且效率很高,最大的交换间隔可以达到n/2。
- 每次分组排序后数组就越来越趋于有序,数据离它正确的位置越来越近,这样下次的交换数据次数会明显减少。