寻找最大的K个数

这是一道很普遍和基础的题目,有很深的扩展性。

一、

首先,第一反应往往会想到快速排序后,再输出K个元素,但是时间复杂度为O(N*logN/log2) + O(K) = O(N*logN/log2)。

        另外一种做法是,通过冒泡排序选出K个最大的出来,它的时间复杂度为O(N*K)。

        这就要比较K < logN/log2 ? K : logN/log2 来取舍算法了,但是这些都不是比较好的办法。

二、

那么,进一步化简:

        快排的过程是找到随机数,将它固定到最终位置,将数据按大小分组,重复递归下去,直到最后排好序,是一种分治的思想。

        所以,我们可以找到第k个数的位置(由大到小排序),从而减少了查找交换次数,效率为O(N*logk/log2)。

   代码如下:

 1 void GetMaxKNumbers(int* input, int n, int* output, int k)
 2 {
 3     if(input == NULL || output == NULL || k > n || n <= 0 || k <= 0)
 4         return;
 5 
 6     int start = 0;
 7     int end = n - 1;
 8     int index = Partition(input, n, start, end);
 9     while(index != k - 1)
10     {
11         if(index > k - 1)
12         {
13             end = index - 1;
14             index = Partition(input, n, start, end);
15         }
16         else
17         {
18             start = index + 1;
19             index = Partition(input, n, start, end);
20         }
21     }
22 
23     for(int i = 0; i < k; ++i)
24         output[i] = input[i];
25 }
26 
27 int Partition(int data[], int length, int start, int end)
28 {
29     if(data == NULL || length <= 0 || start < 0 || end >= length)
30         throw new std::exception("Invalid Parameters");
31 
32     int index = RandomInRange(start, end);
33     Swap(&data[index], &data[end]);
34     //samll为小于end随机数的数字,index为遍历索引,当index发现比end小的数时,就与small交换,使所有small小于end
35     int small = start - 1;
36     for(index = start; index < end; ++ index)
37     {
38         if(data[index] < data[end])
39         {
40             ++ small;
41             if(small != index)
42                 Swap(&data[index], &data[small]);
43         }
44     }
45 
46     ++ small;
47     Swap(&data[small], &data[end]);
48 
49     return small;
50 }
51 
52 int RandomInRange(int min, int max)
53 {
54     int random = rand() % (max - min + 1) + min;
55     return random;
56 }
57 
58 void Swap(int* num1, int* num2)
59 {
60     int temp = *num1;
61     *num1 = *num2;
62     *num2 = temp;
63 }

三、

但上述解法有一点不好就是会修改源数据的顺序,在工程中,往往这种情况是不允许的。

那么,我们也还可以沿用此思想,通过寻找第K大的数,并在寻找过程中标记>K的数,逼近找到K,从而最终得到结果。

其主要思想可以通过二分法查找这个K,二分法有两种:

一种是:通过min + (max - min )* 0.5 的方式,求得随机值;

二种是:通过转换为二进制,判断最高位是1 或 0 来 区分,个人推荐第二种做法。

这种方法有一点不好就是,每次遍历需将>K的数进行标记,循环时在标记的范围中进行再次筛选,而标记位会占用空间。

从时间和空间复杂度上会做到O(logN/log2),但空间上平均也会用到O(logN/log2),最坏情况下为O(N)。

四、

根据三的思想,我们可以一次遍历后,讲最大的K个数进行保存,那么怎么时刻保存这最大的K个数呢?我们可以用小根堆进行保存,将比最小根大的数加入堆,并剔除最小根,从而始终保持堆为当前遍历时,最大K个。

其代码如下:

 1 typedef multiset<int, greater<int> >            intSet;
 2 typedef multiset<int, greater<int> >::iterator  setIterator;
 3 
 4 void GetLeastNumbers_Solution2(const vector<int>& data, intSet& leastNumbers, int k)
 5 {
 6     leastNumbers.clear();
 7 
 8     if(k < 1 || data.size() < k)
 9         return;
10 
11     vector<int>::const_iterator iter = data.begin();
12     for(; iter != data.end(); ++ iter)
13     {
14         if((leastNumbers.size()) < k)
15             leastNumbers.insert(*iter);
16 
17         else
18         {
19             setIterator iterGreatest = leastNumbers.begin();
20 
21             if(*iter < *(leastNumbers.begin()))
22             {
23                 leastNumbers.erase(iterGreatest);
24                 leastNumbers.insert(*iter);
25             }
26         }
27     }
28 }

由于只扫描所有数据一次,时间复杂度为O(N*logK/log2)。
可以说,在内存可容纳K时是完美的算法。

五、

 可以用基数排序的思想来统计这K个数,但是这里有些条件限制:

1、数的取值范围不能太大,不然很容易达到O(N)的空间复杂度。

2、由于需要方便对相应数字次数进行查找统计,最好用到hash函数,为避免冲突,往往hash函数空间开销都会较大。

六、

编程之美2.5中,有一些扩展问题,下面谈一下我的看法:

1、对于浮点数的寻找其实可以借鉴五中的思想。首先要根据整数部分进行hash,对大于K的整数部分的数开始统计小数部分,最终达到K个数。

2、寻找k、m,实质上是在寻找第k个小的数和第m个大的数,然后在其区间确定数值,可用方法一、方法三统计。

3、可以维护一个链表和阀值,阀值为当前K个数中最小的值,网页权值变化后,大于阀值时,替换最小的数,并更新阀值。

4、可以讲每个机器用于根据不同关键词查找出最好的K个文档(允许精确度在90%),然后再有个主调度机器,判断从110%K中选取90%综合相关的作为最终的K个网页提交。

5、对于相关的查询词,其相关文档自然是有关联的,其中通过机器学习方法,获取查询词的相关度,它跟最终K个网页数中他们所占的比例成正比,从而反映,用户对关键词的强调程度。

    

转载于:https://www.cnblogs.com/michaelGD/archive/2012/11/21/2781366.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值