shellShort
插入排序
希尔排序是插入排序的改进版本,它以插入排序为基础。在了解希尔排序前,我们有必要先简单了解插入排序。
插入排序很简单,它假定数组的前i个元素是有序的,然后将第 i+1 个元素插入到 0-i 中的正确位置上。假设第 i+1 个元素将插入到 j 位置,则 j~i 中的元素需要依次移动到 j+1 ~ i+1 的位置上。如 a = [5 9 0 7 1 10 1 7 3 4]
,在第一轮排序中,a1 > a[0],元素 a1 已经在正确位置上,在第二轮排序中,a2 = 0,经过不断的比较,元素 0 将被插入到 a[0] 位置,而元素 5 和 9 将后移,此时数组为 a = [0 5 9 7 1 10 1 7 3 4]
。以此类推。核心代码如下
public static void sort(Comparable[] a) {
for (int i = 0; i < a.length-1; i++) {
Comparable tmp = a[i+1];
int j = i+1;
for (; j > 0; j--) {
if (less(tmp, a[j-1])) a[j] = a[j-1];
else break;
}
a[j] = tmp;
}
}
上述代码需要注意的几点
- 数组元素类型我们设为 Comparable,则任何实现该接口的类型都符合,如Integer,String 等
- less 函数见文末链接的完整代码
- 这里我们在判断
a[j] < a[j-1]
时,并不是直接交换二者的顺序,而是直接让 a[j-1] 覆盖 a[j],最后再将 a[j] 的值放入正确的位置,提高效率。
算法效率分析:
- 插入排序的时间复杂度取决于数组的初始状态,最好的情况下,数组是预排序的,则只需要比较 n-1 次。O(N)
- 当数组元素是随机的,则对于外循环,需要执行N次,对于内循环,平均需要执行 N/4 次(内循环从 i 开始,所以只需要 N/2 次,又在随机状态下,平均每次循环执行到数组中间就 break 出来,故总的为 N/4),即实际情况一般为, 所以 O(N^2)
插入排序的特点:
- 对于小数组十分高效
- 对于预排序数组十分高效
- 每次只能比较相邻元素,比如最后一个元素的实际插入位置为0,则需要比较 N 次,效率比较低。
希尔排序
对于插入排序,它的实质是通过不断的比较来删除数组中的 逆序对,因此,删除逆序对的次数即可间接表示算法的效率,我们也知道,插入排序只能通过比较相邻元素,并在一次交换中只能删除一个逆序对,因此效率有限,而希尔排序则被设计用来解决这个问题。
希尔排序通过每次比较不相邻的元素并执行交换,可以在一个交换中删除一个或多个逆序对,从而在插入排序的基础上,提高算法效率。
如数组 a = [ 2 10 7 1 3 4 ]
,我们考虑 a[3] = 1 元素,易知它的正确位置应该为 a[0],在插入排序中,我们需要比较 a[3], a2 然后交换二者,在比较 a2, a1,再比较 a1, a[0] ,通过三个交换删除了三个逆序对使其回到正确的位置上。而在希尔排序中,我们可以直接比较 a[3], a[0],交换二者,此时我们通过一次交换删除了三个逆序对,这便是希尔排序最核心的思想。
再进一步,我们是如何做到比较不相邻元素的?
假设在第一轮排序中,我们使用增量序列为 h = 3;则上述a数组将被划分为几个逻辑数组,分别为 s1=[2, 1], s2 = [10, 3], s3 = [7, 4]
。我们可以发现,这几个子数组的元素在原数组中的索引是已h=3为间隔。
接下来,我们分别对这三个数组进行插入排序。为 s1=[1, 2], s2 = [3, 10], s3 = [4, 7]
,则a数组为 a = [1, 2, 4, 2, 10, 7]
。
接下来,我们修改增量序列为 h = 2,重复上述过程。
最后我们只要保证 h 最终的值能等于1,即可保证最后一次排序是一次标准的插入排序,则数组一定有序。
通过上述过程,我们可以发现,希尔排序在排序前半段可以保证当前排序数组比较小(如s1),在排序后半段可以保证数组是部分有序的,所以其充分利用了插入排序的两个性质。因此十分高效。
核心代码如下:
public static void sort(Comparable[] a) {
int n = a.length;
int h = n / 2;
while (h >= 1) {
for (int i = h; i < n; i++) {
Comparable tmp = a[i];
int j = i;
for (; j >= h; j -= h) {
if (less(tmp, a[j-h])) a[j] = a[j-h];
else break;
}
a[j] = tmp;
}
h /= 2;
}
}
对于上述实现,有下列几点说明
- 增量序列的选取不是唯一的,并且不同的增量序列对算法的影响也是很大的。但最终我们都要保证 h 最后能等于 1,才可以保证最终数组是有序的。
- 同样的,我们不采取直接交换元素的方式。
- 把 h 当作 1,并去掉最外层的 while循环,其实就是一个插入排序
对于中等规模的数据,希尔排序是比较不错的选择,一方面它的效率是完全可接受的,另一方面,它的编码非常简短且易于理解。
下面是几种排序的比较(对400个6000长度的随机序列排序的时间和)
Selection: 37.014
Insertion: 36.58
Shell: 6.443
Merge: 5.903
Quick: 3.889
可以发现,希尔排序相对插入排序有很明显的效率提升
插入排序完整代码参考:Insertion.java
希尔排序完整代码参考:Shell.java