TopK问题求解

*问题描述*

从arr[1, n]这n个数中,找出最大的k个数,这就是经典的TopK问题。

测试用例

从5,3,7,1,8,2,9,4,7,2,6,6这n=12个数中,找出最大的k=5个。

一、直接排序

将n个数排序之后,取出最大的k个

*伪代码*

sort(arr, 1, n);

return arr[1, k];

*时间复杂度*\(O(n*lg(n))\)

#include<iostream>
#include<algorithm>
using namespace std;
bool cmp(int a,int b)
{
    return a>b;
}
int main()
{
    int a[]={5,3,7,1,8,2,9,4,7,2,6,6};
    int k=5;
    sort(a,a+12,cmp);
    for(int i=0;i<k;i++)
        cout<<a[i]<<" ";
}

*分析*:冒泡,将全局排序优化为了局部排序,非TopK的元素是不需要排序的,节省了计算资源。不少朋友会想到,需求是TopK,是不是这最大的k个元素也不需要排序呢?这就引出了第三个优化方法。

二、局部排序

冒泡是一个很常见的排序方法,每冒一个泡,找出最大值,冒k个泡,就得到TopK。

*伪代码*

for(i=1 to k){
   bubble_find_max(arr,i)
}
return arr[1, k];

*时间复杂度*\(O(n*k)\)

#include<iostream>
#include<algorithm>
using namespace std;
int main()
{
    int a[]={5,3,7,1,8,2,9,4,7,2,6,6};
    int k=5;
    int n=12;
    for(int i=0;i<k;i++)
        for(int j=i+1;j<n;j++)
            if(a[i]<a[j])
                swap(a[i],a[j]);
                
    for(int i=0;i<k;i++)
        cout<<a[i]<<" ";
}

*分析*:冒泡,将全局排序优化为了局部排序,非TopK的元素是不需要排序的,节省了计算资源。不少朋友会想到,需求是TopK,是不是这最大的k个元素也不需要排序呢?这就引出了第三个优化方法。

三、堆排序

  • 先用前k个元素生成一个小顶堆,这个小顶堆用于存储,当前最大的k个元素。

  • 接着,从第k+1个元素开始扫描,和堆顶(堆中最小的元素)比较,如果被扫描的元素大于堆顶,则替换堆顶的元素,并调整堆,以保证堆内的k个元素,总是当前最大的k个元素。

  • 直到,扫描完所有n-k个元素,最终堆中的k个元素,

伪代码:

heap[k] = make_heap(arr[1, k]);

for(i=k+1 to n){
         adjust_heap(heep[k],arr[i]);
}

return heap[k];

*时间复杂度*:O(n*lg(k))

*分析*:堆,将冒泡的TopK排序优化为了TopK不排序,节省了计算资源。堆,是求TopK的经典算法,那还有没有更快的方案呢?

#include<iostream>
#include<algorithm>
#include<queue>
using namespace std;
priority_queue<int,vector<int>,greater<int>> Q;
void printQ(priority_queue<int,vector<int>,greater<int> > q)
{
    while(q.size())
    {
        cout<<q.top()<<" ";
        q.pop();
    }
    cout<<endl;
}
int main()
{
    int a[]={5,3,7,1,8,2,9,4,7,2,6,6};
    int k=5;
    int n=12;
    for(int i=0;i<k;i++)
        Q.push(a[i]);
    for(int i=k;i<n;i++)
    {
        if(a[i]>Q.top())
        {
            Q.push(a[i]);
            Q.pop();
        }
    }
    printQ(Q);
}

四、随机选择

随机选择算在是《算法导论》中一个经典的算法,其时间复杂度为O(n),是一个线性复杂度的方法。

以上述TopK的数组为例,先用第一个元素t=arr[low]为划分依据,扫描一遍数组,把数组分成了两个半区:

  • 左半区比t大

  • 右半区比t小

  • 中间是t

partition返回的是t最终的位置i。

很容易知道,partition的时间复杂度是O(n)。

TopK是希望求出arr[1,n]中最大的k个数,那如果找到了第k大的数,做一次partition,就一次性找到最大的k个数了,然后问题变成了arr[1, n]中找到第k大的数。

再回过头来看看第一次partition,划分之后:

i = partition(arr, 1, n);

如果i大于k,则说明arr[i]左边的元素都大于k,于是只递归arr[1, i-1]里第k大的元素即可;

如果i小于k,则说明说明第k大的元素在arr[i]的右边,于是只递归arr[i+1, n]里第k-i大的元素即可;

这就是随机选择算法randomized_select,RS,其伪代码如下:

int RS(arr, low, high, k){

if(low== high) return arr[low];

i = partition(arr, low, high);

temp= i-low; //数组前半部分元素个数

if(temp>=k)
	return RS(arr, low, i-1, k); //求前半部分第k大

else
	return RS(arr, i+1, high, k-i); //求后半部分第k-i大

}

通过随机选择(randomized_select),找到arr[1, n]中第k大的数,再进行一次partition,就能得到TopK的结果。

posted @ 2024-03-24 13:24  YJQING  阅读(41)  评论(0)    收藏  举报