快速排序一步一步優化

1、快速排序介紹

  快速排序是C.R.A.Hoare於1962年提出的一種劃分交換排序。它採用了一種分治的策略,一般稱其爲分治法(Divide-and-ConquerMethod)。html

  算法思想:1.先從數組中取出一個數組做爲樞軸,通常狀況下選取數組的第一個或者最後一個元素做爲樞軸,固然能夠選取其餘的,在後面的優化措施裏面,我會慢慢介紹。java

       2.雙向遍歷,從左邊選取一個比樞軸大的數,從右邊選擇一個比樞軸小的數,而後交換這兩個數;算法

       3.重複步驟2,直到在樞軸的左邊都比樞軸小,樞軸右邊的數都比樞軸大。數組

  算法的時間複雜度:O(nlogn)安全

2、內容

  示例數組:arr = {1,4,2,5,6,7,9,3};dom

   

  咱們選取第一個數做爲樞軸。ide

  下面,咱們來看看第一趟遍歷過程:函數

    

  咱們從左循環了3次找到了比樞大的數5,從右循環找到了比樞軸小的數3,接下來,咱們要交換這兩個數:性能

    

  至此,第一趟遍歷結束,可是這並無達到要求。咱們來看看第二趟遍歷的結果:優化

    

  交換:

    

  因爲,上述已經知足了條件,所以沒必要進行再次交換。

    直到最後一趟,咱們樞軸歸位:

    

  代碼實現:

int qsort(int *a,int left,int right){
    if (right <= left)
        return -1;
    int pivot = a[right];
    int i = left;
    int j = right - 1;

    //從前向後掃描,不須要判斷是否會出現越界問題
    while(true){
        while(a[i++] < pivot);

        //從後向前掃描,要防止越界
        while(a[j] > pivot && j >= left){
            j--;
        }
        if (i < j)
            swap(a[i++],a[j--]);
        else{
            break;
        }
    }
    swap(a[i],pivot); // 最後一趟是將a[i]與pivot交換
    qsort(a,left,i -1);
    qsort(a,i+1,right);
    return 0;
}

3、優化 

  咱們都知道,快速排序的效率高低主要在於樞軸的選取,不管選取首個元素仍是最後一個元素做爲樞軸,咱們都要對數組進一次遍歷。所以,要想優化快排,還得從樞軸的選取下手。

  1.隨機選取法

  引入緣由:在待排序列是部分有序時,固定選取樞軸使快排效率底下,要緩解這種狀況,就引入了隨機選取樞軸

  思路:使用隨機數生成函數生成一個隨機數rand,隨機數的範圍爲[left, right],並用此隨機數爲下標對應的元素a[rand]做爲中軸,並與最後一個元素a[right]交換,而後進行

與選取最後一個元素做爲中軸的快排同樣的算法便可。

  優勢:這是一種相對安全的策略。因爲樞軸的位置是隨機的,那麼產生的分割也不會老是會出現劣質的分割。在整個數組數字全相等時,仍然是最壞狀況,時間複雜度是O(n^2)。實際上,隨機化快速排序獲得理論最壞狀況的可能性僅爲1/(2^n)。因此隨機化快速排序能夠對於絕大多數輸入數據達到O(nlogn)的指望時間複雜度。

  代碼實現:

int random(int left,int right){
    return rand() % (right - left + 1) + left;
}

