编写一个程序,从10亿个数字的数组中找出100个最大的数字

use*_*erx 298 sorting algorithm

我最近参加了一次采访,我被问到"编写一个程序,从10亿个数字中找出100个最大的数字."

我只能给出一个强力解决方案,即以O(nlogn)时间复杂度对数组进行排序并获取最后100个数字.

Arrays.sort(array);
Run Code Online (Sandbox Code Playgroud)

面试官正在寻找更好的时间复杂性,我尝试了其他一些解决方案但未能回答他.有更好的时间复杂度解决方案吗?

Ron*_*ler 326

每当遇到大于队列中最小数字(队列头部)的数字时,您可以保留100个最大数字的优先级队列,迭代十亿个数字,删除队列的头部并添加新的数字到队列.

编辑: 正如Dev所指出的,使用堆实现的优先级队列,插入队列的复杂性是O(logN)

在最糟糕的情况下,你会得到哪个更好billionlog2(100)billionlog2(billion)O(NlogK)

一般来说,如果你需要一组N个数中最大的K数,那么复杂性O(NlogN)不是i-K,当K与N相比非常小时,这可能非常重要.

EDIT2:

该算法的预期时间非常有趣,因为在每次迭代中,插入可能会也可能不会发生.第i个数字被插入队列的概率是随机变量大于至少{0, 1}来自相同分布的随机变量的概率(第一个k数被自动添加到队列中).我们可以使用订单统计(参见链接)来计算这个概率.例如,假设从中均匀地随机选择数字(i-k)/i,(iK)数的期望值(i个数之外)是1-[(i-k)/i] = k/i,并且随机变量大于该值的机会是k.

因此,预期的插入次数是:

在此输入图像描述

预计运行时间可表示为:

在此输入图像描述

(k生成具有第一个n-k元素的队列的时间,然后是log(k)/2比较,以及如上所述的预期插入次数,每个都需要平均N时间)

请注意,当K比较非常大时n,这个表达式更接近NlogK而不是O(logN).这有点直观,就像在问题的情况下,即使在10000次迭代之后(与十亿次相比非常小),数字插入队列的可能性非常小.

  • @ThomasJungblut十亿也是一个常数,所以如果是这样的话就是O(1):P (17认同)
  • @RonTeller:通常这类问题需要考虑从数十亿的谷歌搜索结果中找到10个首页,或者在文字云中找到50个最常用词,或者在MTV上找到10个最受欢迎的词,等等.所以,我相信,在正常情况下_将`k` _constant_和_small_与`n`进行比较是安全的.但是,人们应该始终牢记这种"正常情况". (9认同)
  • @RonTeller您无法有效地二进制搜索链表,这就是优先级队列通常用堆实现的原因.您描述的插入时间是O(n)而不是O(logn).你是第一次(有序队列或优先级队列),直到Skizz让你自己猜测第二次. (8认同)
  • 每个插入实际上只有*O(100)*. (6认同)
  • 由于您有1G项目,随机抽样1000个元素,并选择最大的100个.这应该避免退化情况(排序,反向排序,大多数排序),大大减少插入数量. (5认同)
  • @RonTeller维护指针数组会破坏使用链表关于添加和删除时间的优点.没有其他办法了. (2认同)
  • @Dev好的,得到你所说的,我会更新我的回答,谢谢 (2认同)
  • O(100)是无稽之谈.所有O(常数)都是O(1). (2认同)
  • @Kaz:你是对的`O(100)= O(常数)= O(1)`.你使用哪个constat并不重要,因此我没有得到关于'O(100)`的'废话'. (2认同)

jin*_*jin 135

如果在面试中询问,我认为面试官可能希望看到你的问题解决过程,而不仅仅是你对算法的了解.

描述很一般,所以也许你可以问他这些数字的范围或含义,以使问题清楚.这样做可能会给面试官留下深刻印象.例如,如果这些数字代表一个国家(例如中国)的人口年龄,那么这是一个更容易解决的问题.假设没有人活着超过200,你可以使用大小为200(可能是201)的int数组来计算一次迭代中具有相同年龄的人数.这里的指数意味着年龄.在此之后,找到100个最大的数字是一块蛋糕.顺便说一句,这个算法被称为计数排序.

