在一亿个数中查找最大(小)的k个数(k << 1,000,000,000),例如k=10,000。越快越好,怎么办呢?
之前跟一同事说起互联网公司的面试题,他说一般思路是先排序,然后再处理数据肯定没错。是不是这样的呢?对于这个问题,我们想想如下的几个方法:
1.使用大多数情况下最快的排序方法—快速排序来解决可以吗?思路是将一亿个数放到一个数组中,然后使用快速排序方法把最大的k个数放到数组的前k个空间里。但是,这个问题没有说(1)要排好序的k个最大的数,(2)所有一亿个数是什么样的序列。我们只要k个最大的数,并且如果这一亿个数如果刚好是从小到大的排列顺序,那么用快速排序就退化成冒泡排序,等排好序已经地老天荒了。
2.从1中我们知道排序可能会做无用功,那么我们假设这一亿个数的数组中前k个数就是最大的,然后我们循环将后面的1,000,000,000-k个数中的每个数与前面k个数中的最小的数比较,就可以将所有最大的数全部交换到前k个元素中。这样只需要一次遍历后边的数就可以找到最大的k个数了。这个方法也有缺陷,就是每次循环必须要在前k个数中查找最小的数,即每次后面的1,000,0000,000-k个数中循环一次,前面的k个数都要比较k次。可不可以继续优化呢?
3.优化2的方法,即维持前k个数基本有序,那么每次循环时,就可以在前k个数中的很小的范围内找到最小的数,例如前面的k个数最开始排序为由大到小的序列,那么我们知道前k个数中最小的数是在靠近k-1附近。但是,经过很多次比较后,前k个数也会变得无序了,就会退化成方法2。所以,在循环一定次数后,我们再将前k个数中无序的部分进行排序,这样就可以保证又可以很快地找到最小的数了。
我们来看方法3是如何实现的:
首先,排序使用C++的标准算法库函数sort,所以需要定义一个比较函数,好告诉sort如何排序:
bool gt(const int a, const int b)
{
return a > b;
}
然后是交换函数:
void swap(int *buff, const int i, const int j)
{
assert(buff);
int temp = buff[i];
buff[i] = buff[j];
buff[j] = temp;
}
最后是我们的查找函数,其中k是上文中的k,size是1,000,000,000,delta表示循环多少次后对前k个数再进行排序:
void findmaxin(int *buff, const int k, const int size, const int delta)
{
if (!buff || delta <= 0 || delta > k || k <= 0 || size <= 0 || k > size - k) {
cout << "bad parameters." << endl;
return;
}
int minElemIdx, zoneBeginIdx;
sort(buff, buff + k, gt); // 首先对前k个数进行排序
minElemIdx = k - 1; // 最小的数是第k - 1个数,数组下标从0开始计算
zoneBeginIdx = minElemIdx; // 将标记范围的变量也指向第k - 1个数,主要用于后续的排序
for (int i = k; i < size; i++) // 从第k个数开始循环
{
if (buff[i] > buff[minElemIdx]) // 从后size - k个数中找到比前k个数中最小的数大的数
{
swap(buff, i, minElemIdx); // 交换
if (minElemIdx == zoneBeginIdx)
{
zoneBeginIdx--; // 标记范围的变量往前移动
if (zoneBeginIdx < k - delta) // 无序的范围已经超过阈值了
{
sort(buff, buff + k, gt); // 再次排序
zoneBeginIdx = minElemIdx = k - 1; // 复位
continue;
}
}
int idx = zoneBeginIdx;
int j = idx + 1;
// 在标记范围内查找最小的数
for (; j < k; j++)
{
if(buff[idx] > buff[j])
idx = j;
}
minElemIdx = idx; // 将指向最小数的标志置成找到的最小数的索引
}
}
}
测试代码如下,系统是Debian 7.8,CPU是Intel Core i5 M480,内存是4GB:
#include <cstdlib>
#include <iostream>
#include <sys/times.h>
#include <unistd.h>
#include "FindMaxIn.h"
int main()
{
const int k = 10000;
const int size = 100000000;
const int delta = 400;
int *buf = NULL;
struct tms begTime, endTime;
long beg, end;
int clocks_per_sec = sysconf(_SC_CLK_TCK);
try {
buf = new int[size];
if (!buf)
return -1;
srandom(time(NULL));
for(int i = 0; i < size; i++)
buf[i] = random() % size;
beg = times(&begTime);
findmaxin(buf, k, size, delta);
end = times(&endTime);
for (int i = 0; i < k; i++)
cout << buf[i] << " ";
cout << endl;
#if 0
cout << "---------------------" << endl;
for (int i = 0; i < size; i++)
cout << buf[i] << " ";
cout << endl;
#endif
cout << "time elapsed: " << (end - beg) * 1000 / clocks_per_sec << " ms" << endl;
delete [] buf;
} catch (...) {
delete [] buf;
}
return 0;
}
测试的结果是找到这10,000个最大的数需要的时间是920ms。
本文参考了
http://blog.csdn.net/lalor/article/details/7368438,在这儿表示感谢!不过在验证的过程中发现原文中的的continue;的位置不正确。为什么?假设第一次交换前,第k-2个数是100,000,第k-1个数是99,998,而第一次在后边的数找到的第一个比99,998大的数是100,002,交换后,第k-2个数是100,000,第k-1个数是100,002,由于原文中的continue;是在if (zoneBeginIdx < k – delta)判断体后,而不是在判断体中,导致第一次交换后的minElemIdx没有指向100,000,仍然指向100,002,如果这时恰好后边有一个100,001,它本来比100,000大,但是由于minElemIdx没有改变,所以不会交换,导致这个应该在最大的数的集合中的数被丢失。