我的一个朋友被问到了一个问题
从一亿个数字中检索最多的前100个数字
在最近的一次面试中。你有什么想法想出一个有效的解决方法吗?
答案 0 :(得分:62)
通过大小为100的min-heap运行它们:对于每个输入数字k
,将当前分钟m
替换为max(k, m)
。之后,堆保留了100个最大的输入。
像Lucene这样的搜索引擎可以使用这种方法进行改进,以选择最相关的搜索答案。
编辑:我未能通过采访 - 我的错误信息两次(之前已经完成了,在制作中)。这是检查它的代码;它与Python的标准heapq.nlargest()
几乎相同:
import heapq
def funnel(n, numbers):
if n == 0: return []
heap = numbers[:n]
heapq.heapify(heap)
for k in numbers[n:]:
if heap[0] < k:
heapq.heapreplace(heap, k)
return heap
>>> funnel(4, [3,1,4,1,5,9,2,6,5,3,5,8])
[5, 8, 6, 9]
答案 1 :(得分:11)
好的,这是一个非常愚蠢的答案,但它是一个有效的答案:
推理:
对某种一次性操作来说,这是一个很好的解决方案。它会每秒运行x次或其他东西。但是,我们需要更多的上下文 - 正如mclientk也有他简单的SQL语句 - 假设内存中不存在1亿个数字是一个可行的问题,因为......它们可能来自数据库,大部分时间都会在谈话时关于商业相关数字。
因此,这个问题很难回答 - 首先必须确定效率。
答案 2 :(得分:5)
100次批量合并,然后只保留前100名。
顺便提一下,您可以在各种方向上进行缩放,包括同时进行。
答案 3 :(得分:5)
如果数据已经存在于您可以修改的数组中,则可以使用Hoare选择算法的变体,该算法(反过来)是Quicksort的变体。
基本想法非常简单。在Quicksort中,您将阵列分为两部分,一部分大于枢轴,另一部分小于枢轴。然后递归排序每个分区。
在Select算法中,您完全按照以前的方式执行分区步骤 - 但不是递归地排序两个分区,而是查看哪个分区包含您想要的元素,并以递归方式选择划分。例如,假设您的1亿个项目几乎分成两半,前几个迭代您将只在上层分区 。
最终,您可能会达到一个点,您想要的部分“桥接”两个分区 - 例如,您有一个约150个数字的分区,当您分区时,您最终会得到两个〜75个分区一块。此时,只有一个小细节发生变化:您不是拒绝一个分区而只是继续工作另一个分区,而是接受 75个项目的上层分区,然后继续查找下层分区中的前25个分区
如果您是在C ++中执行此操作,则可以使用std::nth_element
执行此操作(通常大致如上所述实现)。平均而言,这具有线性复杂性,我认为这与你所希望的一样好(没有一些预先存在的顺序,我没有看到任何方法在没有查看所有元素的情况下找到前N个元素)。
如果数据的不是已经存在于数组中,而您(例如)从文件中读取数据,则通常需要使用堆。您基本上读取了一个项目,将其插入堆中,如果堆大于您的目标(在这种情况下为100个项目),则删除一个项目并重新堆积。
可能不那么明显(但实际上是真的)是你通常不想使用max-heap来完成这项任务。乍一看,似乎非常明显:如果你想获得最大的项目,你应该使用最大堆。
然而,根据您从堆中“删除”的项目来考虑更简单。最大堆可以让您快速找到堆中最大的一个项目。但不,已针对查找堆中的最小项进行了优化。
在这种情况下,我们主要感兴趣的是堆中的最小项。特别是,当我们从文件中读取每个项目时,我们希望将它与堆中的最小项目进行比较。如果(并且仅当)它大于堆中的最小项,我们希望用新项替换当前堆中的最小项。由于(根据定义)大于现有项目,我们需要将其筛选到堆中的正确位置。
但是请注意:如果文件中的项目是随机排序的,当我们读完文件时,我们会很快到达我们读入文件的大多数项目将小于我们堆中的最小项目。由于我们可以轻松访问堆中的最小项目,因此可以非常快速轻松地进行比较,而对于较小的项目,根本不会在堆中插入。
答案 4 :(得分:4)
按TOP 100
,你的意思是100最大?如果是这样的话:
SELECT TOP 100 Number FROM RidiculouslyLargeTable ORDER BY Number DESC
请务必告诉面试官您认为该表已正确编入索引。
答案 5 :(得分:1)
没有理由对整个列表进行排序。这应该在O(n)时间内可行。在伪代码中:
List top = new List
for each num in entireList
for i = 0 to top.Length
if num > top[i] then
top.InsertBefore(num, i)
if top.Length > 100 then
top.Remove(top.Length - 1)
end if
exit for
else
if i = top.Length - 1 and i < 100 then
top.Add(num)
end if
end if
next
next
答案 6 :(得分:0)
@darius实际上可以改进!!!
通过“修剪”或推迟堆替换操作
假设我们在堆的顶部有一个= 1000 它有c,b兄弟 我们知道c,b> 1000左
a=1000
+-----|-----+
b>a c>a
We now read the next number x=1035
Since x>a we should discard a.
Instead we store (x=1035, a=1000) at the root
We do not (yet) bubble down the new value of 1035
Note that we still know that b,c<a but possibly b,c>x
Now, we get the next number y
when y<a<x then obviously we can discard it
when y>x>a then we replace x with y (the root now has (y, a=1000))
=> we saved log(m) steps here, since x will never have to bubble down
when a>y>x then we need to bubble down y recursively as required
Worst run time is still O(n log m)
But average run time i think might be O(n log log m) or something
In any case, it is obviously a faster implementation
答案 7 :(得分:0)
在O(n)中对数组进行修改。然后取出前100名元素。
答案 8 :(得分:0)
我将最初的100个数字存储在Max -Heap中,大小为100。
在最后一级,我会记录我插入的最小号码和新号码,并用最小号码检查。是否有前20位的候选号码。
- 我再次调用reheapify,因此我总是拥有前100名的最大堆。
所以它的复杂性是O(nlogn)。
答案 9 :(得分:-1)
int numbers[100000000000] = {...};
int result[100] = {0};
for( int i = 0 ; i < 100000000000 ; i++ )
{
for( int j = 0 ; j < 100 ; j++ )
{
if( numbers[i] > result[j] )
{
if( j < 99 )
{
memcpy(result+j+1, result+j, (100-j)*sizeof(int));
}
result[j] = numbers[i];
break;
}
}
}
答案 10 :(得分:-1)
第一次迭代:
Quicksort,取前100名.O(n log n)。简单,易于编码。很明显。
更好?我们正在处理数字,做一个基数排序(线性时间)占据前100名。我希望这是面试官正在寻找的。
还有其他考虑吗?好吧,一百万个数字并不是很多内存,但是如果你想最小化内存,那么到目前为止你会遇到最多100个数字,然后只扫描这些数字。什么是最好的方式?
有些人提到了堆,但是更好的解决方案可能是双向链接列表,其中指针指向目前为止找到的前100个中的最小值。如果遇到的数字a大于列出的当前最小值,则与下一个元素相比,并将数字从下一个数字移到当前,直到找到新数字的位置。 (这基本上只是针对这种情况的专用堆)。通过一些调整(如果数字大于当前最小值,与当前最大值进行比较以查看行走列表的哪个方向以找到插入点),这将相对有效,并且只需要1.5k的内存。
答案 11 :(得分:-1)
假设mylist是一个包含数亿个数据的列表。所以我们可以对列表进行排序,并从mylist中获取最后一百个数据。
mylist.sort()
MYLIST [-100:]
第二种方式:
导入heapq
heapq.nlargest(100,mylist)