无论如何,让问题更具体,更清晰,对你来说是个好消息.

  • 很好的一点.没有其他人询问或指出过这些数字的分布情况 - 它可能会对如何处理问题产生重大影响. (26认同)
  • 我想这个答案足以扩展它.一次读取数字以获取最小/最大值,以便您可以假设分布.然后,选择两个选项之一.如果范围足够小,请构建一个数组,您可以在数组发生时简单地检查数字.如果范围太大,请使用上面讨论的排序堆算法....只是一个想法. (13认同)
  • 我同意,回答面试官的问题确实有很大的不同.事实上,诸如您是否受计算能力限制的问题也可以帮助您通过使用多个计算节点来并行化解决方案. (2认同)

Reg*_*ein 69

你可以迭代O(n)的数字

只要找到大于当前最小值的值,就将新值添加到大小为100的循环队列中.

该循环队列的最小值是您的新比较值.继续添加到该队列.如果已满,请从队列中提取最小值.

  • 你无法绕过队列排队.(如果你不想每次都为下一个最小的元素搜索空洞队列) (7认同)
  • 这不起作用.例如,找到{1,100,2,99}的前2个将给出{100,1}作为前2. (3认同)
  • @ MrSmith42在堆中进行部分排序就足够了.请参阅Ron Teller的回答. (3认同)
  • 是的,我默默地假设 extract-min-queue 是作为堆实现的。 (2认同)
  • 循环队列不使用大小为 100 的最小堆,而是在顶部最少有 100 个数字。与队列情况下的 O(n) 相比,插入仅需 O(log n) (2认同)

Fre*_*ell 32

我意识到这是用'算法'标记的,但是会抛弃其他一些选项,因为它可能也应该标记为'面试'.

10亿个数字的来源是什么?如果它是一个数据库,那么'通过值desc limit 100从表顺序中选择值'可以很好地完成工作 - 可能存在方言差异.

这是一次性的,还是会重复的?如果重复,多久一次?如果它是一次性的并且数据在文件中,那么'cat srcfile | 排序(根据需要选择)| head -100'会让你快速做有成效的工作,当你的计算机处理这个琐碎的家务时,你会得到报酬.

如果重复,你会建议采取任何体面的方法来获得初始答案并存储/缓存结果,这样你就可以连续报告前100名.

最后,还有这个考虑因素.您是在寻找一份入门级的工作,并与一位令人讨厌的经理或未来的同事面谈?如果是这样,那么你可以抛弃描述相关技术优缺点的各种方法.如果你正在寻找一份更具管理性的工作,那么就像管理者那样接近它,关心解决方案的开发和维护成本,然后说"非常感谢"并离开,如果这是面试官想要专注于CS琐事.他和你在那里不太可能有太大的进步潜力.

在接下来的采访中祝你好运.

  • 特殊的答案.其他人都集中在问题的技术方面,而这种反应则解决了商业社会问题. (2认同)
  • 我从未想过你可以说谢谢你,并留下面试而不是等待它完成.谢谢你敞开心扉. (2认同)

mcd*_*lla 17

我对此的立即反应是使用堆,但是有一种方法可以使用QuickSelect,而无需在任何时候保留所有输入值.

创建一个大小为200的数组,并用前200个输入值填充它.运行QuickSelect并丢弃低100,留下100个空闲位置.读入接下来的100个输入值并再次运行QuickSelect.继续,直到您以100个批次运行整个输入.

