Java數據結構和算法(九)——高級排序

Java數據結構和算法(三)——冒泡、選擇、插入排序算法中我們介紹了三種簡單的排序算法,它們的時間復雜度大O表示法都是O(N2),如果數據量少,我們還能忍受,但是數據量大,那么這三種簡單的排序所需要的時間則是我們所不能接受的。接著我們在講解遞歸 的時候,介紹了歸并排序,歸并排序需要O(NlogN),這比簡單排序要快了很多,但是歸并排序有個缺點,它需要的空間是原始數組空間的兩倍,當我們需要排序的數據占據了整個內存的一半以上的空間,那么是不能使用歸并排序的。

本篇博客將介紹幾種高級的排序算法:希爾排序和快速排序。

1、希爾排序

希爾排序是基于直接插入排序的,它在直接插入排序中增加了一個新特性,大大的提高了插入排序的執行效率。所以在講解希爾排序之前,我們先回顧一下直接插入排序。

①、直接插入排序

直接插入排序基本思想是每一步將一個待排序的記錄,插入到前面已經排好序的有序序列中去,直到插完所有元素為止。

image

實現代碼為:

public class InsertSort {
    public static int[] sort(int[] array){
        int j;
        //從下標為1的元素開始選擇合適的位置插入,因為下標為0的只有一個元素,默認是有序的
        for(int i = 1 ; i < array.length ; i++){
            int tmp = array[i];//記錄要插入的數據
            j = i;
            while(j > 0 && tmp < array[j-1]){//從已經排序的序列最右邊的開始比較,找到比其小的數
                array[j] = array[j-1];//向后挪動
                j--;
            }
            array[j] = tmp;//存在比其小的數,插入
        }
        return array;
    }

}

我們可以分析一下這個直接插入排序,首先我們將需要插入的數放在一個臨時變量中,這也是一個標記符,標記符左邊的數是已經排好序的,標記符右邊的數是需要排序的。接著將標記的數和左邊排好序的數進行比較,假如比目標數大則將左邊排好序的數向右邊移動一位,直到找到比其小的位置進行插入。

這里就存在一個效率問題了,如果一個很小的數在很靠近右邊的位置,比如上圖右邊待排序的數據 1 ,那么想讓這個很小的數 1 插入到左邊排好序的位置,那么左邊排好序的數據項都必須向右移動一位,這個步驟就是將近執行了N次復制,雖然不是每個數據項都必須移動N個位置,但是每個數據項平均移動了N/2次,總共就是N2/2,因此插入排序的效率是O(N2)。

那么如果以某種方式不必一個一個移動中間所有的數據項,就能把較小的數據項移動到左邊,那么這個算法的執行效率會有很大的改進。

  ②、希爾排序圖解

  希爾排序應運而生了,希爾排序通過加大插入排序中元素的間隔,并在這些有間隔的元素中進行插入排序,從而使數據項能夠大跨度的移動。當這些數據項排過一趟序后,希爾排序算法減小數據項的間隔再進行排序,依次進行下去,最后間隔為1時,就是我們上面說的簡單的直接插入排序。

下圖顯示了增量為4時對包含10個數組元素進行排序的第一個步驟,首先對下標為 0,4,8 的元素進行排序,完成排序之后,算法右移一步,對 1,5,9 號元素進行排序,依次類推,直到所有的元素完成一趟排序,也就是說間隔為4的元素都已經排列有序。

image

當我們完成4-增量排序之后,在進行普通的插入排序,即1-增量排序,會比前面直接執行簡單插入排序要快很多。

  ③、排序間隔選取

對于10個元素,我們選取4的間隔,那么100個數據,1000個數據,甚至更多的數據,我們應該怎么選取間隔呢?

希爾的原稿中,他建議間隔選為N/2,也就是每一趟都將排序分為兩半,因此對于N=100的數組,逐漸減小的間隔序列為:50,25,12,6,3,1。這個方法的好處是不需要在開始排序前為找到初始序列的間隔而計算序列,只需要用2整除N。但是這已經被證明并不是最好的序列。

間隔序列中的數字互質是很重要的指標,也就是說,除了1,他們沒有公約數。這個約束條件使得每一趟排序更有可能保持前一趟排序已經排好的結果,而希爾最初以N/2的間隔的低效性就是沒有遵守這個準則。

所以一種希爾的變形方法是用2.2來整除每一個間隔,對于n=100的數組,會產生序列45,20,9,4,1。這比用2會整除會顯著的改善排序效果。

還有一種很常用的間隔序列:knuth 間隔序列 3h+1

image

