bfprt演算法是一種經典的線性時間內求最小的第k個數或者最小的k個數的演算法。
比如我們要知道銷量是前幾名商品或者是瀏覽量最多的前幾個網站,我們不需要排序,利用bfprt演算法就可以完成。
bfprt演算法又稱快速選擇演算法,其思想是利用快速排序的劃分來確定位置。此演算法經典而優雅,值得我們學習。 步驟
1.將輸入的元素劃分為5個元素一組,至多有一組不足5個元素。
2.利用插入排序尋找每組5個元素的中位元,然後再遞迴找所有中位元的中位元。
3.利用快排的劃分方法,以第二步所找的中位元為哨兵劃分。返回最後劃分的位置i,
如果i+1 = k,則返回i,
如果i+1<k,說明找到的數小了,在i+1到數組末遞迴尋找k-i-1小的數,
同理,i+1>k,在數組開始到i-1的部分遞迴尋找第i小的元素。 c語言代碼
#include <stdio.h>#define N 8 void swap(int* a, int* b) { int t = *a; *a = *b; *b = t;}//插入排序void InsertSort(int a[], int l, int r){ for(int i = l + 1; i <= r; i++) { if(a[i - 1] > a[i]) { int t = a[i]; int j = i; while(j > l && a[j - 1] > t) { a[j] = a[j - 1]; j--; } a[j] = t; } }} //尋找中位元的中位元,利用插入排序對每5個元素排序,再把每組的中位元依次交換到數組的前面,//最後再遞迴對數組前面的中位元排序,找中位元int FindMid(int a[], int l, int r){ if(l == r) return a[l]; int i = 0; int n = 0; for(i = l; i < r - 5; i += 5) { InsertSort(a, i, i + 4); n = i - l; swap(a+l + n / 5, a+i + 2); } //處理剩餘元素 int num = r - i + 1;//剩餘元素的個數 if(num > 0) { InsertSort(a, i, i + num - 1); n = i - l; swap(a+l + n / 5, a+i + num / 2); } n /= 5;//有幾組5個的數 if(n == l) return a[l]; return FindMid(a, l, l + n);} //尋找中位元的所在位置int FindId(int a[], int l, int r, int num){ for(int i = l; i <= r; i++) if(a[i] == num) return i; return -1;} //進行劃分過程int Partion(int a[], int l, int r, int p){ swap(a+p, a+l); int i = l; int j = r; int pivot = a[l]; while(i < j) { while(a[j] >= pivot && i < j) j--; a[i] = a[j]; while(a[i] <= pivot && i < j) i++; a[j] = a[i]; } a[i] = pivot; return i;} int BFPTR(int a[], int l, int r, int k){if (k<1 || k>r-l+1)return -1; int num = FindMid(a, l, r); //尋找中位元的中位元 int p = FindId(a, l, r, num); //找到中位元的中位元對應的id int i = Partion(a, l, r, p); int m = i - l + 1; if(m == k) return a[i]; if(m > k) return BFPTR(a, l, i - 1, k); return BFPTR(a, i + 1, r, k - m);} int main(){ int i, k; int a[N] = {72, 6, 57, 88, 60, 42, 83, 73}; scanf("%d", &k); printf("The %d th number is : %d\n", k, BFPTR(a, 0, N - 1, k)); for(i = 0; i < N; i++) printf("%d ", a[i]); getchar(); getchar(); return 0;}
時間複雜度證明
第一步劃分,複雜度為O(n),
第二步求中位元,對大小為O(1)的數組進行O(n)次插入排序的複雜度為O(n),遞迴求取中位元的複雜度T(n/5).
第三步,得到的中位元x作為哨兵進行劃分,在n/5個中位元中,哨兵x大於其中1/2*n/5=n/10的中位元,而每個中位元在其本來的5個數的小組中又大於或等於其中的3個數,所以哨兵x至少大於所有數中的n/10*3=3/10*n個。同理,哨兵x至少小於所有數中的3/10*n個。即劃分之後,任意一邊的長度至少為3/10*n,在最壞情況下,每次選擇都選到了7/10*n的那一部分,則遞迴的複雜度為T(7/10*n)。劃分的時間複雜度為O(n).
我們假設在每5個數求中位元和劃分的函數中,進行若干個次線性掃描,其時間複雜度為c*n,其中c為常數。
其總的時間複雜度滿足 T(n)<=T(n/5)+T(7/10*n)+c*n。
我們假設T(n)=x*n,其中x不一定是常數(比如x可以為n的倍數,則對應的T(n)=O(n^2))。則有 x*n <= x*n/5 + x*7/10*n + c*n
得到 x<=10*c
於是可以知道x與n無關,T(n)<=10*c*n,為線性時間複雜度演算法。
”為什麼要分成5個元素一組。“的證明和時間複雜度的證明一樣,假設分成k組就行了。下面看演算法導論的證明
k必須要大於4才能保證時間是線性。
由於劃分的性質,當我們找到了第k小的數時,它前面的數小於等於它,後面的數都大於等於它。
這樣我們就找到了最小的k個數。