算法导论学习之线性时间求第k小元素+堆思想求前k大元素

对于以前,如果要我求第k小元素,或者是求前k大元素,我可能会将元素先排序,然后就直接求出来了,但是现在有了更好的思路。

一.线性时间内求第k小元素
这个算法又是一个基于分治思想的算法。其具体的分治思路如下:
1.分解:将A[p,r]分解成A[p,q-1]和A[q+1,r]两部分,使得A[p,q-1]都小于A[q],A[q+1,r]都不小于A[q];
2.求解:如果A[q]恰好是第k小元素直接返回,如果第k小元素落在前半区间就到A[p,q-1]递归查找,否则到A[q+1,r]中递归查找。
3.合并:这个问题不需要合并。
其对应的代码如下:

int RandomziedSelect(int *a,int p,int r,int k)
{
    if(p==r)///如果当前区间只剩一个元素,那么这个元素一定就是我们要求的
        return a[p];
    int q=RandomParatition(a,p,r);  ///随机划分函数
    int x=q-p+1;///求出a[p,q]之间的长度
    if(x==k) ///a[q]恰好是第k小元素
        return a[q];
    if(x>k)  ///x小于k说明第k小元素在a[p,q-1]之间
        return RandomziedSelect(a,p,q-1,k);
    else  ///x大于k说明第k小元素在a[q+1,r]之间,而且是这个区间的第k-x小元素
        return RandomziedSelect(a,q+1,r,k-x);
}

其实这个过程很类似于快排,但是为什么快排的时间复杂度是O(nlgn),而这个算法的时间复杂度只有O(n)?主要的原因在于这个算法每次只要处理分解以后一半的区间,而不像快排那样两边都要处理。当然这只是一个简单的分析,更具体数学分析在这里就不说了。其实我们也可以利用堆的性质来求出第k小元素,只要我们建立一个最小堆后然后再调整k-1次就行了,这样时间复杂度是O(n)+O((k-1)lgn)。

下面给出一份完整的代码:

#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<ctime>
#include<fstream>
using namespace std;

int Paratition(int *a,int p,int r)
{
    int key=a[r];
    int i=p-1;
    for(int j=p;j<r;j++)
        if(a[j]<key)
        {
            i++;
            swap(a[i],a[j]);
        }
    swap(a[i+1],a[r]);
    return i+1;
}

int RandomParatition(int *a,int p,int r)
{
    int x=rand()%(r-p+1)+p;///产生[p,r]之间的随机数
    swap(a[x],a[r]);  ///交换a[x]和a[r]的值,其实就是将a[x]作为划分的关键值
    return Paratition(a,p,r);
}

int RandomziedSelect(int *a,int p,int r,int k)
{
    if(p==r)///如果当前区间只剩一个元素,那么这个元素一定就是我们要求的
        return a[p];
    int q=RandomParatition(a,p,r);  ///随机划分函数
    int x=q-p+1;///求出a[p,q]之间的长度
    if(x==k) ///a[q]恰好是第k小元素
        return a[q];
    if(x>k)  ///x小于k说明第k小元素在a[p,q-1]之间
        return RandomziedSelect(a,p,q-1,k);
    else  ///x大于k说明第k小元素在a[q+1,r]之间,而且是这个区间的第k-x小元素
        return RandomziedSelect(a,q+1,r,k-x);
}

int main()
{
    int b[100];
    ifstream fin("lkl.txt");
    int n,k;
    //cout<<"请输入n,k: ";
    fin>>n>>k;
   //cout<<"请输入"<<n<<"个元素: "<<endl;
    for(int i=1;i<=n;i++)
        fin>>b[i];
    int ans=RandomziedSelect(b,1,n,k);
    sort(b+1,b+n+1);
    for(int i=1;i<=n;i++)
        cout<<b[i]<<" ";
    cout<<endl;
    cout<<"第"<<k<<"小元素为: "<<ans<<endl;
  return 0;
}

二.利用堆求前k大元素
这个算法的思想比较简单: 如果我们要求n个元素中前k大的元素,我们就先将这n个元素中的前k个元素建立一个最小堆,然后从k+1。。。n依次判断,如果某个元素大于堆中最小的元素,我们就将其替代堆中的最小元素,并且调整一下堆。这样将所有元素都检查完了之后,堆中的k个元素也就是这n个元素中的前k大元素了。时间复杂度O(k)+O((n-k)lgk)。

代码如下

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<fstream>
using namespace std;

#define maxn 100

///最小堆调整函数
void MinHeadfly(int *a,int i,int HeadSize)
{
    int l=i*2,r=2*i+1;
    int largest;
    if(a[i]>a[l]&&l<=HeadSize)
        largest=l;
    else
        largest=i;
    if(a[largest]>a[r]&&r<=HeadSize)
        largest=r;
    if(largest!=i)
    {
        swap(a[i],a[largest]);
        MinHeadfly(a,largest,HeadSize);
    }
}

///最小堆建立函数
void MinHeadBuild(int *a,int n)
{
    for(int i=n/2;i>=1;i--)
        MinHeadfly(a,i,n);
}

///最小堆排序函数,从大到小排序
void MinHeadSort(int *a,int HeadSize)
{
    int k=HeadSize;
    for(int i=HeadSize;i>=2;i--)
    {
        swap(a[i],a[1]);
        k--;
        MinHeadfly(a,1,k);
    }
}

///求b数组的前k大元素
void prek(int *a,int *b,int n,int k)
{
    MinHeadBuild(a,k);
    for(int i=k+1;i<=n;i++)
        if(b[i]>a[1])
        {
            a[1]=b[i];
            MinHeadfly(a,1,k);
        }
    MinHeadSort(a,k);
    cout<<"前"<<k<<"大元素为:"<<endl;
    for(int i=1;i<=k;i++)
        cout<<a[i]<<" ";
    cout<<endl;
}

int a[maxn],b[maxn];

int main()
{
    ifstream fin("lkl.txt");
    int n,k;
    //cout<<"请输入n,k: ";
    fin>>n>>k;
   //cout<<"请输入"<<n<<"个元素: "<<endl;
    for(int i=1;i<=n;i++)
    {
        fin>>b[i];
        if(i<=k)
            a[i]=b[i];
    }
    prek(a,b,n,k);
  return 0;
}

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。