但是無論是什么間隔序列,最后必須滿足一個條件,就是逐漸減小的間隔最后一定要等于1,因此最后一趟排序一定是簡單的插入排序。

下面我們通過knuth間隔序列來實現希爾排序:

  ④、knuth間隔序列的希爾排序算法實現

//希爾排序 knuth 間隔序列 3h+1
public static void shellKnuthSort(int[] array){
    System.out.println("原數組為"+Arrays.toString(array));
    int step = 1 ;
    int len = array.length;
    while(step <= len/3){
        step = step*3 + 1;//1,4,13,40......
    }   
    while(step > 0){
        //分別對每個增量間隔進行排序
        for(int i = step ; i < len ; i++){
            int temp = array[i];
            int j = i;
            while(j > step-1 && temp <= array[j-step]){
                array[j] = array[j-step];
                j -= step;
            }
            array[j] = temp;
        }//end for
        System.out.println("間隔為"+step+"的排序結果為"+Arrays.toString(array));
        step = (step-1)/3;
    }//end while(step>0)

    System.out.println("最終排序:"+Arrays.toString(array));
}

測試結果:

public static void main(String[] args) {
    int[] array = {4,2,8,9,5,7,6,1,3,10};
    shellKnuthSort(array);
}
image

⑤、間隔為2h的希爾排序

//希爾排序 間隔序列2h
public static void shellSort(int[] array){
    System.out.println("原數組為"+Arrays.toString(array));
    int step;
    int len = array.length;
    for(step = len/2 ;step > 0 ; step /= 2){
        //分別對每個增量間隔進行排序
        for(int i = step ; i < array.length ; i++){
            int j = i;
            int temp = array[j];
            if(array[j] < array[j-step]){
                while(j-step >=0 && temp < array[j-step]){
                    array[j] = array[j-step];
                    j -= step;
                }
                array[j] = temp;
            }
        }
        System.out.println("間隔為"+step+"的排序結果為"+Arrays.toString(array));
    }
}

測試結果:

image

2、快速排序

快速排序是對冒泡排序的一種改進,由C. A. R. Hoare在1962年提出的一種劃分交換排序,采用的是分治策略(一般與遞歸結合使用),以減少排序過程中的比較次數。

①、快速排序的基本思路

一、先通過第一趟排序,將數組原地劃分為兩部分其中一部分的所有數據都小于另一部分的所有數據原數組被劃分為2份

二、通過遞歸的處理, 再對原數組分割的兩部分分別劃分為兩部分,同樣是使得其中一部分的所有數據都小于另一部分的所有數據。 這個時候原數組被劃分為了4份

三、就1,2被劃分后的最小單元子數組來看,它們仍然是無序的,但是! 它們所組成的原數組卻逐漸向有序的方向前進。

四、這樣不斷劃分到最后,數組就被劃分為多個由一個元素或多個相同元素組成的單元,這樣數組就有序了。

具體實例:

image

對于上圖的數組[3,1,4,1,5,9,2,6,5,3],通過第一趟排序將數組分成了[2,1,1]或[4,5,9,3,6,5,3]兩個子數組,且對于任意元素,左邊子數組總是小于右邊子數組。通過不斷的遞歸處理,最終得到有序數組[1 1 2 3 3 4 5 5 6]

②、快速排序的算法實現

假設被排序的無序區間為[A[i],......,A[j]]

一、基準元素選取:選擇其中的一個記錄的關鍵字 v 作為基準元素(控制關鍵字);怎么選取關鍵字?

  二、劃分:通過基準元素 v 把無序區間 A[I]......A[j] 劃分為左右兩部分,使得左邊的各記錄的關鍵字都小于 v;右邊的各記錄的關鍵字都大于等于 v;(如何劃分?)

  三、遞歸求解:重復上面的一、二步驟,分別對左邊和右邊兩部分遞歸進行快速排序。

  四、組合:左、右兩部分均有序,那么整個序列都有序。

上面的第 三、四步不用多說,主要是第一步怎么選取關鍵字,從而實現第二步的劃分?

劃分的過程涉及到三個關鍵字:“基準元素”、“左游標”、“右游標”

  基準元素:它是將數組劃分為兩個子數組的過程中,用于界定大小的值,以它為判斷標準,將小于它的數組元素“劃分”到一個“小數值的數組”中,而將大于它的數組元素“劃分”到一個“大數值的數組”中,這樣,我們就將數組分割為兩個子數組,而其中一個子數組的元素恒小于另一個子數組里的元素。

左游標:它一開始指向待分割數組最左側的數組元素,在排序的過程中,它將向右移動。

