希尔排序是插入排序的优化版,我们知道插入排序是普通排序,在两两比较的前提下,进行的排序算法。这样有一个问题,如果数组极端情况下全是逆序,比如[j,i,h,g,f,e,d,c,b,a],要完成升序的排序的话,会让比较和交换的次数为N*N/2次。如果这个数据量越大,移动的距离就更长,
在第一次048,159,26,37这四组下标的时候,048下标的三个元素进行了远距离的比较,虽然步长——对,请记住这个名词,步长。虽然步长大,但是因为数据量很小,只有三个,所以很快。然后接着比较159这三个元素,同样的处理方式。接着比较其他组的元素。
比较完成之后,然后缩小步长,我们使用两两比较,再把整个数组进行比较,这样就比较快了。刚开始我会想,有一种情况:如果048下标刚好是c,b,a这三个元素,即使是完成了第一次步长的排序,也还是会变成【a,x,x,x,b,x,x,x,d,x】,那d岂不是还要移动6个位置去到达最终位置,这个可能是有的,因为这类特例是无法控制的。所以不能使用特例来讨论。
下面是代码:注意,我这里是1万的数据。。。。所以,普通排序可能会稍慢,你也可以实施10万的数据,大概是2分钟吧。建议copy下去,执行一下。sort为希尔排序,normalsort为冒泡排序。所以,如果太急的话,可以只看上面的sort方法。
public class Shellsort {
public static void sort(int[] ints) {
System.out.println("希尔排序前,前6个元素");
for(int i = 0 ; i<6 ; i++){
System.out.print(ints[i]+"\t");
}
long startTime=System.currentTimeMillis();
int inner, outer;
int temp; //定义三个数据变量,在循环中接收比较的数据
int h = 1;
int length = ints.length;
while (h <= length/3) {
h = h * 3 + 1;
}
while (h > 0) {
for (outer = h ; outer < length ; outer ++){
temp = ints[outer]; //temp 获得outer的引用
inner = outer;
while (inner > h-1 && ints[inner-h] >= temp){ //如果前面的大于后面的,执行交换
ints[inner] = ints[inner-h]; //inner 在第一次进入while的时候= outer ,这是inner下标=outer,outer的引用被修改为大的值,outer原本指向的数据断开,由temp保存引用,作为最小值,在推出while的时候,赋值给这一个分组的最小的下标
inner -= h; //每次while循环,如果前面大于后面,则交换,同时修改inner所表示的下标,这时,前面进行的第一次排序也就失效,所以再次进入while,进行排序。
}
ints[inner] = temp; //如果这一组数据是多余两个的,需要在这一次完成多个数据排序。因为采用的是 h*3+1的数据,会存在 n-h>h-1的情况,比如增量为4的时候,8,9,10,11,12都是这样的数据。
}
h = (h-1)/3; //对增量进行缩减控制,刚开始增量大,分组多,数据距离大,但是数据项少。越到后面,增量小,分组少,数据项多,但是交换距离小。
}
long endTime=System.currentTimeMillis();
System.out.println("\n\n希尔排序数据所用时间: "+(endTime-startTime)+"ms");
for(int i = 0 ; i<6 ; i++){
System.out.print(ints[i]+"\t");
}
System.out.println("");
}
public static void normalsort(int[] ints){
System.out.println("\n\n冒泡排序前,前6个元素");
for(int i = 0 ; i<6 ; i++){
System.out.print(ints[i]+"\t");
}
long startTime=System.currentTimeMillis();
int length = ints.length;
for(int i =0 ; i<length; i++ ){
for(int j= i+1 ; j<length; j++){
if(ints[j] < ints[i]){
int temp = ints[j];
ints[j] = ints[i];
ints[i] = temp;
}
}
}
long endTime=System.currentTimeMillis();
System.out.println("\n\n冒泡循环数据所用时间: "+(endTime-startTime)+"ms");
for(int i = 0 ; i<6 ; i++){
System.out.print(ints[i]+"\t");
}
}
public static void main(String[] args){
int length = 100000;
int[] ints = new int[length];
int[] normals = new int[length];
for(int n=0 ; n<ints.length ; n++){
normals[n] = ints[n] = (int)(Math.random()*2000000);
}
System.out.println("开始排序,数据量为"+length+"\n\n");
sort(ints);
normalsort(normals);
}
}
如果觉得过于抽象,或者是我有注释没有说清楚,请参考图示:《10大经典排序》中的希尔排序图示。
数组中保存的元素都是数据的引用(基本数据类型保留的是值,数据本身),那么对于java而言,数组交换的实质是对引用的交换,而C是真实地对数据的值进行了交换,这点需要明确一下。所以C里面的交换成本理论上来讲,是大于java的交换成本的。
最后,一个问题,希尔排序的关键点在于步长的控制,如何选择合适的步长呢?请参考《数据结构与算法》第七章,241页的内容。