算法思想介绍:
希尔排序思想的精髓: 让数组中任意间隔为 h 的元素,都是有序的。这样的数组 我们叫它 h有序数组。 换句话说:一个h有序数组 就是h个相互独立的有序数组,编织在一起的一个数组。在进行排序的时候,如果h很大,我们就能将元素移动到很远的地方。由于每轮的 步长step逐渐缩小,希尔排序又叫为“缩小增量排序”。 shell 排序的比插入排序 更高效的原因是 : 它权衡了子数组的规模和有序性。
算法代码实现:
辅助函数
private static boolean less(Comparable a, Comparable b) {
return a.compareTo(b) < 0;
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
@Test
public void shellSort0() {
int[] a = {14, 13, 12 ,11, 10, 9, 8, 7,// 15/2 = 7 , 左边多一个元素。
6, 5, 4, 3, 2, 1, 0};
int length = a.length;
System.out.println("数组长度为:"+length);
int group = 2;
// 增幅步长
int step = 1;
// 引入一个while循环,让h按照 递增序列 进行递减。
while (step < length / group) { // step < (15/2 即为 7); 除以2的话,步长 能循环到 step=7。
step = group * step + 1;
System.out.println("group * step + 1 公式计算ING... 计算中 step => " + step);
}
System.out.println("group * step + 1 最终的h值:" + step);
while (step >= 1) {
System.out.println("while 循环开始 , step ==> " + step);
//将数组变为h有序。
for (int i = step; i < length; i++) { //通过i的递增,实现
// ps 为什么 j -= step 写这个代码,因为插入排序是从后面向前面插入的。 将arr[i] 插入到 a[i-step],a[i-2h],a[i-3h]中。
// ps j>=step 为什么? j-step 这个索引必须合法 >=0. 否则 a[j - step] 索引没有意义了。
for (int j = i; j >= step; j -= step) {
System.out.println(i + " VS " + "[" + (j - step) + "<=" + j + "-" + step + "]");
if (less(a[j], a[j - step])) {
swap(a, j, j - step);
}
}
}
// 由于每轮的 步长step逐渐缩小,希尔排序又叫为“缩小增量排序”。
step = step / group;
}
System.out.println("\n What the final arr looks like 最终数组的模样: \n" + Arrays.toString(a));
}
关于最内层for循环,比较容易困惑的问题有:
- 为什么 写 j -= h 这样的代码?因为利用的是插入排序思想,前面我们介绍过,插入排序是从后面向前面插入的。 将a[i] 插入到 a[i-h],a[i-2h],a[i-3h]中。
- j>=step 为什么? 因为 j-step 这个索引必须合法 >=0. 否则 a[j - step] 索引没有意义了。
另外一种实现,供参考:
@Test
public void shellSort() {
int[] a = {6, 2, 4, 0, 7, 3, 8, 1, 9, 5};
int n = a.length;
//定义变量 :i和j控制循环; inc偏移增量; key插入的时候临时保存的变量。
int i, j = 0, inc, key;
int count = 0;
//第一次 inc 为 长度的一半。 跑完了,再来一次对半。直到最后一次为1为止。 inc用于控制偏移的步长
for (inc = n / 2; inc > 0; inc /= 2) { //分组,计算分组步长。
//这里i 为什么等于 inc ,因为第一个元素本身就是有序的。
for (i = inc; i < n; i++) { // 使得key a[i] 可以递增 到 (length-1)
// ps 每一轮 采用插入排序 i开始为inc=7,然后依次取值 => 7,8,9,10,11,12,13,14
// 待插入的元素 key 即为 a[i]
key = a[i]; //a[7] 数据 作为 key 数据
System.out.println(i + " VS " + (j - inc) + " ,次数" + (++count));
for (j = i; j >= inc && key < a[j - inc]; j -= inc) { // j =7 开始, 7 >=7 && a[7] < a[7-7]
a[j] = a[j - inc]; // a[7] = a[0]
}
//此时,key 找到归宿索引位置j,插入排序中用key变量来记录,应该是方便减少交换操作。
a[j] = key;
}
System.out.println(Arrays.toString(a));
}
System.out.println("\n What the final a looks like 最终数组的模样: \n" + Arrays.toString(a));
}
个人感觉,代码实现,还是比之前的几个初级算法有更有一些实现难度的。唯有多练,勤能补拙。
算法分析 希尔排序的核心在于间隔序列的h设定和h之间的数学性质,比如公因子等,目前为止最好的间隔序列尚未有定论。当然你可以提前设定好间隔序列,或者可以动态的定义间隔序列。本文的第1种算法实现,你可以通过控制group变量来动态定义间隔序列的算法。