原文連接: https://subetter.com/algorith...
在一堆數中求其前k大或前k小的問題,簡稱TOP-K問題。而目前解決TOP-K問題最有效的算法便是BFPRT算法,又稱爲中位數的中位數算法,該算法由Blum、Floyd、Pratt、Rivest、Tarjan提出,最壞時間複雜度爲$O(n)$。html
在首次接觸TOP-K問題時,咱們的第一反應就是能夠先對全部數據進行一次排序,而後取其前k便可,可是這麼作有兩個問題:ios
除這種方法以外,堆排序也是一個比較好的選擇,能夠維護一個大小爲k的堆,時間複雜度爲$O(nlogk)$。c++
那是否還存在更有效的方法呢?咱們來看下BFPRT算法的作法。算法
在快速排序的基礎上,首先經過判斷主元位置與k的大小使遞歸的規模變小,其次經過修改快速排序中主元的選取方法來下降快速排序在最壞狀況下的時間複雜度。數組
下面先來簡單回顧下快速排序的過程,以升序爲例:函數
BFPRT算法步驟以下:spa
上面的描述可能並不易理解,先看下面這幅圖:3d
BFPRT()調用GetPivotIndex()和Partition()來求解第k小,在這過程當中,GetPivotIndex()也調用了BFPRT(),即GetPivotIndex()和BFPRT()爲互遞歸的關係。code
下面爲代碼實現,其所求爲前k小的數:htm
#include <iostream> #include <algorithm> using namespace std; int InsertSort(int array[], int left, int right); int GetPivotIndex(int array[], int left, int right); int Partition(int array[], int left, int right, int pivot_index); int BFPRT(int array[], int left, int right, int k); int main() { int k = 8; // 1 <= k <= array.size int array[20] = { 11,9,10,1,13,8,15,0,16,2,17,5,14,3,6,18,12,7,19,4 }; cout << "原數組:"; for (int i = 0; i < 20; i++) cout << array[i] << " "; cout << endl; // 由於是以 k 爲劃分,因此還能夠求出第 k 小值 cout << "第 " << k << " 小值爲:" << array[BFPRT(array, 0, 19, k)] << endl; cout << "變換後的數組:"; for (int i = 0; i < 20; i++) cout << array[i] << " "; cout << endl; return 0; } /** * 對數組 array[left, right] 進行插入排序,並返回 [left, right] * 的中位數。 */ int InsertSort(int array[], int left, int right) { int temp; int j; for (int i = left + 1; i <= right; i++) { temp = array[i]; j = i - 1; while (j >= left && array[j] > temp) { array[j + 1] = array[j]; j--; } array[j + 1] = temp; } return ((right - left) >> 1) + left; } /** * 數組 array[left, right] 每五個元素做爲一組,並計算每組的中位數, * 最後返回這些中位數的中位數下標(即主元下標)。 * * @attention 末尾返回語句最後一個參數多加一個 1 的做用其實就是向上取整的意思, * 這樣能夠始終保持 k 大於 0。 */ int GetPivotIndex(int array[], int left, int right) { if (right - left < 5) return InsertSort(array, left, right); int sub_right = left - 1; // 每五個做爲一組,求出中位數,並把這些中位數所有依次移動到數組左邊 for (int i = left; i + 4 <= right; i += 5) { int index = InsertSort(array, i, i + 4); swap(array[++sub_right], array[index]); } // 利用 BFPRT 獲得這些中位數的中位數下標(即主元下標) return BFPRT(array, left, sub_right, ((sub_right - left + 1) >> 1) + 1); } /** * 利用主元下標 pivot_index 進行對數組 array[left, right] 劃分,並返回 * 劃分後的分界線下標。 */ int Partition(int array[], int left, int right, int pivot_index) { swap(array[pivot_index], array[right]); // 把主元放置於末尾 int partition_index = left; // 跟蹤劃分的分界線 for (int i = left; i < right; i++) { if (array[i] < array[right]) { swap(array[partition_index++], array[i]); // 比主元小的都放在左側 } } swap(array[partition_index], array[right]); // 最後把主元換回來 return partition_index; } /** * 返回數組 array[left, right] 的第 k 小數的下標 */ int BFPRT(int array[], int left, int right, int k) { int pivot_index = GetPivotIndex(array, left, right); // 獲得中位數的中位數下標(即主元下標) int partition_index = Partition(array, left, right, pivot_index); // 進行劃分,返回劃分邊界 int num = partition_index - left + 1; if (num == k) return partition_index; else if (num > k) return BFPRT(array, left, partition_index - 1, k); else return BFPRT(array, partition_index + 1, right, k - num); }
運行以下:
原數組:11 9 10 1 13 8 15 0 16 2 17 5 14 3 6 18 12 7 19 4 第 8 小值爲:7 變換後的數組:4 0 1 3 2 5 6 7 8 9 10 12 13 14 17 15 16 11 18 19
BFPRT算法在最壞狀況下的時間複雜度是$O(n)$,下面予以證實。令$T(n)$爲所求的時間複雜度,則有:
$$ T(n)≤T(\frac n 5)+T(\frac {7n}{10})+c⋅n\tag{c爲一個正常數} $$
其中:
設$T(n)=t⋅n$,其中t爲未知,它能夠是一個正常數,也能夠是一個關於n的函數,代入上式:
$$ \begin{align} t⋅n&≤\frac {t⋅n}5+\frac{7t⋅n}{10}+c⋅n \tag{兩邊消去n}\\ t&≤\frac t 5+\frac {7t}{10}+c \tag{再化簡}\\ t&≤10c \tag{c爲一個正常數} \end{align} $$
其中c爲一個正常數,故t也是一個正常數,即$T(n)≤10c⋅n$,所以$T(n)=O(n)$,至此證實結束。
接下來咱們再來探討下BFPRT算法爲什麼選5做爲分組主元,而不是2, 3, 7, 9呢?
首先排除偶數,對於偶數咱們很難取捨其中位數,而奇數很容易。再者對於3而言,會有$T(n)≤T(\frac n 3)+T(\frac {2n}3)+c⋅n$,它自己仍是操做了n個元素,與以5爲主元的$\frac {9n}{10}$相比,其複雜度並無減小。對於7,9,...而言,上式中的10c,其總體都會增長,因此與5相比,5更適合。