void Qsort(int *a,int left,int right){
    if (left >= right)
    {
        return;
    }
    //隨機選取一個元素做爲樞軸,並與最後一個元素進行交換
    int ic = random(left,right);
    swap(a[ic],a[right]);

    int midIndex = data[right];
    int i = left;
    int j = right - 1;

    while(true){
        //找大於樞軸的數據
        while(a[i++] < midIndex);

        //找到小於樞軸的數據
        while(a[j] > midIndex && j >= left){
            j--;
        }
        //數據已經找到,準備交換
        if (i < j)
        {
            swap(a[i++],a[j--]);
        }
        else{
            break;
        }
    }
    swap(a[i],midIndex); //將樞軸放在正確的位置
    Qsort(a,left,i -1);
    Qsort(a,i+1,right);
}

  2.三數取中(median-of-three)

  引入的緣由:雖然隨機選取樞軸時,減小出現很差分割的概率,可是仍是最壞狀況下仍是O(n^2),要緩解這種狀況,就引入了三數取中選取樞軸

  思路:假設數組被排序的範圍爲left和right,center=(left+right)/2,對a[left]、a[right]和a[center]進行適當排序,取中值爲中軸,將最小者放a[left],最大者放在a[right],把中軸元與a[right-1]交換,並在分割階段將i和j初始化爲left+1和right-2。而後使用雙向描述法,進行快排。

  分割好處:      

    1.將三元素中最小者被分到a[left]、最大者分到a[right]是正確的,由於當快排一趟後,比中軸小的放到左邊,而比中軸大的放到右邊,這樣就在分割的時候把它們分到了正確的位置,減小了一次比較和交換。

    2.在前面所說的全部算法中,都有雙向掃描時的越界問題,而使用這個分割策略則能夠解決這個問題。由於i向右掃描時,必然會遇到不小於中軸的數a[right-1],而j在向左掃描時,必然會遇到不大於中軸的數a[left],這樣,a[right-1]和a[left]提供了一個警惕標記,因此不須要檢查下標越界的問題。

  分析:最佳的劃分是將待排序的序列分紅等長的子序列,最佳的狀態咱們可使用序列的中間的值,也就是第N/2個數。但是,這很難算出來,而且會明顯減慢快速排序的速度。這樣的中值的估計能夠經過隨機選取三個元素並用它們的中值做爲樞紐元而獲得。事實上,隨機性並無多大的幫助,所以通常的作法是使用左端、右端和中心位置上的三個元素的中值做爲樞紐元。顯然使用三數中值分割法消除了預排序輸入的很差情形,而且減小快排大約14%的比較次數

   例子:

    初始數組:6  1  8  9  4  3  5  2  7  0

    選取三個中間數:6  1  8  9  4  3  5  2  7  0

     對這三個數進行排序:0  1  8  9  4  3  5  2  7  6

    最後中軸與a[right-1]交換:0  1  8  9  7  3  5  2  4  6

  實例代碼:

int Median(int *a,int left,int right){
    int midIndex = (left + right)>>1;
    if (a[left] > a[midIndex])
    {
        swap(a[left],a[midIndex]);
    }
    if (a[left] > a[right])
    {
        swap(a[left],a[right]);
    }
    if (a[midIndex] > a[right])
    {
        swap(a[midIndex],a[right]);
    }
    swap(a[midIndex],a[right-1]);
    return a[right-1]; //返回中軸
}
void qSort(int *a,int left,int right){
        //若是須要排序的數據大於3個則使用快速排序
        if (right - left >=3)
        {
            int midIndex = Median(a,left,right);
            int begin = left;
            int end = right - 1;
            while (true){
                while(a[++begin] < midIndex);
                while(a[--end]<midIndex);
                if (begin < end)
                {
                    swap(a[begin],a[end]);
                }
                else{
                    swap(a[begin],a[right -1]);//將樞軸移動到什麼時候位置
                    break;
                }
            }
            qSort(a,left,begin -1);
            qSort(a,begin + 1,right);
        }
        else{
            BubbleSort(a,left,right);//當數據小於3個,直接用冒泡排序
        }//當要排序的數據不多時(小於3個),則不能進行三數取中值,此時直接使用簡單的排序(例如冒泡)便可,並且從效率的角度來考慮這也是合理的,由於能夠避免函數調用的開銷。
    }

4、進一步優化

  上述三種快排,在處理重複數的時候,效率並無很大提升,所以,咱們能夠想辦法優化。

  1.當待排序序列長度分割到必定大小後,使用插入排序。

   緣由:對於很小和部分有序的數組,快排不如插排好。當待排序序列的長度分割到必定大小後,繼續分割的效率比插入排序要差,此時可使用插排而不是快排

