一:背景介紹
在一堆數中求其前k大或前k小的問題,簡稱TOP-K問題。而目前解決TOP-K問題最有效的演算法即是”BFPRT演算法”,又稱為”中位數的中位數演算法”,該演算法由Blum、Floyd、Pratt、Rivest、Tarjan提出,最壞時間複雜度為$O(n)$。
在首次接觸TOP-K問題時,我們的第一反應就是可以先對所有資料進行一次排序,然後取其前k即可,但是這麼做有兩個問題:
- 快速排序的平均複雜度為$O(nlogn)$,但最壞時間複雜度為$O(n^2)$,不能始終保證較好的複雜度。
- 我們只需要前k大的,而對其餘不需要的數也進行了排序,浪費了大量排序時間。
除這種方法之外,堆排序也是一個比較好的選擇,可以維護一個大小為k的堆,時間複雜度為$O(nlogk)$。
那是否還存在更有效的方法呢?受到快速排序的啟發,通過修改快速排序中主元的選取方法可以降低快速排序在最壞情況下的時間複雜度,並且我們的目的只是求出前k,故遞迴的規模變小,速度也隨之提高。下面來簡單回顧下快速排序的過程,以升序為例:
(1):選取主元(陣列中隨機一個元素);
(2):以選取的主元為分界點,把小於主元的放在左邊,大於主元的放在右邊;
(3):分別對左邊和右邊進行遞迴,重複上述過程。
二:BFPRT演算法過程及程式碼
BFPRT演算法步驟如下:
(1):選取主元;
(1.1):將n個元素劃分為$⌊frac n5⌋$個組,每組5個元素,若有剩餘,捨去;
(1.2):使用插入排序找到$⌊frac n5⌋$個組中每一組的中位數;
(1.3):對於(1.2)中找到的所有中位數,呼叫BFPRT演算法求出它們的中位數,作為主元;
(2):以(1.3)選取的主元為分界點,把小於主元的放在左邊,大於主元的放在右邊;
(3):判斷主元的位置與k的大小,有選擇的對左邊或右邊遞迴。
上面的描述可能並不易理解,先看下面這幅圖:
BFPRT()呼叫GetPivotIndex()和Partition()來求解第k小,在這過程中,GetPivotIndex()也呼叫了BFPRT(),即GetPivotIndex)和BFPRT()為互遞迴的關係。
下面為程式碼實現,其所求為前K小的數:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
/** * BFPRT演算法(前K小數問題) * * author : 劉毅(Limer) * date : 2017-01-25 * mode : C++ */ #include <iostream> #include <algorithm> using namespace std; /* 插入排序,返回中位數下標 */ 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--]; array[j + 1] = temp; } return ((right - left) >> 1) + left; } /* 返回中位數的中位數下標 */ int BFPRT(int array[], int left, int right, const int & k); 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]); // 依次放在左側 } return BFPRT(array, left, sub_right, ((sub_right - left + 1) >> 1) + 1); } /* 利用中位數的中位數的下標進行劃分,返回分界線下標 */ int Partition(int array[], int left, int right, int pivot_index) { swap(array[pivot_index], array[right]); // 把主元放置於末尾 int divide_index = left; // 跟蹤劃分的分界線 for (int i = left; i < right; i++) { if (array[i] < array[right]) swap(array[divide_index++], array[i]); // 比主元小的都放在左側 } swap(array[divide_index], array[right]); // 最後把主元換回來 return divide_index; } int BFPRT(int array[], int left, int right, const int & k) { int pivot_index = GetPivotIndex(array, left, right); // 得到中位數的中位數下標 int divide_index = Partition(array, left, right, pivot_index); // 進行劃分,返回劃分邊界 int num = divide_index - left + 1; if (num == k) return divide_index; else if (num > k) return BFPRT(array, left, divide_index - 1, k); else return BFPRT(array, divide_index + 1, right, k - num); } int main() { int k = 5; int array[10] = { 1,1,2,3,1,5,-1,7,8,-10 }; cout << "原陣列:"; for (int i = 0; i < 10; i++) cout << array[i] << " "; cout << endl; cout << "第" << k << "小值為:" << array[BFPRT(array, 0, 9, k)] << endl; cout << "變換後的陣列:"; for (int i = 0; i < 10; i++) cout << array[i] << " "; cout << endl; return 0; } |
執行如下:
三:時間複雜度分析
四:參考文獻
- 演算法導論(第3版). Page 120.