在一億個數中查找最大(小)的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沒有改變,所以不會交換,導致這個應該在最大的數的集合中的數被丟失。