1. 程式人生 > 其它 >遞迴分治 --- 例題5. 線性時間選擇

遞迴分治 --- 例題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

歡迎大家訪問我的個人部落格喬治的程式設計小屋,和我一起體驗一下養成式程式設計師的打怪升級之旅吧!