第K小的数 K-th Smallest Element

第K小的数 K-th Smallest Element

   这个问题的解决方法很多啊,一下列举一下

 

1 基于排序的传统方法 O(nlogn) 

  如果你只需要找一次第k小的数,这不是一个好方法。。。如果你要找n次,还是用排序吧

 

2 Las Vegas概率算法

  也可以称之为随机化算法,其算法的特点优点类似于快速排序,都是需要选择pivot,然后进行划分。把一个序列划分为3部分,x 小于x 和大于x的。。然后根据k的取值,进行递归计算。

  毕竟是LasVegas算法,其最坏时间复杂度很差,是O(n^2)。。当然,平均复杂度非常好 O(n) 如果学过随机化算法,证明不难!

3 基于r划分的O(n)算法

   这个算法可以说是一个trick,他的思想就是想优化LasVegas算法中的pivot选择。。所以才会有这个算法。假设我们把r取成5的话,以每5个元素为子集划分源数组,然后对每个子集排序求出中位数,这样的中位数集合组成一个新的序列M,对M递归的求解其中位数p,得到的p即为划分点。p将源数组划分为3部分:
  * s1:所有小于p的元素
  * s2: 所有等于p的元素
  * s3: 所有大于p的元素
下一轮迭代的时候根据输入的level参数判断该剪去哪部分元素。

这样,每一轮迭代至少剪去n/4的元素(画个中位数排列的图出来就容易看出),则最坏情况下的时间复杂度表示为:

T(n)<=T(3n/4)+T(n/5)+O(n)

其中

  * T(3n/4)是下一轮迭代的最大数据量情况下的时间复杂度
  * T(n/5)是求寻找中位数组成集合的中位数的时间复杂度

 

利用归纳法,可以很清楚的证明出来T(n) <=20cn  (c为常数)

  基于r划分的算法,其最坏时间复杂度是O(n)。。感觉是不是很优秀!但是,优秀的代价是你的常数因子太大了!!! 

  平时在选择算法的时候,基本上可以说是一边倒的选择Las Vegas算法!

此外,在解决问题的时候,最好看清楚题目的情景,如果题目要求n个数中的第k个数,但是k取从1-n。。。那么还是用基于排序的吧。。。毕竟做了O(nlogn)的工作之后,每次都是O(1)搞定。。而LasVegas算法和基于r划分的方法都要O(n^2)

基于LasVegas算法的代码:

#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
#define Max 10000
int cnt=0;
int select( int *a, int b, int e, int k)          //从b到e
{
    cnt++;
    if (b==e) return a[b];
    int x=a[b+rand()%(e-b+1)],i=b,j=e;
    i--;j++;
    while (i<j)
    {
        while (a[++i] < x); while (a[--j] > x);
        if (i<j)std::swap(a[i],a[j]);
    }
    if (j==e)j--;i=j-b+1;
    if (k <=i) return select(a,b,j,k);
    else return select(a,j+1,e,k-i);
}

int main()
{
    int a[Max];
    for(int i=0;i<Max;i++)
        a[i]=i;
    random_shuffle(a,a+Max);
    int b[Max];
    for(int i=0;i<Max;i++)
        b[i]=a[i];
    cout<<endl;
    vector<int> C;
    for(int i=0;i<Max;i++)
    {
        cnt=0;
        select(a,0,Max-1,i);
        for(int i=0;i<Max;i++)
            a[i]=b[i];
        C.push_back(cnt);
    }
    sort(C.begin(),C.end());
    cout<<C[0]<<endl;
    cout<<C[C.size()-1]<<endl;

    return 0;

}

从代码结果可以看出LasVegas算法表现还是很优秀的,毕竟10000个数,最深递归层次是35层!

 

此外,当然求第k小元素的各种复杂数据结构也可以做,而且效率也可以,看第四种方法:  (这种方法等以后再总结吧。。)

4 树状数组实现查找K小的元素

回顾树状数组的定义,注意到有如下两条性质:
一,c[ans]=sum of A[ans-lowbit(ans)+1 ... ans];
二,当ans=2^k时,
c[ans]=sum of A[1 ... ans];

下面说明findK(k)如何运作:
1,设置边界条件ans,ans'<maxn且cnt<=k;
2,初始化cnt=c[ans],其中ans=2^k且k为满足边界条件的最大整数;
3,找到满足边界条件的最大的ans'使得ans'-lowbit(ans')=ans,即ans'满足c[ans']=A[ans+1 .. ans'](根据性质一),只要将c[ans']累加到cnt中(此时cnt=sum of A[1 ... ans'],根据性质二),cnt便可以作为k的逼近值;
4,重复第3步直到cnt已无法再逼近k,此时ans刚好比解小1,返回ans+1。

因此findk(k)的实质就是二分逼近

/**********************************
树状数组实现查找K小的元素
                  经典。
限制:数据范围在1<<20 以内
***********************************/
#include <iostream>
using namespace std;
#define maxn 1<<20
int n,k;
int c[maxn];
int lowbit(int x){
return x&-x;
}
void insert(int x,int t){
while(x<maxn){
          c[x]+=t;
          x+=lowbit(x);    
       }
}
int find(int k){
int cnt=0,ans=0;
for(int i=20;i>=0;i--){
        ans+=(1<<i);
if(ans>=maxn || cnt+c[ans]>=k)ans-=(1<<i);
else cnt+=c[ans];
    }
return ans+1;
}
void input(){
       memset(c,0,sizeof(c));
int t;
       scanf("%d%d",&n,&k);
for(int i=0;i<n;i++){    
            scanf("%d",&t);
            insert(t,1);
       }
       printf("%d\n",find(k));
}
int main(){
int cases;
    scanf("%d",&cases);
while(cases--){
        input();
    }
return 0;
}

你可能感兴趣的:(第K小的数 K-th Smallest Element)