在一亿个数中查找最大的k个数(k << 1,000,000,000)

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

点赞