java實現排序(3)-希爾排序

引言

希爾排序也是經典的排序算法之一,其實本質上還是插入排序,不過它對插入排序做了進一步的優化。在本篇博文中會詳細介紹希爾排序,討論算法性能,用代碼實現希爾排序並解釋爲什麼它相對於插入排序有了進一步的優化。筆者目前整理的一些blog針對面試都是超高頻出現的。大家可以點擊鏈接:http://blog.csdn.net/u012403290

希爾排序

希爾排序是Donald Shell發明的,所以用他的名字命名。在希爾排序中,通過比較相距一定間隔的元素來工作,這一定間隔我們稱之爲增量。在一趟趟的排序中,不斷的減小增量,直到比較相鄰元素(增量爲1)的最後一趟排序爲止。所以希爾排序也叫縮減增量排序

爲了後面更好的理解,我這裏先用個例子來描述希爾排序的過程:

這裏寫圖片描述
我們來一步步分析一下上述過程:
目標數據爲:8,2,3,7,5
增量序列爲:2,1

A、在增量爲2的時候,其實數據的分組情況是:
①組:arr[0],arr[2],arr[4],即 8,3,5爲一組;
②組:arr[1],arr[3],即2,7爲一組。

那麼我們就需要分別對兩組實現在上一篇博文介紹的簡單插入排序來做排序處理:
處理第①組,第一組第一個默認爲是已排好序的序列(單獨一個數字,當然可以算已排好序的),拿出3來進行插入,發現3<8且8前面已經沒有數據了,所以就把3插入8前面,也就是我們上面說的“2增量1次”;接着處理5,發現5<8,但是5>3。所以就把5插入到3和8之間,注意這個插入和冒泡的區別,在插入排序中並不是兩兩交換順序,而是要尋找待插入的數字的準確位置,插入到3和8之間的過程是,把8往後移動一位,空出5的正確插入位置!!這就是上面說的“2增量3次
處理第②組,第二組第一個默認爲是已排好序的序列, 發現7比2大,直接把7插入到2的後面。也就是上面描述的“2增量2次”。
這裏有的小夥伴會問,爲什麼總結的執行順序和上面討論的不一致呢?不一致的原因是,我們並不是一個小組一個小組處理的,比如說上面的原始數據中,數組①和數組②是穿插在一起處理的,所以變成了處理①組再處理②組的情況。

B、在增量爲1的時候,其實數據就是隻有一組了:
①組 3,2,5,7,8。注意,這裏的數據是在增量爲2**處理之後**的數據。
其實增量爲1的時候,就是對所有的數據進行一次簡單插入排序。且只有增量爲1才能保證數據是排序完畢的,所以在設置增量的時候最後一個增量肯定是1,不然你的希爾排序就是不正確的。這裏的排序過程就是簡單的插入排序,會進行N-1輪(這裏爲4輪)的簡單插入排序,可以查看前面介紹插入排序的博文,這裏不再贅述。

通過上面的簡單瞭解,我們在進一步介紹一下希爾排序的核心模塊:增量序列。
從上面的例子可以看出,只要保證最後一個增量爲1,那麼任何增量序列都是可行的。對於上面的排序既可以[2、1]作爲增量,你還可以用 [3、1];[4、1];[4、3、1]等等。所以,這裏就會暴露出一個問題,如何選擇增量將會直接影響到你排序的性能問題,這個在後面我會用代碼測試運行時間來闡述。
希爾排序的時間複雜度依賴於增量序列的選擇,所以不同的增量序列,他們所導致的時間複雜度是不同的。

希爾增量序列

希爾增量序列是指這樣的一種增量序列:h = N/2,h/2…..1。的過程,就如上面的有5個元素進行排序,那麼它的希爾增量序列就是5/2 = 2, 2/2 = 1,也就是[2、1]增量序列。同時在希爾增量序列下,希爾排序的時間複雜度爲O(N^2)

Hibbard增量序列

Hibbard增量序列是一個更有經驗的序列,它描述這麼一個序列:1,3,7…….2^k-1。關鍵的因素是相鄰的增量是不存在公因子的。這個時候的希爾排序時間複雜度就爲O(N^(3/2))

在後面,我們嘗試比較①希爾增量下的希爾排序和②Hibbard增量下的希爾排序的運行效率。
在代碼實現之前,我們總結以下實現的規律:
①存在一個增量序列,且這個增量序列的最後一位爲1。同時在每一個增量序列下需要處理一次數據,那麼狠明顯,我們需要依賴增量序列進行一個for循環操作。
②對於按增量序列分組後的數據,對每個組的數據其實是一次簡單的插入排序。所以核心底層應該還是簡單插入排序。
③我們前面也說道,雖然說每一個增量分組需要單獨的進行簡單的插入排序,但是作爲一個整體的目標數據,進行多數組拆分顯然是不可能的,所以我們需要交叉處理各個數組。

代碼實現希爾增量下的希爾排序

package com.brickworkers;

import java.awt.color.ICC_ColorSpace;

/**
 * 
 * @author Brickworker
 * Date:2017年4月27日下午2:54:00 
 * 關於類ShellSort.java的描述:希爾排序
 * Copyright (c) 2017, brcikworker All Rights Reserved.
 */
public class ShellSort {