右游標:它一開始指向待分割數組最右側的數組元素,在排序的過程中,它將向左移動。

注意:上面描述的基準元素/右游標/左游標都是針對單趟排序過程的, 也就是說,在整體排序過程的多趟排序中,各趟排序取得的基準元素/右游標/左游標一般都是不同的。

  對于基準元素的選取,原則上是任意的。但是一般我們選取數組中第一個元素為基準元素(假設數組是隨機分布的)

③、快速排序圖示

[圖片上傳中...(image-f70433-1527743588444-4)]

上面表示的是一個無序數組,選取第一個元素 6 作為基準元素。左游標是 i 哨兵,右游標是 j 哨兵。然后左游標向左移動,右游標向右移動,它們遵循的規則如下:

一、左游標掃描, 跨過所有小于基準元素的數組元素, 直到遇到一個大于或等于基準元素的數組元素, 在那個位置停下

二、右游標掃描, 跨過所有大于基準元素的數組元素, 直到遇到一個小于或等于基準元素的數組元素,在那個位置停下。

  第一步:哨兵 j 先開始出動。因為此處設置的基準數是最左邊的數,所以需要讓哨兵 j 先開始出動,哨兵 j 一步一步的向左挪動,直到找到一個小于 6 的元素停下來。接下來,哨兵 i 再一步一步的向右挪動,直到找到一個大于 6 的元素停下來。最后哨兵 i 停在了數字 7 面前,哨兵 j 停在了數字 5 面前。

image

到此,第一次交換結束,接著哨兵 j 繼續向左移動,它發現 4 比基準數 6 要小,那么在數字4面前停下來。哨兵 i 也接著向右移動,然后在數字 9 面前停下來,然后哨兵 i 和 哨兵 j 再次進行交換。

image

第二次交換結束,哨兵 j 繼續向左移動,然后在數字 3 面前停下來;哨兵 i 繼續向右移動,但是它發現和哨兵 j 相遇了。那么此時說明探測結束,將數字 3 和基準數字 6 進行交換,如下:

image

到此,第一次探測真正結束,此時已基準點 6 為分界線,6 左邊的數組元素都小于等于6,6右邊的數組元素都大于等于6。

左邊序列為【3,1,2,5,4】,右邊序列為【9,7,10,8】。接著對于左邊序列而言,以數字 3 為基準元素,重復上面的探測操作,探測完畢之后的序列為【2,1,3,5,4】;對于右邊序列而言,以數字 9 位基準元素,也重復上面的探測操作。然后一步一步的劃分,最后排序完全結束。

通過這一步一步的分解,我們發現快速排序的每一輪操作就是將基準數字歸位,知道所有的數都歸位完成,排序就結束了。

image

④、快速排序完整代碼

package com.ys.high.sort;

public class QuickSort {

    //數組array中下標為i和j位置的元素進行交換
    private static void swap(int[] array , int i , int j){
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }

    private static void recQuickSort(int[] array,int left,int right){
        if(right <= left){
            return;//終止遞歸
        }else{

            int partition = partitionIt(array,left,right);
            recQuickSort(array,left,partition-1);// 對上一輪排序(切分)時,基準元素左邊的子數組進行遞歸
            recQuickSort(array,partition+1,right);// 對上一輪排序(切分)時,基準元素右邊的子數組進行遞歸
        }
    }

    private static int partitionIt(int[] array,int left,int right){
        //為什么 j加一個1,而i沒有加1,是因為下面的循環判斷是從--j和++i開始的.
        //而基準元素選的array[left],即第一個元素,所以左游標從第二個元素開始比較
        int i = left;
        int j = right+1;
        int pivot = array[left];// pivot 為選取的基準元素(頭元素)
        while(true){
            while(i<right && array[++i] < pivot){}

            while(j > 0 && array[--j] > pivot){}

            if(i >= j){// 左右游標相遇時候停止, 所以跳出外部while循環
                break;
            }else{
                swap(array, i, j);// 左右游標未相遇時停止, 交換各自所指元素,循環繼續 
            }
        }
        swap(array, left, j);//基準元素和游標相遇時所指元素交換,為最后一次交換
        return j;// 一趟排序完成, 返回基準元素位置(注意這里基準元素已經交換位置了)
    }

    public static void sort(int[] array){
        recQuickSort(array, 0, array.length-1);
    }

    //測試
    public static void main(String[] args) {
        //int[] array = {7,3,5,2,9,8,6,1,4,7};
        int[] array = {9,9,8,7,6,5,4,3,2,1};
        sort(array);
        for(int i : array){
            System.out.print(i+" ");
        }
        //打印結果為:1 2 3 4 5 6 7 7 8 9 
    }
}