最后,您有前100个值.对于N值,您大约运行QuickSelect N/100次.每个Quickselect的成本约为常量的200倍,因此总成本是某些常数的2N倍.这看起来与我输入的大小成线性关系,无论我在这个解释中硬接线为100的参数大小.

  • 您可以添加一个小但可能很重要的优化:运行QuickSelect以对大小为200的数组进行分区后,前100个元素中的最小值是已知的.然后,当迭代整个数据集时,如果当前值大于当前最小值,则仅填充较低的100值.在C++中使用这个算法的简单实现与libstdc ++的`partial_sort`直接运行在2亿32位`int`(通过MT19937创建,均匀分布)的数据集上相同. (10认同)
  • 这正是[Guava的](http://guava-libraries.googlecode.com)[`Ordering.greatestOf(Iterable,int)`](http://docs.guava-libraries.googlecode.com/git-history /release/javadoc/com/google/common/collect/Ordering.html#greatestOf(java.util.Iterator,%20int)).它绝对是线性时间和单通,它是一个超级可爱的算法.FWIW,我们也有一些实际的基准:它的常数因素在平均情况下比传统的优先级队列慢,但是这种实现更能抵抗"最坏情况"输入(例如严格上升的输入). (8认同)

One*_*rew 15

您可以使用快速选择算法在(按顺序)索引[十亿-101]中查找数字,然后迭代数字并查找与该数字相比的数字.

array={...the billion numbers...} 
result[100];

pivot=QuickSelect(array,billion-101);//O(N)

for(i=0;i<billion;i++)//O(N)
   if(array[i]>=pivot)
      result.add(array[i]);
Run Code Online (Sandbox Code Playgroud)

此算法时间为:2 XO(N)= O(N)(平均案例性能)

Thomas Jungblut这样的第二个选择建议是:

使用建设的最大堆需要O(N),那么前100最大的数字将在堆的顶部,所有你需要的是从堆取出来(100 XO(日志(N)).

该算法时间为:O(N)+ 100 XO(Log(N))= O(N)

  • 您正在完成整个列表三次.1生物.整数大约是4gb,如果你不能将它们放入记忆中你会怎么做?在这种情况下,quickselect是最糟糕的选择.迭代一次并保留前100项的堆是恕我直言O(n)中表现最佳的解决方案(请注意,您可以切断堆插入的O(log n),因为堆中的n为100 =常量=非常小). (8认同)
  • 即使它仍然是"O(N)",执行两次QuickSelects和另一次线性扫描的开销比所需要的要多. (3认同)

mri*_*rip 10

虽然另一个quickselect解决方案已被低估,但事实仍然是quickselect将比使用100号队列更快地找到解决方案.在比较方面,Quickselect的预期运行时间为2n + o(n).一个非常简单的实现将是

array = input array of length n
r = Quickselect(array,n-100)
result = array of length 100
for(i = 1 to n)
  if(array[i]>r)
     add array[i] to result
Run Code Online (Sandbox Code Playgroud)

这将平均进行3n + o(n)次比较.此外,使用quickselect将在最右边100个位置留下阵列中最大的100个项目这一事实可以提高效率.所以事实上,运行时间可以提高到2n + o(n).

问题在于这是预期的运行时间,而不是最坏的情况,但是通过使用适当的枢轴选择策略(例如,随机挑选21个元素,并选择那些21的中位数作为枢轴),那么比较的数量可以是对于任意小的常数c,保证最高概率为(2 + c)n.

实际上,通过使用优化的采样策略(例如,随机采样sqrt(n)元素,并选择第99百分位数),运行时间可以降低到(1 + c)n + o(n)任意小的c (假设K,要选择的元素数是o(n)).

另一方面,使用大小为100的队列将需要O(log(100)n)比较,并且100的log基数2约等于6.6.

如果我们从更大抽象意义上考虑这个问题,从大小为N的数组中选择最大的K元素,其中K = o(N)但K和N都变为无穷大,那么quickselect版本的运行时间将是O(N)和队列版本将是O(N log K),因此在这个意义上,quickselect也渐近优越.

在评论中,有人提到队列解决方案将在随机输入的预期时间N + K log N上运行.当然,除非问题明确说明,否则随机输入假设永远不会有效.可以使队列解决方案以随机顺序遍历数组,但是这将导致对随机数生成器的N次调用的额外成本以及置换整个输入数组或者分配包含n的新长度N的数组.随机指数.

如果问题不允许你移动原始数组中的元素,并且分配内存的成本很高,那么复制数组不是一个选项,这是另一回事.但严格来说,就运行时间而言,这是最好的解决方案.

  • 在任何算法问题中,如果读取数据是一个问题,则必须在问题中提及.问题是"给定一个数组"而不是"在磁盘上给出一个不适合内存的数组,不能根据von neuman模型进行操作,而von neuman模型是算法分析的标准".这些天你可以买一台带8g内存的笔记本电脑.我不确定在内存中保存十亿个数字的想法是不可行的.我现在在工作站上的内存中有数十亿个数字. (14认同)
  • 你的最后一段是关键点:拥有十亿个数字,将所有数据保存在内存中或交换元素是不可行的.(至少我会解释这个问题,因为这是一个面试问题.) (4认同)

Sam*_*ton 5

取出十亿分之一的前100个数字并对它们进行排序.现在只迭代十亿,如果源数高于100的最小值,则按排序顺序插入.你最终得到的是与集合大小相比更接近O(n)的东西.

  • oops没有看到比我自己更详细的答案. (3认同)