遞迴分治 --- 例題5. 線性時間選擇
一.問題描述
給定線性序列集中的n個元素和正整數k, 1≤ k≤ n, 求第k小元素的位置.
二.解題思路
該篇文章中我們討論與排序問題類似的元素選擇問題,元素選擇問題的一般提法是:給定線性序集合中n個元素和一個整數k(1 <= k <= n),要求找出這n個元素中第k小的元素,即如果將這n個元素依其線性排列數時,排在第k個位置的元素即為要找的元素.
①最直觀的解題方法:
先排序,然後找到第k小的元素即可.這樣時間複雜度至少是: O(n), 平均: O(nlogn)時間
②隨機選擇演算法:
模仿快速排序對輸入陣列A進行二分劃分,使子陣列A1的元素<=A2中的元素,分點J由隨機數產生,若K<=J,則K為A1的第K小元, 若K>J,則K為A2的第K-J小元.
與快速排序演算法不同,它只對子陣列之一進行遞迴處理
按照上面的思路,我們先用上篇文章說到過的隨機選擇演算法來確定基準,從而對陣列進行劃分.
程式碼描述:
template<class T> int randomizedPartition(vector<T> &a, int p, int r) //在p:r中隨機選擇一個數i { srand(int(time(0))); //每次以當前時間作為隨機數生成的種子 int i = random(p, r); swap(a[i], a[p]); //將a[i]換到左端點 return Partition(a, p, r); } Type RandomizedSelect(Type a[], int p, int r, int k) { if(p == r) return a[p]; int i = RandumizePartition(a, p, r); //使用了快速排序那篇文章中的RandomziedPartition隨機劃分演算法 j = i-p+1; //j為a[p:i]中的元素個數 if(k <= j) return RandomizedSelect(a, p, i, k); else return RandomizedSelect(a, i+1, r, k-j); }
時間複雜度分析:
-
若分點總是等分點,則有: Tmin(n) = d
Tmin(n) = T(n/2) + cn, 計算得到T(n) = θ(n)
-
若一部分總是為空,則有: Tmax(n) = O(n^2)
從上面可以看到,我們若果使用隨機劃分的話,最壞情況下,演算法RandomizedSelect時間複雜度達到了O(n^2),例如,我們在找最小元素時,總是在最大元素處劃分.儘管如此,該演算法的效能還是可以的.
隨機選擇演算法的最佳情況即線性選擇!!!
③線性時間選擇演算法(證明比較繞,可以直接記住結論:每次選擇的基準為中位數的中位數,這樣可以保證我們每次遞迴劃分問題規模縮小1/4.當元素個數小於閾值的話,我們直接用sort排序)
我們可以通過尋找一個好的劃分基數,可使最壞情況下時間為O(n).
下面我們來介紹一下如何劃分可以達到目標,具體步驟如下:
可以按以下步驟找到滿足要求的劃分基準,(「」表示向下取整,符號打不出來見諒.)
- 將n個輸入元素劃分成「n/5」個組,每組5個元素,除可能有一個組不是5個元素外。用任意一種排序演算法,將每組中的元素排好序,並取出每組的中位數,共「n/5」個。
- 遞迴呼叫Select找出這「n/5」個元素的中位數。如果「n/5」是偶數,就找它的兩個中位數中較大的一個。然後以這個元素作為劃分基準
下圖是上述劃分策略的示意圖,其中n個元素用小圓點來表示,空心小圓點為每組元素的中位數。中位數的中位數x在圖中標出。圖中所畫箭頭是由較大元素指向較小元素的。
如下圖所示:
為了簡化問題,我們先設所有元素互不相同.在這種情況下,找出的基準x至少比3「(n-5)/10」個元素大,因為在每組中有兩個元素小於本組的中位數,而「n/5」箇中位數中又有「(n-5)/10」個小於基準x.同理可得基準x至少比3「(n-5)/10」個元素小.
圖中紅色框起來的就是必然小於基準的元素,藍色框起來的就是必然大於基準的元素.
而當n>=75時,3「(n-5)/10」 >= n/4.所以,按照此基準劃分所得的兩個子陣列的長度都至少縮短1/4,這一點是至關重要的.
下面,據此我們來計算一下時間複雜度.
設對n個元素的陣列呼叫Select需要T(n)時間,那麼找中位數的中位數x至多用T(n/5)的時間.現已證明,按照演算法所選的基準x進行劃分所得到的兩個子陣列分別至多由3n/4個元素,所以無論對哪一個子陣列進行呼叫Select都至多需要T(3n/4)時間.(這就是為什麼要選擇中位數的中位數為基準)
故可得到關於T(n)的遞迴式:
- T(n) <= C1 , n<75
- T(n) <= C2n + T(n/5) + T(3n/4), n>=75
解此遞迴式可得到:T(n) = O(n)
程式碼如下:
#include<bits/stdc++.h>
using namespace std;
const int N = 1e8+10;
int p[N];
int Partition(int a[], int low, int high, int x)
{
int key = x;
while(low<high)
{
while(low<high&&a[high]>key) --high;
a[low] = a[high];
while(low<high&&a[low]<=key) ++low;
a[high] = a[low];
}
a[low] = key;
return low;
}
// 線性時間選擇
int select(int a[], int left, int right, int k) //找出第k小的數
{
if(right-left<75) //元素個數小於閾值,直接sort排序
{
sort(a, a+right-left+1); //如果元素個數不夠多,選擇簡單排序演算法排序
return a[left+k-1];
}
for(int i=0; i<=(right-left-4)/5; ++i) //(right-left-4)/5表示分成多少組,即有多少箇中位數,即(n-5)/5
{
sort(a+left+5*i, a+left+5*i+4); //每五個元素一組,分別排序
swap(a[left+i*5+2], a[left+i]); //將每一組的中位數調到整個陣列最前面
}
int x = select(a, left, left+(right-left-4)/5, (right-left-4)/10); //x為中位數的中位數
int loc = Partition(a, left, right, x), j = loc-left+1; //j表示一半陣列的元素個數
if(k <= j) return select(a, left, loc, k); //針對所有元素互不相等的情況,如果有元素相等的話,需要再加操作.
else return select(a, loc+1, right, k-j);
}
int main()
{
cout<<"線性時間選擇"<<endl;
int n, k;
cout<<"請輸入陣列大小以及要查詢第幾小的元素:";
while(cin>>n>>k && n)
{
memset(p, 0, sizeof(p));
cout<<"請輸入陣列元素:";
for(int i=0; i<n; ++i)
scanf("%d", &p[i]);
printf("%d\n", select(p, 0, n-1, k));
cout<<"請輸入陣列大小以及要查詢第幾小的元素:";
}
// // 做一個測試:
// // srand(time(0)); //隨機數生成的種子
// for(int i=0; i<1000000; ++i)
// {
// p[i] = rand()%1000000;
// }
// cout<<"資料量:1000000"<<endl;
// clock_t start, end;
// clock_t start2, end2;
// start = clock();
// int ans = select(p, 0, 999999, 1324);
// for(int i=0; i<1326; i++) cout<<p[i]<<" ";
// cout<<"第1324小的個數據為:"<<ans<<endl;
// end = clock();
// cout<<"線性時間選擇演算法用時: "<<double(end-start)/CLOCKS_PER_SEC<<endl;
// start2 = clock();
// sort(p, p+999999);
// cout<<"第1324個小的資料為:"<<p[1324]<<endl;
// for(int i=0; i<1326; i++) cout<<p[i]<<" ";
// end2 = clock();
// cout<<"快速排序選擇演算法用時: "<<double(end2-start2)/CLOCKS_PER_SEC<<endl;
system("pause");
return 0;
}
執行結果:
我還做了一個小小的對比,觀測兩種演算法的效率相差如何:
可以看到,當資料量為1000000時,效率差距已經逐漸顯現出來了,到了更大的計算量的話,一個好的演算法確實是能夠節省很多資源.
演算法優化過程:
演算法 | 快排 | 隨機選擇 | 線性時間選擇 |
---|---|---|---|
時間複雜度 | O(nlogn) | O(n)-O(n^2) | O(n) |
基準值 | a[p] | random(p,r) | 中位數 |
參考畢方明老師課件《演算法設計與分析》.
參考文章:https://www.cnblogs.com/xuzf/p/12556016.html
歡迎大家訪問我的個人部落格喬治的程式設計小屋,和我一起體驗一下養成式程式設計師的打怪升級之旅吧!