⑤、優化分析

假設我們是對一個逆序數組進行排序,選取第一個元素作為基準點,即最大的元素是基準點,那么第一次循環,左游標要執行到最右邊,而右游標執行一次,然后兩者進行交換。這也會劃分成很多的子數組。

那么怎么解決呢?理想狀態下,應該選擇被排序數組的中值數據作為基準,也就是說一半的數大于基準數,一般的數小于基準數,這樣會使得數組被劃分為兩個大小相等的子數組,對快速排序來說,擁有兩個大小相等的子數組是最優的情況。

  三項取中劃分

為了找到一個數組中的中值數據,一般是取數組中第一個、中間的、最后一個,選擇這三個數中位于中間的數。

//取數組下標第一個數、中間的數、最后一個數的中間值
    private static int medianOf3(int[] array,int left,int right){
        int center = (right-left)/2+left;
        if(array[left] > array[right]){ //得到 array[left] < array[right]
            swap(array, left, right);
        }
        if(array[center] > array[right]){ //得到 array[left] array[center] < array[right]
            swap(array, center, right);
        }
        if(array[center] > array[left]){ //得到 array[center] <  array[left] < array[right]
            swap(array, center, left);
        }

        return array[left]; //array[left]的值已經被換成三數中的中位數, 將其返回
    }
    private static int partitionIt(int[] array,int left,int right){
        //為什么 j加一個1,而i沒有加1,是因為下面的循環判斷是從--j和++i開始的.
        //而基準元素選的array[left],即第一個元素,所以左游標從第二個元素開始比較
        int i = left;
        int j = right+1;
        int pivot = array[left];// pivot 為選取的基準元素(頭元素)

        int size = right - left + 1;
        if(size >= 3){
            pivot = medianOf3(array, left, right); //數組范圍大于3,基準元素選擇中間值。
        }
        while(true){
            while(i<right && array[++i] < pivot){}

            while(j > 0 && array[--j] > pivot){}

            if(i >= j){// 左右游標相遇時候停止, 所以跳出外部while循環
                break;
            }else{
                swap(array, i, j);// 左右游標未相遇時停止, 交換各自所指元素,循環繼續 
            }
        }
        swap(array, left, j);//基準元素和游標相遇時所指元素交換,為最后一次交換
        return j;// 一趟排序完成, 返回基準元素位置(注意這里基準元素已經交換位置了)
    }

處理小劃分

如果使用三數據取中劃分方法,則必須遵循快速排序算法不能執行三個或者少于三個的數據,如果大量的子數組都小于3個,那么使用快速排序是比較耗時的。聯想到前面我們講過簡單的排序(冒泡、選擇、插入)。

當數組長度小于M的時候(high-low <= M), 不進行快排,而進行插入排序。轉換參數M的最佳值和系統是相關的,一般來說, 5到15間的任意值在多數情況下都能令人滿意。

//插入排序
    private static void insertSort(int[] array){
        for(int i = 1 ; i < array.length ; i++){
            int temp = array[i];
            int j = i;
            while(j > 0 && array[j-1] > temp){
                array[j] = array[j-1];
                j--;
            }
            array[j] = temp;
        }
    }
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,836評論 6 540
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,275評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,904評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,633評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,368評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,736評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,740評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,919評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,481評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,235評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,427評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,968評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,656評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,055評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,348評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,160評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,380評論 2 379

推薦閱讀更多精彩內容

  • 一、 單項選擇題(共71題) 對n個元素的序列進行冒泡排序時,最少的比較次數是( )。A. n ...
    貝影閱讀 9,160評論 0 10
  • 概述排序有內部排序和外部排序,內部排序是數據記錄在內存中進行排序,而外部排序是因排序的數據很大,一次不能容納全部的...
    Luc_閱讀 2,288評論 0 35
  • 04月16日-04月21日 收獲: 1,0416周日第一次做主持人,第一次自我突破,雖然不完美,但是圓滿的完成了,...
    lijianhua_run閱讀 184評論 0 0
  • 人,別精得過火, 太精明的人,往往惹人厭, 凡事爭個明白,處處非得較真, 把便宜占盡,把好事包攬, 時間長了,再好...
    peter_621f閱讀 184評論 0 0
  • 我是陸的妹夫,來上海多年。我終日穿著考究的長衫、上午泡茶館、下午泡澡堂子,悠閑地混跡在上海灘,連麻將都比他們...
    annie十二月閱讀 269評論 0 1