在一大堆數中求其前k大或前k小的問題,簡稱top-k問題。而目前解決top-k問題最有效的演算法即是bfprt演算法,其又稱為中位數的中位數演算法,該演算法由blum、floyd、pratt、rivest、tarjan提出,最壞時間複雜度為o(n)
在首次接觸top-k問題時,我們的第一反應就是可以先對所有資料進行一次排序,然後取其前k即可,但是這麼做有兩個問題:
(1):快速排序的平均複雜度為o(nlogn)
,但最壞時間複雜度為o(n2)
,不能始終保證較好的複雜度。
(2):我們只需要前k大的,而對其餘不需要的數也進行了排序,浪費了大量排序時間。
除這種方法之外,堆排序也是乙個比較好的選擇,可以維護乙個大小為k的堆,時間複雜度為o(nlogk)
那是否還存在更有效的方法呢?受到快速排序的啟發,通過修改快速排序中主元的選取方法可以降低快速排序在最壞情況下的時間複雜度(即bfprt演算法),並且我們的目的只是求出前k,故遞迴的規模變小,速度也隨之提高。下面來簡單回顧下快速排序的過程,以公升序為例:
(1):選取主元(首元素,尾元素或乙個隨機元素);
(2):以選取的主元為分界點,把小於主元的放在左邊,大於主元的放在右邊;
(3):分別對左邊和右邊進行遞迴,重複上述過程。
bfprt演算法步驟如下:
(1):選取主元;
(1.1):將n個元素劃分為⌊n5⌋
個組,每組5個元素,若有剩餘,捨去;
(1.2):使用插入排序找到⌊n5⌋
個組中每一組的中位數;
(1.3):對於(1.2)中找到的所有中位數,呼叫bfprt演算法求出它們的中位數,作為主元;
(2):以(1.3)選取的主元為分界點,把小於主元的放在左邊,大於主元的放在右邊;
(3):判斷主元的位置與k的大小,有選擇的對左邊或右邊遞迴。
上面的描述可能並不易理解,先看下面這幅圖:
bfprt()呼叫getpivotindex()和partition()來求解第k小,在這過程中,getpivotindex()也呼叫了bfprt(),即getpivotindex)和bfprt()為互遞迴的關係。
下面為**實現,其所求為前k小的數:
/**
* bfprt演算法(前k小數問題)
** author 劉毅(limer)
* date 2017-01-25
* mode c++
*/#include#includeusing 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, const int & k); //求第k小,返回其位置的下標
int main()
; 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;
}/* 插入排序,返回中位數下標 */
int insertsort(int array, int left, int right)
return ((right - left) >> 1) + left;
}/* 返回中位數的中位數下標 */
int getpivotindex(int array, int left, int right)
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[divide_index], array[right]); //最後把基準換回來
return divide_index;
}int bfprt(int array, int left, int right, const int & k)
bfprt演算法在最壞情況下的時間複雜度是o(n)
,下面予以證明。令t(n)
為所求的時間複雜度,則有:
t(n)≤t(n5)+t(7n10)+c⋅n(c為乙個正常數)
其中:來自getpivotindex(),n個元素,5個一組,共有⌊n5⌋
設t(n)=t⋅n
,其中t為未知,它可以是乙個正常數,也可以是乙個關於n的函式,代入上式:
t⋅ntt≤t⋅n5+7t⋅n10+c⋅n≤t5+7t10+c≤10c(兩邊消去n)(再化簡)(c為乙個正常數)
其中c為乙個正常數,故t也是乙個正常數,即t(n)≤10c⋅n,因此t(n)=o(n),至此證明結束。
接下來的更有意思的話題就是bfprt演算法為何選5作為分組基準,為何不是2,3,7,9呢?
首先排除偶數,對於偶數我們很難取捨其中位數,而奇數很容易。
再者對於3而言,會有t(n)≤t(n3)+t(2n3)+c⋅n
,它本身還是操作了n個元素,與以5為基準的9n10
相比,其複雜度並沒有減少。
對於,7,9,…而言,對於上式中的10c,其整體都會增加,所以與5相比,5更適合。
[1] 演算法導論(第3版)
[2] 演算法設計與分析基礎(第3版)
[3] wikipedia. median of medians
[4] acdreamers. bfprt 演算法
[5] noalgo. bfprt演算法
**我的個人部落格,原文為:
BFPRT演算法詳解
在一大堆數中求其前k大或前k小的問題,簡稱top k問題。而目前解決top k問題最有效的演算法即是bfprt演算法,其又稱為中位數的中位數演算法,該演算法由blum floyd pratt rivest tarjan提出,最壞時間複雜度為o n 在首次接觸top k問題時,我們的第一反應就是可以先...
bfprt演算法解析
首先講一下bfprt演算法是幹嘛的?bfprt演算法是用來求陣列中第k小的元素的演算法,bfprt演算法可以在o n 時間內求出答案。對於求陣列中第k小的元素的問題,我們已經有很好的常規演算法了,這個演算法在最好的情況下時間複雜度是o n 但在最壞的情況下是o n 2 的,其實bfprt演算法就是在...
Top K演算法問題的實現
前奏 中,後來為了論證類似快速排序中partition的方法在最壞情況下,能在o n 的時間複雜度內找到最小的k個數,而前前後後updated了10餘次。所謂功夫不負苦心人,終於得到了乙個想要的結果。簡單總結如下 詳情,請參考原文第三章 1 randomized select,以序列中隨機選取乙個元...