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));
    }
}

 

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