一:背景介紹
在一大堆數中求其前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算法過程及代碼
上面的描述可能並不易理解,先看下面這幅圖:
BFPRT()調用GetPivotIndex()和Partition()來求解第k小,在這過程中,GetPivotIndex()也調用了BFPRT(),即GetPivotIndex)和BFPRT()爲互遞歸的關係。
下面爲代碼實現,其所求爲前K小的數:
/**
* 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 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()
{
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;
}
/* 插入排序,返回中位數下標 */
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 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);
}
三:時間複雜度分析
參考文獻:
[1] 算法導論(第3版)
[2] 算法設計與分析基礎(第3版)
[3] Wikipedia. Median of medians
[4] ACdreamers. BFPRT 算法
[5] NOALGO. BFPRT算法