Java数据结构和算法-高级排序(1-希尔排序)

在之前所讲的简单排序,虽然简单易实现,但是速度很慢,归并排序速度不慢,也易实现,但是却需要原始数组两倍大的空间。
在本章所讲的高级排序:希尔排序和快速排序都要比简单排序快得多:希尔排序只需要时间O(N*log(N)^2),而快速排序只需要时间O(N*log(N))。并且希尔排序和快速排序都不需要很大的辅助空间,希尔排序和归并排序一样容易实现,而快速排序是当前所讲的排序当中速度最快的一种。

希尔排序

希尔排序是以计算机科学家Donald L. shell的名字命名的。它基于插入排序,但在其基础上增加了一些特性,使其排序的速度要比插入排序快得多。
希尔排序对于将近几千条的数据或者中等数据量的数据进行排序是一种最优的选择。不过当数据量相当大时,希尔排序不像时间为O(N*log(N))的快速排序和归并排序那么快,所以大文件的排序处理,希尔排序不是最好的选择。但是希尔排序相对于时间为O(N^2)的插入排序和选择排序要快的多,并且希尔排序也是容易实现的,它的代码短且简单。
希尔排序最坏的情况和其平均情况的效率差不多,一些专家(Sedgwick)也提倡差不多在所有的排序开始都可以先使用希尔排序,若在实际中证明它不够快,再更换为其他比较快的排序比如快速排序。

插入排序的缺点-复制太多

希尔排序是基于插入排序的,它改变了插入排序复制太多的缺点。在插入排序中,标记符左边的数据是局部有序的,右边的数据无序的,对于N个数据而言,需要进行N趟插入操作,将标记符的数据插入到左边局部有序的队列当中,而在这个过程中,左边的数据需要不断向右移位(复制),虽然不是每次左边的所有数据都要进行复制,但对于N个数据而言,平均会进行N/2次复制,那么N趟操作即为N*N/2次复制,因此插入排序的效率为O(N^2)。
如果能有一种方法能够不用一个个地移动所有的中间数据,就能把最小的数据移到左边适当的位置,那么将节省不少的时间。而希尔排序正好能解决这个问题。

n-增量排序

希尔排序增大了插入排序的元素之间的间隔,在这些有间隔的元素之间进行插入排序,从而使得数据项能够大跨度的移动。当在这个间隔下的所有元素都进行了排序,就减少间隔元素个数的大小,然后再次进行插入排序,以此类推。在希尔排序中这个间隔配称为增量,并且习惯地用变量h表示。在下图中显示了以增量为4的对包含了10个数据的数组进行的第一趟排序。排序之后0,4,8位置的元素已经相对有序了。
Java数据结构和算法-高级排序(1-希尔排序)_第1张图片
当0,4,8有序之后,再向右移动一步,对1,5,9进行插入排序,这个过程一直持续下去,知道所有增量(间隔)为4的数据都进行了插入排序,如下图:
Java数据结构和算法-高级排序(1-希尔排序)_第2张图片
此时,(0,4,8),(1,5,9),(2,6),(3,7)的位置上的数据都是完全有序,且相互交错的的排列着,但又相互独立。

减少增量

当4-增量排序全部进行完之后,就会减少增量,进行1-增量排序,当然当间隔为1时此时的希尔排序就是插入排序。
当数据量比较大时,它的初始间隔也应该随之增大。比如1000个数据量的数组,它的初始增量为364,然后是121,再是40,然后是13,再是4,最后是1。这种用来形成间隔的序列(比如此处的364、121、40、13、4、1)被称为是间隔序列,它是由Knuth提出来的。这种序列的产生以逆向形式从1为初始值,然后通过递归公式:h=3*h+1来产生。

h 3*h+1 (h-1)/3
1 4
4 13 1
13 40 4
40 121 13
121 364 40
364 1093 121
1093 3280 364

当然,也可以使用其他方法来产生间隔序列,不过在这之前我们先使用Knuth的。最大的间隔是不能大于数据项个数的,所以在算到364的时候就是最大的间隔(初始增量)。然后通过等式(h-1)/3来减少增量获取下一个增量值。

当希尔排序的数据量较小时,并不能看出希尔排序比插入排序要快多少,时间是差不多的,但是当数据量增大时,初始增量也会随之增大,此时每一趟的排序要进行复制的数据量就会减少,而当增量h减少时,由于此时数据已经十分接近有序时最终的位置了,所以此时要移动的次数也很少,所以总体的效率就会提高。

希尔排序的Java代码

希尔排序的代码不必插入排序麻烦多少。在下面的代码中,通过数组类封装希尔排序的方法shellSort。

class ArraySh {
    private int[] theArray;
    private int nItem;

    public ArraySh(int max) {
        theArray = new int[max];
        nItem = 0;
    }

    public void insert(int value) {
        theArray[nItem ++] = value;
    }

    public void display() {
        for(int i = 0; i < nItem; i ++)
            System.out.print(theArray[i] + " ");
        System.out.println(" ");
    }

    public void shellSort() {
        int outer, inner;
        int temp;         //存放标记符所指数据
        int h = 1;        //增量

        //计算间隔序列
        while(h <= nItem)
            h = 3×h + 1;

        while(h > 0) {
            //对所有间隔为h的数据进行排序
            for(int i = 0; i < h; i ++) {
                outer = h + i;

                //对当前数据进行插入排序
                while(outer <= nItem) {
                    temp = theArray[outer];
                    inner = outer;

                    while(inner > 0 && theArray[inner - h] > temp) {
                        theArray[inner] = theArray[inner - h];
                        inner -= h;
                    }
                    theArray[inner] = temp;

                    outer += h;
                }
            }//end for

            h = (h - 1)/3; //减少增量
        }//end while
    }//end shellSort

}//end ArraySh

public ShellApp {
    public static void main(String[] args) {
        int max = 10;
        ArraySh arr = new ArraySh(max);

        for(int i = 0; i < max;  i ++)
            arr.insert((int)()java.lang.Math.random()*99);

        arr.display();
        arr.shellSort();
        arr.display();
    }
}

控制台输出:
96 38 90 39 28 67 87 93 47 56
28 38 39 47 56 67 87 90 93 96

其他间隔序列

间隔序列必须满足终止间隔为1,且序列中的所有数必须是互质的。
在希尔的原稿当中,间隔序列是通过N/2产生的,但是这并不满足互质的条件,尽管这对大于大多数数据而言这种方法还是比插入排序效果要好,但是这种方法有时会使运行效率降到O(N^2)。这使得希尔排序的效率并不比插入排序更高。

希尔排序的效率

希尔排序除了一些特殊的情况,还没有人能够精确地评估出它的效率,只能估计出它的效率从O(N^(3/2))到O(N^(7/6))不等。

你可能感兴趣的:(Java数据结构和算)