1. 程式人生 > 其它 >有趣的演算法(七) ——快速排序改進演算法

有趣的演算法(七) ——快速排序改進演算法

有趣的演算法(七)

——快速排序改進演算法

(原創內容,轉載請註明來源,謝謝)

一、概述

快速排序,被認為是最好的排序演算法之一。快速排序是20世紀60年代被提出的,其基本過程如下:

現假設長度為n的陣列a[n],需要進行排序。步驟如下:

1)隨機選其中一個元素,假設為a[i],將所有值比a[i]小的元素,移到a[i]的左邊,假設為陣列b;所有比a[i]大的元素,移到a[i]的右邊,假設為陣列c。

2)將陣列b、c分別遞迴執行步驟1,即將陣列不斷的分割成大的半部分和小的半部分,並且得到的結果繼續遞迴執行第1步,直到滿足第3步的條件。

3)當一個數組的元素只有兩個的時候,則直接比較這兩個元素的大小,並返回比較結果;當陣列元素只有一個,則直接返回這個陣列。

快速排序的速度很快,只需要O(nlogn),而且可以不需要額外的空間。

二、問題分析

快速排序在眾多排序演算法中,屬於非常優秀的演算法,不過這幾十年來,還是有許多人對其進行貢獻,提供了一些很好的改進。

從上述步驟中,分析出快速排序主要存在幾個問題:

1)第一步需要隨機選取一個元素作為切分元素。

現有陣列:[1, 2,3, 5, 8, 2, 6, 10],如果恰好取到第一個元素作為切分元素,則比較的結果,是所有後面的元素都要進入大的陣列,而小陣列沒有內容。這樣會導致效率低下。

因此,對於切分元素,不能選的太隨意,需要改進。

2)快速排序是一個遞迴的排序演算法。

在陣列元素很少的時候,如果也用快速排序,則要不斷的遞迴與函式呼叫,效率較低。而有一些簡單的演算法,對於陣列數量較少的時候,不需要遞迴,而且方便。

因此,對於陣列元素較少的情況,可以採用其他演算法。

3)元素值一樣的問題。

上述分析,都只考慮大於小於,而沒有考慮等於的情況。則在排序的時候,對於等於的元素,也會被移動或者遞迴,效率較低。

因此,需要考慮多個元素值一致的情況。

三、解決方案

針對上述三個問題,分別有解決方案。

1、切分元素選取

首先,針對傳過來的陣列,需要打散陣列,或者隨機選取一個元素,作為基準切分元素,假設為i,則值是a[i],假設v=a[i]。

接著,設定左右掃描指標(實質是陣列下標),一個從第一個元素開始(假設下標為p),一個從最後一個元素開始(假設下標為q)。

在每次迴圈的時候,p從前往後移動,直到找到一個比v小的值的下標;q則從後往前取比v大的下標。將這兩個下標對應的值互換。

迴圈結束的條件是p>=q。結束迴圈後,將a[i]和a[q]進行互換,實現將切分元素換到陣列的中間位置。

程式碼如下:

    /**
     * 獲取快速排序的切分元素,並進行部分排序,保證切分元素左側元素都小,右側都大
     */
    private static int partition(Comparable[]a, int low, int high){
        int i = low - 1, j = high + 1;//左右掃描指標
        int randomIndex = (int)(low +Math.random()*(high - low + 1));
        Comparable v = a[randomIndex];//切分元素
        while(true){
            //左邊找到比v大的元素
            while(less(a[++i], v, true)){
                if(i >= high) break;
           }
            //右邊找到比v小的元素
            while(less(v, a[--j], true)){
                if(j <= low) break;
            }
            //掃描結束退出條件
            if(i >= j) break;
            //交換左右兩邊找到的元素,保證相對有序
            exchange(a, i, j);
        }
        //將切分元素換到中間
        exchange(a, randomIndex, j);
        return j;
    }

上面程式碼中,less是自定義方法,用於比較兩個數大小;exchange也是自定義方法,用於交換陣列下標i、j的值。

經過上述方法,在獲取切分元素的同時,實際上已經完成了以切分元素值為中值,對陣列進行的切分。

如下圖所示:

2、小陣列排序

當陣列元素較少,不採用快速排序。經過前人研究,陣列元素少於5~15個的時候,用插入排序的效率更高。

因此,在遞迴的返回條件中,將high<low改成high<low+5即可。整個程式碼如下:

    /**
     * 快速排序
     */
    private static voidstartQuickSort(Comparable[] a, int low, int high){
        if(a.length <= 5 || high < low +5){
            insertSort(a);//陣列長度5以內採用插入排序
            return;
        }
        int partitionNum = partition(a, low,high);
        startQuickSort(a, low, partitionNum-1);
        startQuickSort(a, partitionNum+1,high);
}
    /**
     * 插入排序,陣列長度5以內採用此方法
     */
    private static void insertSort(Comparable[]a){
        int n = a.length;
        for(int i=1;i<n;i++){
            for(int j=i;j>0 &&less(a[j], a[j-1], false);j--){
                exchange(a, j, j-1);
            }
        }
}

3、同值元素問題

因為當前的快速排序,僅考慮大於和小於。對於等於的情況,可以在設定一個數組,專門存放於切分元素值一樣的元素,且放於陣列的中間位置。

這個解決方案,被稱為三取樣切分。和普通的快速排序,區別就在於切分多預留一個區間。

如下圖所示:

核心程式碼如下:

   /**
     * 三取樣切分
     */
    private static voidstart3WayQuickSort(Comparable[] a, int low, int high){
        if(a.length <= 5 || high < low +5){
            insertSort(a);//陣列長度5以內採用插入排序
            return;
        }
        //equalLeft~equalRight區間是等值的情況,low~equal~equalLeft是小的
        int equalLeft = low, equalRight = high,i = equalLeft +1;
        Comparable v = a[low];
        while(i <= equalRight){
            int cmp = a[i].compareTo(v);
            if(0 < cmp) exchange(a, i,equalRight--);//a[i]>v,交換i和當前最後一個元素,並將最後一個元素-1
            else if(0 > cmp) exchange(a,equalLeft++, i++);//a[i]<v,交換i和左邊的元素,並且指標往後
            else i++;//相同的情況,則直接比較下一個元素
        }
        start3WayQuickSort(a, low, equalLeft-1);
        start3WayQuickSort(a, equalRight+1,high);
}

四、總結

快速排序採用三取樣切分的改進方案後,在加上小陣列情況下引入插入排序,其排序的速度非常快,適合大部分的排序場景。

完整的程式碼見https://github.com/linhxx/taskmanagement/blob/master/src/main/java/com/lin/service/algorithm/QuickSortService.java,另外,也歡迎到github下載整個專案,這是一個基於springboot的網站,路徑:

https://github.com/linhxx/taskmanagement.git

——written by linhxx 2017.10.12