高級排序算法(二)

快速排序(Quick Sort)

算法思想:在待排序表L[1...n]中任取一個元素pivot作為基準,通過一趟排序將帶排序表劃分為獨立的兩部分L[1...k-1]和L[k+1...n],使得L[1...k-1]中所有元素小于pivot,L[k+1...n]中所有元素大于或等于pivot,則pivot放在了其最終位置L(k)上,這個過程稱為一趟快速排序。而后分別遞歸地對兩個子表重復上述過程,直至每部分內只有一個元素或空為止,即所有元素放在了最終的位置上。

算法演示:

快速排序

基本代碼如下:

// 對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) {

    T v = arr[l];

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

// 對arr[l...r]部分進行快速排序
template<typename T>
void __quickSort(T arr[], int l, int r) {

    if (l >= r)
        return;

    int p = __partition(arr, l, r);
    __quickSort(arr, l, p - 1);
    __quickSort(arr, p + 1, r);
}

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

    __quickSort(arr, 0, n - 1);
}

好了,按照慣例我們將同時調用快速排序和歸并排序進行測試,看看哪種算法的運行效率更高,其結果如下(隨機數據):

Quick Sort : 0.029 s
Merge Sort : 0.03 s

我們從結果中發現快速排序算法的運行效率與優化后的歸并排序算法的效率不相上下。不知大家有沒有發現歸并排序算法前“優化后”這三個字加粗顯示,這么做其實是為了想告訴大家,我們的快速排序算法還有優化的空間,而歸并排序算法已是目前最優的結果。

那么,我們將怎么樣優化快速排序呢?我們先回想一下對歸并排序的優化操作,在歸并排序算法中,我們針對數據較少時采用插入排序,類似的,我們也可以進行這樣的操作。

有一個重要的問題,我們始終忽略了。這就是我們沒有測試在近乎有序的隨機數據情況下,兩種排序算法的運行效率。大家若學習過數據結構這門課程就會知道,快速排序有個致命的缺陷——在處理近乎有序的隨機數據時,其時間復雜度會變為O(n2)。

為什么會成這種結果呢?這是因為我們在對待排序表進行劃分時,不像歸并排序算法一樣一分為二,而是先找到一個元素pivot作為基準,使得待排序列表在基準之前的元素均小于它,在基準后面的元素均大于它。當快速排序算法處理近乎有序的隨機數據時,這種劃分操作就會類似于冒泡排序算法的處理操作,所以在這種情況下時間復雜度就變為O(n2)。

為了解決這種問題,我們就不再采用選用待排序表第一個元素作為基準(請大家不要被嚴奶奶版的數據結構中快速排序算法的講解所束縛,推薦在學習數據結構時翻閱“黑皮書”對數據結構做進一步了解),而選用待排序列表中盡可能中間的元素作為基準,即隨機選擇一個數。(注:這里不過多敘述其原因,具體請參考算法導論或“黑皮”版數據結構與算法分析。)

改進后的快速排序算法基本代碼如下:

// 對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];

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

// 對arr[l...r]部分進行快速排序
template<typename T>
void __quickSort(T arr[], int l, int r) {

    if (l >= r)
        return;

    int p = __partition(arr, l, r);
    __quickSort(arr, l, p - 1);
    __quickSort(arr, p + 1, r);
}

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

    srand(time(NULL));
    __quickSort(arr, 0, n - 1);
}

讓我們看看運行的結果吧(近乎有序的隨機數據)。

Quick Sort : 0.035 s
Merge Sort : 0.005 s

我們發現快速排序算法的運行效率雖比上歸并排序,但其性能已經遠遠好于優化前的性能。

除此之外,我們的快速排序還可進行優化。例如在含有大量重復數據的情況下,我們的快速排序算法的運行效率依舊不高。這里我們向大家展示一下快速排序算法的龜速!

首先,我們按如下代碼修改main()中的代碼:

int main() {

    int n = 100000;
    int *arr_1 = SortTestHelper::generateRandomArray(n, 0, 10);
    int *arr_2 = SortTestHelper::copyIntArray(arr_1, n);

    SortTestHelper::testSort("Quick Sort", quickSort, arr_1, n);
    SortTestHelper::testSort("Merge Sort", mergeSort, arr_2, n);

    delete[] arr_1;
    delete[] arr_2;
    return 0;
}

