BFPRT算法:求數組中第k小(大)的元素

BFPRT

bfprt算法是用來求數組中第k小的元素的算法,bfprt算法可以在O(n)時間內求出答案。

算法思想


對於求數組中第k小的元素的問題,我們已經有很好的常規算法了,這個算法在最好的情況下時間複雜度是O(n),但在最壞的情況下是O(n^2)的,其實bfprt算法就是在這個基礎上改進的。

常規解法


我們隨機在數組中選擇一個數作爲劃分值(number),然後進行快排的partation過程(將小於number的數放到數組左邊,等於number的數放到數組中間,大於number的數放到數組右邊),然後判斷k與等於number區域的相對關係,如果k正好在等於number區域,那麼數組第k小的數就是number,如果k在等於number區域的左邊,那麼我們遞歸對左邊再進行上述過程,如果k在等於number區域的右邊,那我們遞歸對右邊再進行上述過程。

對於常規解法,我們分析一下它的時間複雜度:

遞歸函數複雜度計算:

T(N)=a*T(N/b)+o(N^d);
當log(b,a)>d時   複雜度爲O(N^log(b,a));
當log(b,a)=d時   複雜度爲O(N^d*log(2,N));
當log(b,a)<d時   複雜度爲O(N^d);
N爲樣本數據個數  即數組中元素的個數。
N/b爲將這N個數劃分爲更小的部分,每個部分的樣本數據個數,一般爲均分,那麼b等於2。
a爲劃分爲多個部分後,小部分執行的次數。
d爲遞歸函數調用完成後剩下的操作的時間複雜度。

對於最好的情況:每次所選的number正好在數組的正中間,那麼上式中a等於1,b等於2,對於partation過程,時間複雜度是O(n),所以d等於1。所以T(N)= T(N/2)+ O(N),此時 log( 2 , 1 ) < 1,故時間複雜度爲O(N)。

對於最壞情況:每次所選的number正好在數組最邊上,那麼時間複雜度爲O(N ^ 2).

bfprt算法就是在這個number上做文章,bfprt算法能夠保證每次所選的number在數組的中間位置,那麼時間複雜度就是O(N)。

bfprt解法:


bfprt解法和常規解法唯一不同的就是在number的選取上,其他地方一模一樣,所以我們只講選取number這一過程。

第一步:我們將數組每5個相鄰的數分成一組,後面的數如果不夠5個數也分成一組。

第二步:對於每組數,我們找出這5個數的中位數,將所有組的中位數構成一個median數組(中位數數組)。

第三步:我們再求這個中位數數組中的中位數,此時所求出的中位數就是那個number。

第四步:通過這個number進行partation過程,下面和常規解法就一樣了。

接下來我們分析一下爲什麼bfprt算法每次選number的時候都能夠在數組的中間位置。

我們假設這就是分出來的每5個數的小組,每一列代表一個小組。

圖中紅框內的數我們假設就是每一組的中位數。我們假設總數組的數字個數是N,那麼中位數數組中數字的個數就是 N / 5 。

我們假設用藍框框起來的數是中位數數組的中位數(divide),那麼由中位數的性質可知,中位數數組中有一半的數比這個divide大,所以總共有 N / 10個數比這個divide大。

用紫色框框出的數肯定也是比divide大,所以至少有N / 10 + ( 2*N ) / 10 = ( 3*N ) / 10 個數比divide大,那麼以divide爲劃分的partation過程能夠使得divide在數組的靠近中間的位置,最壞情況也能夠在數組的 ( 3*N ) / 10 或者 ( 7*N ) / 10 的位置。時間複雜度爲O(N)。本文轉自https://blog.csdn.net/qq_40938077/article/details/81213820#commentsedi

代碼如下:見BFPRT算法GITHUB

#include<bits/stdc++.h>
using namespace std;
int a[2];
int bfprt(int root[],int begin,int end,int k);//這是核心函數,表示在root數組的begin位置到end位置上找出第k小的元素
int getmedian(int root[],int beginI,int endI)//這個函數求的是在root數組中beginI位置到endI位置上的中位數(其實就是求由每5個數所分成的小組的小組內的中位數)
{
    sort(root+beginI,root+endI+1);
    int sum=beginI+endI;
    int mid=(sum/2)+(sum%2);//這個地方加上sum%2是爲了確保偶數個數時我們求的是中間兩個數的後一個
    return root[mid];
}
int medianOfMedians(int root[],int star,int finish)//這個函數是將root數組star位置到finish位置分成每5個數一個小組,並求出每個小組內的中位數
{
    int num=finish-star+1;//求出長度
    int offset=num%5==0?0:1;//最後如果剩餘的數不足5個,我們也將其分成一個小組,和前面同等對待
    int range=num/5+offset;
    int median[range];//這個數組存的是每個小組內的中位數
    for(int i=0;i<range;i++)//依次往median數組中填數
    {
        int beginI=star+i*5;//第i組數對應root數組上的位置
        int endI=beginI+4;
        median[i]=getmedian(root,beginI,min(endI,finish));
    }
    return bfprt(median,0,range-1,range/2);//求出生成好的median數組的中位數,作爲partation函數的劃分值
}
void swap(int root[],int a,int b)
{
    int temp=root[a];
    root[a]=root[b];
    root[b]=temp;
}
void partation(int root[],int beginJ,int endJ,int number)//partation函數求的是等於number的範圍
{
    int less=beginJ-1;
    int more=endJ+1;
    int cur=beginJ;
    while (cur<more)
    {
        if(root[cur]<number)
        {
            less++;
            swap(root,cur,less);
            cur++;
        }
        else if(root[cur]==number)
            cur++;
        else
        {
            more--;
            swap(root,cur,more);
        }
    }
    a[0]=less+1;
    a[1]=more-1;
}
int bfprt(int root[],int begin,int end,int k)
{
    if(begin==end)//數組中只有一個數時,直接返回
        return root[begin];
    int divide=medianOfMedians(root,begin,end);//求出以哪個數作爲劃分值
    partation(root,begin,end,divide);//注意,進行完partation過程後,root數組已經不是無序的了
    if(k>=a[0]&&k<=a[1])//如果需要求的數正好在等於區域,直接返回root[k]
        return root[k];
    else if(k<a[0])//此時我們要找的數比divide小,遞歸求前半部分
        return bfprt(root,begin,a[0]-1,k);
    else if(k>a[1])//此時我們要找的數比divide大,遞歸求後半部分
        return bfprt(root,a[1]+1,end,k);
}
int main()
{
    int n,k;
    int root[100000];
    while (cin>>n)
    {
        cin>>k;
        for(int i=0;i<n;i++)
            cin>>root[i];
        cout<<bfprt(root,0,n-1,k-1)<<endl;
        memset(root,0,sizeof(root));
        memset(a,0,sizeof(a));
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章