寻找最大的K个数

来源:互联网 发布:哪个电视直播软件清晰 编辑:程序博客网 时间:2024/05/17 01:28

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

一、

首先,第一反应往往会想到快速排序后,再输出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)。

   代码如下:

void GetMaxKNumbers(int* input, int n, int* output, int k){    if(input == NULL || output == NULL || k > n || n <= 0 || k <= 0)        return;    int start = 0;    int end = n - 1;    int index = Partition(input, n, start, end);    while(index != k - 1)    {        if(index > k - 1)        {            end = index - 1;            index = Partition(input, n, start, end);        }        else        {            start = index + 1;            index = Partition(input, n, start, end);        }    }    for(int i = 0; i < k; ++i)        output[i] = input[i];}int Partition(int data[], int length, int start, int end){    if(data == NULL || length <= 0 || start < 0 || end >= length)        throw new std::exception("Invalid Parameters");    int index = RandomInRange(start, end);    Swap(&data[index], &data[end]);    //samll为小于end随机数的数字,index为遍历索引,当index发现比end小的数时,就与small交换,使所有small小于end    int small = start - 1;    for(index = start; index < end; ++ index)    {        if(data[index] < data[end])        {            ++ small;            if(small != index)                Swap(&data[index], &data[small]);        }    }    ++ small;    Swap(&data[small], &data[end]);    return small;}int RandomInRange(int min, int max){    int random = rand() % (max - min + 1) + min;    return random;}void Swap(int* num1, int* num2){    int temp = *num1;    *num1 = *num2;    *num2 = temp;}

三、

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

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

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

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

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

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

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

四、

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

其代码如下:

typedef multiset<int, greater<int> >            intSet;typedef multiset<int, greater<int> >::iterator  setIterator;void GetLeastNumbers_Solution2(const vector<int>& data, intSet& leastNumbers, int k){    leastNumbers.clear();    if(k < 1 || data.size() < k)        return;    vector<int>::const_iterator iter = data.begin();    for(; iter != data.end(); ++ iter)    {        if((leastNumbers.size()) < k)            leastNumbers.insert(*iter);        else        {            setIterator iterGreatest = leastNumbers.begin();            if(*iter < *(leastNumbers.begin()))            {                leastNumbers.erase(iterGreatest);                leastNumbers.insert(*iter);            }        }    }}

由于只扫描所有数据一次,时间复杂度为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个网页数中他们所占的比例成正比,从而反映,用户对关键词的强调程度。



0 0
原创粉丝点击