if (high - low + 1 < 10)  
{  
    InsertSort(arr,low,high);  
    return;  
}//else時,正常執行快排  

  2.在一次分割結束後,能夠把與Key相等的元素聚在一塊兒,繼續下次分割時,不用再對與key相等元素分割(處理重複效率極高) 

  舉例:

    待排序序列 1 4 6 7 6 6 7 6 8 6

    三數取中選取樞軸:下標爲4的數6

    轉換後,待分割序列:6 4 6 7 1 6 7 6 8 6  樞軸key:6

    本次劃分後,未對與key元素相等處理的結果:1 4 6 6 7 6 7 6 8 6

    下次的兩個子序列爲:1 4 6 和 7 6 7 6 8 6

    本次劃分後,對與key元素相等處理的結果:1 4 6 6 6 6 6 7 8 7

    下次的兩個子序列爲:1 4 和 7 8 7

    通過對比,咱們能夠看出,在一次劃分後,把與key相等的元素聚在一塊兒,能減小迭代次數,效率會提升很多

  具體過程:在處理過程當中,會有兩個步驟

    第一步,在劃分過程當中,把與key相等元素放入數組的兩端

    第二步,劃分結束後,把與key相等的元素移到樞軸周圍

  舉例:

    待排序序列 1 4 6 7 6 6 7 6 8 6

    三數取中選取樞軸:下標爲4的數6

    轉換後,待分割序列:6 4 6 7 1 6 7 6 8 6  樞軸key:6

    第一步,在劃分過程當中,把與key相等元素放入數組的兩端

    結果爲:6 4 1 6(樞軸) 7 8 7 6 6 6

    此時,與6相等的元素全放入在兩端了

    第二步,劃分結束後,把與key相等的元素移到樞軸周圍

    結果爲:1 4 66(樞軸)  6 6 6 7 8 7

    此時,與6相等的元素全移到樞軸周圍了

    以後,在1 4 和 7 8 7兩個子序列進行快排

  代碼示例:

void QSort(int arr[],int low,int high)  //三數中值+彙集相等元素
{
    int first = low;
    int last = high;

    int left = low;
    int right = high;

    int leftLen = 0;
    int rightLen = 0;

    if (high - low + 1 < 10)
    {
        InsertSort(arr,low,high);
        return;
    }
    
    //一次分割
    int key = SelectPivotMedianOfThree(arr,low,high);//使用三數取中法選擇樞軸
        
    while(low < high)
    {
        while(high > low && arr[high] >= key)
        {
            if (arr[high] == key)//處理相等元素
            {
                swap(arr[right],arr[high]);
                right--;
                rightLen++;
            }
            high--;
        }
        arr[low] = arr[high];
        while(high > low && arr[low] <= key)
        {
            if (arr[low] == key)
            {
                swap(arr[left],arr[low]);
                left++;
                leftLen++;
            }
            low++;
        }
        arr[high] = arr[low];
    }
    arr[low] = key;

    //一次快排結束
    //把與樞軸key相同的元素移到樞軸最終位置周圍
    int i = low - 1;
    int j = first;
    while(j < left && arr[i] != key)
    {
        swap(arr[i],arr[j]);
        i--;
        j++;
    }
    i = low + 1;
    j = last;
    while(j > right && arr[i] != key)
    {
        swap(arr[i],arr[j]);
        i++;
        j--;
    }
    QSort(arr,first,low - 1 - leftLen);
    QSort(arr,low + 1 + rightLen,last);
}

  緣由:在數組中,若是有相等的元素,那麼就能夠減小很多冗餘的劃分。這點在重複數組中體現特別明顯啊。

   3.優化遞歸操做 

  快排函數在函數尾部有兩次遞歸操做,咱們能夠對其使用尾遞歸優化

  優勢:若是待排序的序列劃分極端不平衡,遞歸的深度將趨近於n,而棧的大小是頗有限的,每次遞歸調用都會耗費必定的棧空間,函數的參數越多,每次遞歸耗費的空間也越多。優化後,能夠縮減堆棧深度,由原來的O(n)縮減爲O(logn),將會提升性能。

void QSort(int arr[],int low,int high)  
{   
    int pivotPos = -1;  
    if (high - low + 1 < 10)  
    {  
        InsertSort(arr,low,high);  
        return;  
    }  
    while(low < high)  
    {  
        pivotPos = Partition(arr,low,high);  
        QSort(arr,low,pivot-1);  
        low = pivot + 1;  
    }  
} 

  

參考文獻

  http://blog.sina.com.cn/s/blog_5a3744350100jnec.html

  http://www.blogjava.net/killme2008/archive/2010/09/08/331404.html

  http://www.cnblogs.com/cj723/archive/2011/04/27/2029993.html

  http://blog.csdn.net/zuiaituantuan/article/details/5978009

  http://blog.csdn.net/ljianhui/article/details/16797431

相關文章
相關標籤/搜索