对于直接插入排序而言,当插入排序执行到一半时,待插值左边的所有数据都已经处于有序状态,直接插入排序将待插值存储在一个临时变量里,然后,从待插值左边第一个数据单元开始,只要该数据单元的值大于待插值,该数据单元就右移一格,直到找到第一个小于待插值的数据单元,接下来,将临时变量里的值放入小于待插值的数据单元之后(前面的所有数据都右移一格,因此该数据单元有一个空格)。
从上面算法可以发现一个问题:如果一个很小的数据单元位于很靠近右端的位置上,为了把这个数据单元移动到左边正确的位置上,中间所有的数据单元都得向右移一格,这个步骤都每一个数据项都执行了近 n 次的复制。虽然不是所有的数据项都得移动 n 个位置,但平均下来,每个数据项都会移动 n/2 格,总共是n^2/2 次复制,因此插入排序的执行效率是 O(n^2)。
Shell排序对直接插入排序进行了简单的改进:它通过加大插入排序中元素之间的间隔,并在这些个有间隔的元素进行插入排序,从而使数据项大跨度地移动。当这些数据项排过一趟序后,Shell排序算法减小数据项的隔间再进行排序。依次下去,这些进行排序的数据项之间的间隔被称之为增量,习惯上用 h 来表示这个增量。
下面以如下数据序列为例,进行说明。
9 , -16, 21*, 23, -30, -49, 21, 30*, 30
如果采用直接插入排序算法:
-16, 9, 21*, 23, -30, -49, 21, 30*, 30——第一趟,将第二个数据插入,前两个元素有序
-16, 9, 21*, 23, -30, -49, 21, 30*, 30——第二趟,将第三个数据插入,前三个元素有序
-16, 9, 21*, 23, -30, -49, 21, 30*, 30——第三趟,将第四个数据插入,前四个元素有序
-30, -16, 9, 21*, 23, -49, 21, 30*, 30——第四趟,将第五个数据插入,前五个元素有序
……
Shell排序就不一样了。假设本次Shell排序的 h 为 4,其插入操作如下:
9 , -16, 21*, 23, -30, -49, 21, 30*, 30
-30, -16, 21*, 23,9, -49, 21, 30*,30
-30, -49, 21*, 23, 9, -16, 21, 30*,30
-30, -49, 21*, 23, 9, -16, 21, 30*,30
-30, -49, 21*, 23, 9, -16, 21, 30*,30
-30, -49, 21*, 23, 9, -16, 21, 30*,30
注意上面排序过程中的粗体部分。
当 h 增量为4时,第一趟将保证索引为 0 ,4,8 的数据元素已经有序。第一趟完成后,算法向右一步,对索引 1,5 的数据元素进行排序。这个排序过程保持持续进行,直到所有的数据项都完成了以4为增量的排序。
当完成以4为增量的Shell排序后,所有元素;离他的最终有序位置相差不到两个单元,这就是数组的“基本有序”的含义,也正是Shell排序的奥义所在。通过这种交错的内部有序的数据项集合,就可以减少直接插入排序算法中数据项“整体搬家”的工作量。
常用的 h 序列由Knuth提出,该序列从 1 开始,通过如下公式产生:
h=3 * h + 1
可以看到常用的 h 序列的值有:1 、4、13、 40……反过来,程序中还要方向计算 h 的值:
h= (h - 1)/ 3
下面是一个简单的Shell排序实现:
模拟数据和直接插入排序一致:-56, -21, 56, -21*, 20, 56*, -123, 9, 26, 33。
public class ShellSort {
public static void shellSort(DataWrap[] data){
System.out.println("-开始排序-");
int arrayLength = data.length;
//增量的初始值为 1 ,同时也说明了直接插入排序算法是Shell排序算法当 h=1 时的一个特例
int h = 1;
//求h的最大值,
while( h <= arrayLength/3 ){
h = 3 * h + 1;
}
while(h > 0){
System.out.println("当h等于:"+h);
//从第h个元素开始判断
for(int i = h; i < arrayLength; i ++){
//存放当前索引处的数据元素。
DataWrap dw = data[i];
if(data[i].compareTo(data[i - h]) < 0){ //说明当前数据元素比它相邻的前面一个元素要小
int j = i - h;
for(; j >= 0 && data[j].compareTo(dw) > 0; j -= h){
data[j + h] = data[j];
}
data[j + h] = dw;
}
System.out.println(java.util.Arrays.toString(data));
}
h = (h - 1)/3;
}
}
public static void main(String[] args){
DataWrap[] data = {new DataWrap(-56,""),
new DataWrap(-21,""),
new DataWrap(56,""),
new DataWrap(-21,"*"),
new DataWrap(20,""),
new DataWrap(56,"*"),
new DataWrap(-123,""),
new DataWrap(9,""),
new DataWrap(26,""),
new DataWrap(33,"")};
System.out.println("-排序前-"+java.util.Arrays.toString(data))
shellSort(data);
System.out.println("-排序后-"+java.util.Arrays.toString(data));
}
}
运行结果为:
Shell排序是直接插入排序的改进版,因此它是稳定的。它的空间开销也是O(1)。时间开销估计在O(n的二分之三次幂)~O(n的六分之七次幂)之间,具体怎么得来的楼主暂不知。
参考资料:《疯狂Java程序员的基本修养》 --李刚 编著