然后我們運行程序,看看其運行結果:

Quick Sort : 1.459 s
Merge Sort : 0.017 s

在處理含有大量重復數據時,快速排序算法的運行效率可稱為龜速啊!這是因為我們在對待排序表進行劃分操作時,由于數據中含有大量的重復數據,會有很大概率上將待排序表劃分得極度不平衡,從而導致快速排序算法退化為時間復雜度為O(n2)。

那么對于這種情況,我們優化思路的算法演示為:

實際上,圖中兩側的數據應該是如下圖所示:

二路快速排序

優化的基本代碼如下:

// 對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, j = r;
    while (true) {
        while (i <= r && arr[i] < v) i++;
        while (j >= l + 1 && arr[j] > v) 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 __quickSort2(T arr[], int l, int r) {

    if (l >= r)
        return;

    int p = __partition2(arr, l, r);
    __quickSort2(arr, l, p - 1);
    __quickSort2(arr, p + 1, r);
}

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

    // 設置當前的時間值為種子,那么種子總是變化的,所以以該種子產生的隨機數總是變化的
    srand(time(NULL));
    __quickSort2(arr, 0, n - 1);
}

那我們來看看這次優化后的結果吧。

Quick Sort : 1.487 s
Merge Sort : 0.018 s
Quick Sort 2 : 0.016 s

通過這次優化,我們的快速排序算法的運行效率有了顯著地提升。實際上,我們將這種方式的快速排序算法稱之為雙路快速排序算法。除此之外,在處理含有大量重復數據的數據時,我們還有一個更為經典的快速排序算法,通常我們將其稱為三路快速排序算法。

其實這個排序算法的思路很簡單,其算法演示如下圖所示:

三路快速排序

三路快速排序算法的基本代碼如下:

// 三路快速排序
// 將arr[l...r]分為 < v; == v; > v 三部分
// 之后遞歸對 < v; > v 兩部分進行三路快速排序
template<typename T>
void __quickSort3(T arr[], int l, int r) {

    if (l >= r)
        return;

    // partition操作
    swap(arr[l], arr[rand() % (r - l + 1) + l]);
    T v = arr[l];

    // arr[l+1...lt] < v
    int lt = l;
    // arr[gt...r] > v
    int gt = r + 1;
    // arr[lt+1...i) == v
    int i = l + 1;

    while (i < gt) {
        if (arr[i] < v) {
            swap(arr[i], arr[lt + 1]);
            lt++;
            i++;
        } else if (arr[i] > v) {
            swap(arr[i], arr[gt - 1]);
            gt--;
        } else {
            // arr[i] == v
            i++;
        }
    }
    swap(arr[l], arr[lt]);

    __quickSort3(arr, l, lt - 1);
    __quickSort3(arr, gt, r);
}

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

    // 設置當前的時間值為種子,那么種子總是變化的,所以以該種子產生的隨機數總是變化的
    srand(time(NULL));
    __quickSort3(arr, 0, n - 1);
}

好了,我們調用一些三路快速排序算法并運行程序,其結果如下:

Quick Sort : 1.393 s
Merge Sort : 0.018 s
Quick Sort 2 : 0.015 s
Quick Sort 3 : 0.006 s
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 一、直接插入排序 直接插入排序(Insertion Sort)的基本思想是:每次將一個待排序的元素記錄,按其關鍵字...
    kevin16929閱讀 579評論 0 0
  • 概述 排序有內部排序和外部排序,內部排序是數據記錄在內存中進行排序,而外部排序是因排序的數據很大,一次不能容納全部...
    蟻前閱讀 5,223評論 0 52
  • 概述:排序有內部排序和外部排序,內部排序是數據記錄在內存中進行排序,而外部排序是因排序的數據很大,一次不能容納全部...
    每天刷兩次牙閱讀 3,747評論 0 15
  • 情人玫瑰和愛情 在我心中卻是骯臟煽情與交易 我不再相信神圣 因為失去了神圣的你
    雨野閱讀 261評論 4 6
  • 一天,一家三口出門。我習慣性地對兒子說:“寶貝,你的這件羽絨太薄了,適合在家穿,外面冷,去換一件厚的吧。”兒子嘟囔...
    簡遐思閱讀 465評論 0 3