这是一道很普遍和基础的题目,有很深的扩展性。
一、
首先,第一反应往往会想到快速排序后,再输出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个网页数中他们所占的比例成正比,从而反映,用户对关键词的强调程度。