被忽視的 partition 算法

若是你學習過算法,那麼確定據說過快速排序的大名,可是對於快速排序中用到的 partition 算法,你瞭解的夠多嗎?或許是快速排序太過於光芒四射,使得咱們每每會忽視掉一樣重要的 partition 算法。c++

Partition 可不僅用在快速排序中,還能夠用於 Selection algorithm(在無序數組中尋找第K大的值)中。甚至有可能正是這種經過一趟掃描來進行分類的思想激發 Edsger Dijkstra 想出了 Three-way Partitioning,高效地解決了 Dutch national flag problem 問題。接下來咱們一塊兒來探索 partition 算法。git

Partition 一次掃描進行劃分

Partition 實現

快速排序中用到的 partition 算法思想很簡單,首先從無序數組中選出樞軸點 pivot,而後經過一趟掃描,以 pivot 爲分界線將數組中其餘元素分爲兩部分,使得左邊部分的數小於等於樞軸,右邊部分的數大於等於樞軸(左部分或者右部分均可能爲空),最後返回樞軸在新的數組中的位置。github

Partition 的一個直觀簡單實現以下(這裏取數組的第一個元素爲pivot):算法

// Do partition in arr[begin, end), with the first element as the pivot.
int partition(vector<int>&arr, int begin, int end){
    int pivot = arr[begin];
    // Last position where puts the no_larger element.
    int pos = begin;
    for(int i=begin+1; i!=end; i++){
        if(arr[i] <= pivot){
            pos++;
            if(i!=pos){
                swap(arr[pos], arr[i]);
            }
        }
    }
    swap(arr[begin], arr[pos]);
    return pos;
}

若是原始數組爲[5,9,2,1,4,7,5,8,3,6],那麼整個處理的過程以下圖所示:數組

Partition 簡單實現

這種實現思路比較直觀,可是其實並不高效。從直觀上來分析一下,每一個小於pivot的值基本上(除非到如今爲止尚未碰見大於pivot的值)都須要一次交換,大於pivot的值(例如上圖中的數字9)有可能須要被交換屢次才能到達最終的位置。less

若是咱們考慮用 Two Pointers 的思想,保持頭尾兩個指針向中間掃描,每次在頭部找到大於pivot的值,同時在尾部找到小於pivot的值,而後將它們作一個交換,就能夠一次把這兩個數字放到最終的位置。一種比較明智的寫法以下:函數

int partition(vector<int>&arr, int begin, int end)
{
    int pivot = arr[begin];
    while(begin < end)
    {
        while(begin < end && arr[--end] >= pivot);
        arr[begin] = arr[end];
        while(begin < end && arr[++begin] <= pivot);
        arr[end] = arr[begin];
    }
    arr[begin] = pivot;
    return begin;
}

若是是第一次看到上面的代碼,那麼停下來,好好品味一下。這裏沒有用到 swap 函數,但其實也至關於作了 swap 操做。之前面的數組爲例,看看以這種方法來作的話,整個處理的流程。學習

Partition 高效實現

直觀上來看,賦值操做的次數很少,比前面單向掃描的swap次數都少,效率應該會更高。這裏從理論上對這兩種方法進行了分析,有興趣能夠看看。ui

Partition 應用

咱們都知道經典的快速排序就是首先用 partition 將數組分爲兩部分,而後分別對左右兩部分遞歸進行快速排序,過程以下:spa

void quick_sort(vector<int> &arr, int begin, int end){
    if(begin >= end - 1){
        return;
    }
    int pos = partition(arr, begin, end);

    quick_sort(arr, begin, pos);
    quick_sort(arr, pos+1, end);
}

雖然快排用到了經典的分而治之的思想,可是快排實現的前提仍是在於 partition 函數。正是有了 partition 的存在,才使得能夠將整個大問題進行劃分,進而分別進行處理。

除了用來進行快速排序,partition 還能夠用 O(N) 的平均時間複雜度從無序數組中尋找第K大的值。和快排同樣,這裏也用到了分而治之的思想。首先用 partition 將數組分爲兩部分,獲得分界點下標 pos,而後分三種狀況:

  • pos == k-1,則找到第 K 大的值,arr[pos];

  • pos > k-1,則第 K 大的值在左邊部分的數組。

  • pos < k-1,則第 K 大的值在右邊部分的數組。

下面給出基於迭代的實現:

int find_kth_number(vector<int> &arr, int k){
    int begin = 0, end = arr.size();
    assert(k>0 && k<=end);

    int target_num = 0;
    while (begin < end){
        int pos = partition(arr, begin, end);
        if(pos == k-1){
            target_num = arr[pos];
            break;
        }
        else if(pos > k-1){
            end = pos;
        }
        else{
            begin = pos + 1;
        }
    }
    return target_num;
}

該算法的時間複雜度是多少呢?考慮最壞狀況下,每次 partition 將數組分爲長度爲 N-1 和 1 的兩部分,而後在長的一邊繼續尋找第 K 大,此時時間複雜度爲 O(N^2 )。不過若是在開始以前將數組進行隨機打亂,那麼能夠儘可能避免最壞狀況的出現。而在最好狀況下,每次將數組均分爲長度相同的兩半,運行時間 T(N) = N + T(N/2),時間複雜度是 O(N)。

Partition 進階

接下來先考慮這樣一個問題,給定紅、白、藍三種顏色的小球若干個,將其排成一列,使相同顏色的小球相鄰,三種顏色前後順序爲紅,白,藍。這就是經典的 Dutch national flag problem

咱們能夠針對紅,藍,白三種顏色的球分別計數,而後根據計數結果來從新放球。不過若是咱們將問題進一步抽象,也就是說將一個數組按照某個target值分爲三部分,使得左邊部分的值小於 target,中間部分等於 target,右邊部分大於 target,這樣就不能再用簡單的計數來肯定排序後的結果。這時候,就能夠用到另外一種 partition 算法:three-way-partition。它的思路稍微複雜一點,用三個指針將數組分爲四個部分,經過一次掃描最終將數組分爲 <,=,> 的三部分,以下圖所示:

三分劃分

能夠結合下面代碼來理解具體的邏輯:

// Assume target is in the arr.
void three_way_partition(vector<int> &arr, int target){
    int next_less_pos = 0, next_bigger_pos = arr.size()-1;
    int next_scan_pos = 0;
    while (next_scan_pos <= next_bigger_pos){
        if(arr[next_scan_pos] < target){
            swap(arr[next_scan_pos++], arr[next_less_pos++]);
        }
        else if(arr[next_scan_pos] > target){
            swap(arr[next_scan_pos], arr[next_bigger_pos--]);
        }
        else{
            next_scan_pos++;
        }
    }
}

這裏的主要思想就是在一遍掃描中,經過交換不一樣位置的數字,使得數組最終能夠維持必定的順序,和前面快排中用到的 partition 思想一致。區別在於快排按照 pivot 將數組分爲兩部分,左右部分中的值均可能等於 pivot,而 three-way-partition 將數組分爲 <, =, >的三部分。

更多閱讀

Algorithms 4.0: DemoPartitioning
Dutch national flag problem
Time complexity of quick-sort in detail
Quicksort Partitioning: Hoare vs. Lomuto
數學之美番外篇:快排爲何那樣快

本文由selfboot 發表於我的博客,採用署名-非商業性使用-相同方式共享 3.0 中國大陸許可協議。
非商業轉載請註明做者及出處。商業轉載請聯繫做者本人
本文標題爲:被忽視的 partition 算法
本文連接爲:http://selfboot.cn/2016/09/01...

相關文章
相關標籤/搜索