在一億個數中查找最大的k個數(k

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章