在一亿个数中查找最大(小)的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没有改变,所以不会交换,导致这个应该在最大的数的集合中的数被丢失。