TOP K 问题

一、Top K问题(可以采取的方法有哪些,各自优点?)

1、直接全部排序(只适用于内存够的情况)

当数据量较小的情况下,内存中可以容纳所有数据。则最简单也是最容易想到的方法是将数据全部排序,然后取排序后的数据中的前K个。这种方法对数据量比较敏感,当数据量较大的情况下,内存不能完全容纳全部数据,这种方法便不适应了。即使内存能够满足要求,该方法将全部数据都排序了,而题目只要求找出top K个数据,所以该方法并不十分高效,不建议使用。

 

2.类选择排序法

为什么叫类选择排序法呢?因为这种方法很像选择排序,选择排序是抽出序列中的最大或最小值放在一端,这里也类似。算法思路:对目标序列N个数遍历,取出其中最大的数最为Top1;再次遍历剩下的N-1个数,取出其中最大的数为Top2;....再对剩下的N-K+1个数遍历,取出其中最大的数为TopK,这样就可以找到最大的K个数了。

vector<int> TopKBySelect(vector<int>& nums,int k,int len)
{
    vector<int>res;
 
    vector<int>flag(len);
 
    for(int i=0;i<k;i++)
    {
        int maxIndex=0;   //保存最大数的索引
        int maxNum=nums[0];  //保存最大数
 
        for(int j=0;j<len;j++)
        {
            if(nums[j]>maxNum&&!flag[j])  //如果大于最大数并且没有被取出来过
            {
                maxNum=nums[j];
                maxIndex=j;
 
            }
        }
        flag[maxIndex]=-1;    //将此次遍历的最大数索引标记为-1,放置再次被取出
        res.push_back(maxNum);  //存入该最大数
    }
 
    return res;
}

 

        时间复杂度方面,要求TopK就需要进行K次遍历,然后取出其中最大的数,因此算法平均时间复杂度为O(N*K);

        空间复杂度方面,可以看到这种方法需要开辟一个辅助空间来对取出过的元素进行标记,因此空间复杂度为O(N),除此之外,还需注意到的是,这种方法有效的前提是提前将所有数读入,这样如果一开始的N较大,那么空间开销是不可忽视的,而且,如果数据是动态的,即是可能会不停的增加新数据,那么就还需要每插入一个新数据就将其与前面取出的TopK做比较,排除K+1个数中最小的,最后剩下的才是TopK。
 

3、快速排序的变形 (只使用于内存够的情况)

这种方法类似于快速排序,首先选择一个基准,将比这个基准大的元素放到它的前面,比划分元小的元素放到它的后面,此时完成了一趟排序。

如果此时这个划分元的序号index刚好等于K,那么这个划分元以及它左边的数,刚好就是前K个最大的元素;

如果index > K,那么前K大的数据在index的左边,那么就继续递归的从index-1个数中进行一趟排序;

如果index < K,那么再从划分元的右边继续进行排序,直到找到序号index刚好等于K为止。再将前K个数进行排序后,返回Top K个元素。这种方法就避免了对除了Top K个元素以外的数据进行排序所带来的不必要的开销。

int Select_K_Min (int *ar,int n,int k)

{

         if(ar == NULL || n < 1 || k<1 || k > n)

                   return -1;

         else

                   return Select_K(ar,0,n-1,k);

}


int Select_K(int *ar,int left,int right,int k)

{

         if(left == right && k == 1) return ar[left];


         int pos = Partition(ar,left,right);

         int j = pos - left + 1;

         if(k <= j) return Select_K(ar,left,pos,k);

         else return Select_K(ar,pos+1,right,k - j);

}

 

     这种方法是利用了快速排序中找分割点的方法,每次分割后的数组大小近似为原数组大小的一半,因此这种方法的时间复杂度实际上是O(N)+O(N/2)+O(N/4)+……<O(2N),因此时间复杂度为O(N),时间复杂度虽然低,但是这种方法也需要提前将N个数读入,空间开销是一笔负担,并且对于动态的数据放入也是比较“死板”的。

4、最小堆法

这是一种局部淘汰法。先读取前K个数,建立一个最小堆。然后将剩余的所有数字依次与最小堆的堆顶进行比较,如果小于或等于堆顶数据,则继续比较下一个;大于堆顶,删除堆顶元素,并将新数据插入堆中,重新调整最小堆。当遍历完全部数据后,最小堆中的数据即为最大的K个数。

void adjustMinHeap(vector<int>& nums,int root,int len) //小顶堆结点调整
{
    int lch=2*root+1;  //左子结点
    int rch=lch+1;   //右子结点
    int index=root;  //较大结点
 
    if(rch<len&&nums[rch]<nums[index])index=rch; 
 
    if(lch<len&&nums[lch]<nums[index])index=lch;
 
    if(index!=root) //当前结点非最小结点
    {
        swap(nums[index],nums[root]);
        adjustMinHeap(nums,index,len);
    }
    return;
}
 
vector<int> TopKInHeap(vector<int>& nums,int k,int len)
{
    vector<int>res(nums.begin(),nums.begin()+k); //取出前k个数
 
    for(int i=k/2-1;i>=0;i--)  //根据前K个数建立一个小顶堆
    {
        adjustMinHeap(res,i,k);
    }
 
    //将剩下的数与堆顶做比较
    for(int i=k;i<len;i++)
    {
        if(nums[i]>res[0])  //当前数比堆顶数大
        {
            res[0]=nums[i]; //将堆顶更新为该数
            adjustMinHeap(res,0,k); //重新调整堆
        }
    }
 
    return res;
}

    根据堆排序的复杂度,不难得出,在该方法中,首先需要对K个元素进行建堆,时间复杂度为O(K);然后对剩下的N-K个数对堆顶进行比较及更新,最好情况下当然是都不需要调整了,那么时间复杂度就只是遍历这N-K个数的O(N-K),这样总体的时间复杂度就是O(N),而在最坏情况下,N-K个数都需要更新堆顶,每次调整堆的时间复杂度为logK,因此此时时间复杂度就是NlogK了,总的时间复杂度就是O(K)+O(NlogK)≈O(NlogK)。空间复杂度是O(1)。值得注意的是,堆排序法提前只需读入K个数据即可,可以实现来一个数据更新一次,能够很好的实现数据动态读入并找出TopK。
 

5、分治法

将全部数据分成N份,前提是每份的数据都可以读到内存中进行处理,找到每份数据中最大的K个数。此时剩下NK个数据,如果内存不能容纳NK个数据,则再继续分治处理,分成M份,找出每份数据中最大的K个数,如果M*K个数仍然不能读到内存中,则继续分治处理。直到剩余的数可以读入内存中,那么可以对这些数使用快速排序的变形或者归并排序进行处理。

6、Hash法

如果这些数据中有很多重复的数据,可以先通过hash法,把重复的数去掉。这样如果重复率很高的话,会减少很大的内存用量,从而缩小运算空间。处理后的数据如果能够读入内存,则可以直接排序;否则可以使用分治法或者最小堆法来处理数据。

总结:

 https://blog.csdn.net/qq_28114615/article/details/86231822

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值