    //希爾增量下的希爾排序
    //爲了使代碼更加的通用,我們用泛型來書寫,同時和前幾篇一樣實現comparable接口
    public static<T extends Comparable<? super T>> void shellsort(T[] target){//用T表示泛型
        int j;//標記當前數據應該要插入的位置

        //第一個for循環,循環增量
        //希爾增量增量大於0,且第一個增量爲目標數據總長度的一半取整,後面的增量都爲前面增量的一半取整
        for(int increment = target.length / 2; increment > 0; increment /=2){

            //簡單的插入排序
            //遍歷目標數據,從第一個增量開始,第0個增量默認排序好的
            for(int i = increment; i < target.length; i++){
                //把帶插入的數據用temp暫存起來
                T temp = target[i];
                //尋找準確的插入位置
                //其實核心是在增量的分組下尋找這個值在某一個分組下應該屬於它的位置
                for(j = i; j >= increment&&temp.compareTo(target[j - increment]) < 0; j-=increment){//注意,因爲是處理分組的,所以不能和簡單插入排序一樣j--
                    //如果當前位置的數據比前一個數據小,那麼就需要把前面數據往後移動一位
                    target[j] = target[j - increment];//這裏並不是冒泡的交換位置!!
                }
                target[j] = temp;//把數據插入到準確的地方,這個纔是插入排序
                //打印每次排序後的結果
                String result = "";
                for (T t : target) {
                    result += t+" ";
                }
                System.out.println(increment+"增量的排序結果:" + result);

            }
        }
    }

public static void main(String[] args) {

        Integer[] target = {8,2,3,7,5};
        shellsort(target);
    }
}

    //輸出結果:
//  2增量排序結果:3 2 8 7 5 
//  2增量排序結果:3 2 8 7 5 
//  2增量排序結果:3 2 5 7 8 
//  1增量排序結果:2 3 5 7 8 
//  1增量排序結果:2 3 5 7 8 
//  1增量排序結果:2 3 5 7 8 
//  1增量排序結果:2 3 5 7 8 

大家仔細查看輸出結果,和我們在文章開頭所描述的希爾排序示例是一模一樣的。接下來我們嘗試用代碼實現Hibbard來實現希爾排序,有了上面的基礎,這裏就會簡單很多,只需要把increment做一下修改即可。

package com.brickworkers;

import java.util.Random;

/**
 * 
 * @author Brickworker
 * Date:2017年4月27日下午2:54:00 
 * 關於類ShellSort.java的描述:希爾排序
 * Copyright (c) 2017, brcikworker All Rights Reserved.
 */
public class ShellSort {

    //直接定義一種增量枚舉類型
    private static enum Type{
        SHELL, HIBBARD;
    }

    public static<T extends Comparable<? super T>> void shellsort(T[] target, Type type){//修改原來的方法,新增一個增量類型參數
        //但是要保證兩者的增量數是一致的,所以用size標記增量個數
        int size = 0;
        for(int increment = target.length / 2; increment > 0; increment /=2){
            size++;
        }
        switch (type) {
        case SHELL:
            long startTimeShell = System.currentTimeMillis();
            for(int increment = target.length / 2; increment > 0; increment /=2){
                dataHanding(target, increment);
            }
            System.out.println("希爾增量排序耗時:"+ (System.currentTimeMillis() - startTimeShell));
            break;
        case HIBBARD:
            long startTimeHibbard = System.currentTimeMillis();
            for(int increment = (int) (Math.pow(2, size)-1); size > 0; increment = (int) (Math.pow(2, --size)-1)){
                dataHanding(target, increment);
            }
            System.out.println("Hibbard增量排序耗時:"+ (System.currentTimeMillis() - startTimeHibbard));
            break;

        default:
            break;
        }


    }

    private static <T extends Comparable<? super T>> void dataHanding(T[] target, int increment) {
        int j ;//標記當前數據應該要插入的位置
        //簡單的插入排序
        //遍歷目標數據,從第一個增量開始,第0個增量默認排序好的
        for(int i = increment; i < target.length; i++){
            //把帶插入的數據用temp暫存起來
            T temp = target[i];
            //尋找準確的插入位置
            //其實核心是在增量的分組下尋找這個值在某一個分組下應該屬於它的位置
            for(j = i; j >= increment&&temp.compareTo(target[j - increment]) < 0; j-=increment){//注意,因爲是處理分組的,所以不能和簡單插入排序一樣j--
                //如果當前位置的數據比前一個數據小,那麼就需要把前面數據往後移動一位
                target[j] = target[j - increment];//這裏並不是冒泡的交換位置!!
            }
            target[j] = temp;//把數據插入到準確的地方,這個纔是插入排序
/*          //打印每次排序後的結果
            String result = "";
            for (T t : target) {
                result += t+" ";
            }
            System.out.println(increment+"增量的排序結果:" + result);*/
        }
    }


    public static void main(String[] args) {

        //製造一個2000個數據的無序序列
        Integer[] target = new Integer[20000];
        for (int i = 0; i < 20000; i++) {
            target[i] = new Random().nextInt(2000);
        }

        shellsort(target, Type.SHELL);
        shellsort(target, Type.HIBBARD);
    }
}


//輸出結果:
//希爾增量排序耗時:13
//Hibbard增量排序耗時:8

用一個靜態內部枚舉類表示情況分類,同時用size保證兩種增量的數量是一致的。因爲中間的一段代碼是複用的,所以我用eclipse中的Extract Method方法把中間重複部分抽離出來形成了一個新的方法。對於一個20000個數據的排序,發現用Hibbard增量的希爾排序會比希爾增量的希爾排序快的多,大家可以自己嘗試。

尾記

希爾排序是插入排序的一種優化,核心仍舊是插入排序。那麼爲什麼會使得性能有所提升呢?在簡單的插入排序中,我們討論過如果目標數據本身就是比較有序的那麼它的排序效率就會高很多,因爲它減少了複製的次數。那麼在希爾排序中,存在優越的增量情況下,可以減少數據比較和複製的次數。所以希爾排序的高效性和增量序列有極大的關係,一個好的增量序列才能提升關鍵效率。如果大家有興趣,可以把簡單插入排序也列入比較目標,3中類型進行比較很快就能發現各自之間的效率問題。

希望對大家有所幫助

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章