1. 程式人生 > >資料結構與演算法C++之快速排序(續)

資料結構與演算法C++之快速排序(續)

上一篇部落格資料結構與演算法C++之快速排序介紹了快速排序演算法。
但是上面實現的快速排序有兩個缺點:
(一)對於近乎有序的陣列,演算法的計算複雜度由O(nlogn)退化到O(n2)
(二)如果陣列中存在大量重複的元素,那麼演算法的計算複雜度也會退化到O(n2)

(一)對於近乎有序的陣列,演算法的計算複雜度由O(nlogn)退化到O(n2)

使用上篇部落格裡的快速排序演算法做測試,測試程式為

int main()
{
    int n = 50000;
    //int *arr = generateRandomArray(n, 0, n);
    int *arr = generateNearlyOrderedArray
(n, 100);//生成只有200個無序元素的陣列 int *arr2 = copyIntArray(arr, n); testSorting("MergeSorting", MergeSorting, arr, n); testSorting("quickSorting", quickSorting, arr2, n); delete[] arr;//最後刪除陣列開闢的空間 delete[] arr2; return 0; }

測試結果
在這裡插入圖片描述
可以看出對於一個近乎有序的5萬個元素的陣列,快速排序用了 0.545s,比歸併排序慢了很多倍快速排序演算法中,我們一般將陣列最左邊的元素作為參考元素,這樣的話,當陣列近乎有序的時候,對整個陣列進行partition後,左邊小於參考元素的個數會很少,近乎沒有,右邊大於參考元素的個數將會接近整個陣列元素個數,在遞迴過程中,每層的partition都將近乎只有一部分,如下圖所示
在這裡插入圖片描述


解決方案很簡單,就是隨機選取參考元素,具體實現如下

#include <iostream>

#ifndef _SORTINGHELP_H_
#define _SORTINGHELP_H_
#include "SortingHelp.h"
#endif // _SORTINGHELP_H_

#include "MergeSorting.h"

using namespace std;

//對arr[l...r]進行partition操作
//返回p,使得arr[l...p-1] < arr[p]; arr[p+1...r] > arr[p]
template<typename T>
int __partition(T arr[], int l, int r){ //隨機找一個元素與最左邊元素進行交換位置 swap(arr[l], arr[rand()%(r-l+1)+l]); T v = arr[l]; int j = l; //arr[l+1...j] < v; arr[j+1...i) > v for(int i = l + 1; i <= r; i++){ if (arr[i] < v){ swap(arr[i], arr[j + 1]); j++; } } swap(arr[l], arr[j]); return j; } //對arr[l...r]部分進行排序 template<typename T> void __quickSorting(T arr[], int l, int r){ if (l >= r) return; srand(time(NULL)); int p = __partition(arr, l, r); __quickSorting(arr, l, p - 1); __quickSorting(arr, p + 1, r); } template<typename T> void quickSorting(T arr[], int n){ __quickSorting(arr, 0, n-1); } int main() { int n = 50000; //int *arr = generateRandomArray(n, 0, n); int *arr = generateNearlyOrderedArray(n, 100);//生成只有200個無序元素的陣列 int *arr2 = copyIntArray(arr, n); testSorting("MergeSorting", MergeSorting, arr, n); testSorting("quickSorting", quickSorting, arr2, n); delete[] arr;//最後刪除陣列開闢的空間 delete[] arr2; return 0; }

輸出為
在這裡插入圖片描述
隨機設定參考元素後,對於近乎有序的陣列,快速排序演算法的速度恢復正常了

(二)如果陣列中存在大量重複的元素,那麼演算法的計算複雜度也會退化到O(n2)

首先測試一下當陣列中有大量重複元素時,演算法的執行時間

int main()
{
    int n = 50000;
    //生成5萬個只有0-10的陣列,這樣肯定會有很多重複的元素
    int *arr = generateRandomArray(n, 0, 10);
    int *arr2 = copyIntArray(arr, n);
    testSorting("MergeSorting", MergeSorting, arr, n);
    testSorting("quickSorting", quickSorting, arr2, n);
    delete[] arr;//最後刪除陣列開闢的空間
    delete[] arr2;
    return 0;
}

輸出為
在這裡插入圖片描述
可以看出使用上面隨機設定參考元素的方法,計算時間還是很長
由下圖可以看出,當存在大量重複元素時,還是會出現partition後左右不平衡的現象
在這裡插入圖片描述
在上篇部落格的演算法中遍歷陣列元素時是從左到右依次遍歷,遇到大於參考元素的值不操作,遇到小於參考元素的值將其與第一個大於參考元素的值相交換,下面換一種策略
在這裡插入圖片描述
如上圖所示,同時從陣列兩邊進行遍歷,首先還是隨機設定一個參考元素 v v ,然後與最左邊元素進行位置交換
(1)索引為 i i 的元素從左向右遍歷,如果遍歷的元素小於 v v ,那麼就繼續遍歷,如果大於等於 v v ,那麼就停止
(2)此時索引為 j j 的元素從右向左遍歷,如果遍歷的元素大於 v v ,那麼就繼續遍歷,如果小於等於 v v ,那麼就停止
(3)將索引為 i i 的元素與索引為 j j 的元素交換位置,此時左右兩邊又都符合條件了, i i j j 就繼續開始遍歷
這樣的話,即使有大量重複的元素,也會不斷交換分散到左右兩邊( i i j j 交換時有三種可能,一、 i i j j 都等於 v v ;二、 i i 等於 v v j j 不等於 v v ;三、 i i 不等於 v v j j 等於 v v ),不會出現左右不平衡的現象

上述改進原理也可檢視部落格

實現程式如下

#include <iostream>

#ifndef _SORTINGHELP_H_
#define _SORTINGHELP_H_
#include "SortingHelp.h"
#endif // _SORTINGHELP_H_

#include "MergeSorting.h"

using namespace std;

//對arr[l...r]進行partition操作
//返回p,使得arr[l...p-1] < arr[p]; arr[p+1...r] > arr[p]
template<typename T>
int __partition(T arr[], int l, int r){

    //隨機找一個元素與最左邊元素進行交換位置
    swap(arr[l], arr[rand()%(r-l+1)+l]);
    T v = arr[l];
    int j = l;
    //arr[l+1...j] < v; arr[j+1...i) > v
    for(int i = l + 1; i <= r; i++){
        if (arr[i] < v){
            swap(arr[i], arr[j + 1]);
            j++;
        }
    }
    swap(arr[l], arr[j]);
    return j;
}

//對arr[l...r]部分進行排序
template<typename T>
void __quickSorting(T arr[], int l, int r){
    if (l >= r)
        return;

    srand(time(NULL));
    int p = __partition(arr, l, r);
    __quickSorting(arr, l, p - 1);
    __quickSorting(arr, p + 1, r);

}


template<typename T>
void quickSorting(T arr[], int n){

    __quickSorting(arr, 0, n-1);

}

////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////
//下面是改進後的快速排序演算法

//對arr[l...r]進行partition操作
//返回p,使得arr[l...p-1] < arr[p]; arr[p+1...r] > arr[p]
template<typename T>
int __partition2(T arr[], int l, int r){

    //隨機找一個元素與最左邊元素進行交換位置
    swap(arr[l], arr[rand()%(r-l+1)+l]);
    T v = arr[l];

    //arr[l+1...i)<=v; arr(j...r]>=v
    int i = l + 1;
    int j = r;
    while(true){
        while(arr[i] < v && i <= r) i++;
        while(arr[j] > v && j >= l + 1) j--;
        if (i > j) break;
        swap(arr[i], arr[j]);
        i ++;
        j --;
    }
    swap(arr[l], arr[j]);
    return j;
}

//對arr[l...r]部分進行排序
template<typename T>
void __quickSorting2(T arr[], int l, int r){
    if (l >= r)
        return;

    srand(time(NULL));
    int p = __partition2(arr, l, r);
    __quickSorting2(arr, l, p - 1);
    __quickSorting2(arr, p + 1, r);

}

template<typename T>
void quickSorting2(T arr[], int n){

    __quickSorting2(arr, 0, n-1);

}

int main()
{
    int n = 50000;
    int *arr = generateRandomArray(n, 0, 10);
    //int *arr = generateNearlyOrderedArray(n, 100);//生成只有200個無序元素的陣列
    int *arr2 = copyIntArray(arr, n);
    int *arr3 = copyIntArray(arr, n);
    testSorting("MergeSorting", MergeSorting, arr, n);
    testSorting("quickSorting", quickSorting, arr2, n);
    testSorting("quickSorting2", quickSorting2, arr3, n);
    delete[] arr;//最後刪除陣列開闢的空間
    delete[] arr2;
    delete[] arr3;
    return 0;
}

輸出為
在這裡插入圖片描述
可以看出使用左右兩個方向都遍歷,已經可以對有大量重複元素的陣列進行快速排序