这是一个面试问题。我有K台机器,每台机器都连接到1台中央机器。每台机器上都有一个包含4字节数字的数组文件。您可以使用任何数据结构将这些数字加载到这些机器的内存中,并且它们适合内存。数字在K个机器之间不唯一。查找所有K个机器中数字的并集中的K个最大数字。我能做到最快的速度是多少?
这是一个面试问题。我有K台机器,每台机器都连接到1台中央机器。每台机器上都有一个包含4字节数字的数组文件。您可以使用任何数据结构将这些数字加载到这些机器的内存中,并且它们适合内存。数字在K个机器之间不唯一。查找所有K个机器中数字的并集中的K个最大数字。我能做到最快的速度是多少?
第一步:
每台计算机需要足够的时间来读取每个元素。这意味着,除非元素已经在内存中,否则时间的两个边界之一为O(最大数组大小)。例如,如果您的最大数组大小变化为O(K log(K))或O(K^2)等,则无论如何进行算法技巧,都无法使速度更快。因此,实际的最佳运行时间在技术上是O(max(K, largestArraySize))
。
假设数组的最大长度为N,即<=K。在上述警告下,我们可以限制N<K
,因为每台计算机都必须至少查看其每个元素(每台计算机的O(N)预处理),每台计算机可以选择最大的K个元素(这被称为查找kth-order-statistics,参见这些线性时间algorithms)。此外,我们可以免费这样做(因为它也是O(N))。
范围和合理预期:
让我们从一些最坏情况和所需最小工作量的估计开始思考。
这个O(N)的限制能够实现吗?让我们看看...
简单方法 - O(NlogN + K) = O(KlogK):
现在让我们想出一个简单的方法,达到O(NlogN + K)。
考虑数据按以下方式排列,其中每列是一台计算机,每行是数组中的一个数字:
computer: A B C D E F G
10 (o) (o)
9 o (o) (o)
8 o (o)
7 x x (x)
6 x x (x)
5 x ..........
4 x x ..
3 x x x . .
2 x x . .
1 x x .
0 x x .
o
的(x)
答案,该算法将收敛于正确的o
响应。(附注:将回调钩子添加到优先级队列中掉出是一个O(1)操作。)
我们可以从图形上看出,这将执行最多2K *(findNextHighest_time + queueInsert_time)次操作,随着我们这样做,元素自然会从优先级队列中掉落。由于我们已经排序了数组,因此findNextHighest_time为O(1),因此为了最小化2K*queueInsert_time,我们选择具有O(1)插入时间的优先级队列(例如基于斐波那契堆的优先级队列)。这给我们带来了O(log(queue_size))的提取时间(我们不能同时具有O(1)插入和提取);但是,我们从不需要使用提取操作!一旦我们完成,我们只需将优先级队列作为无序集合转储,这需要O(queue_size)=O(K)时间。更好的方法 - O(N+K) = O(K):
然而,我提出了一种更好的方法,它可以实现O(K)。它基于中位数选择算法,但是并行化了。它的步骤如下:
如果我们确定在所有计算机中有至少K(不是严格的)个较大的数字,那么我们就可以消除一组数字。
算法:
sqrt(N)
大的元素,并将集合分成小于和大于它的元素。这在并行中需要O(N)时间。K/sqrt(N)
大的元素(让我们称之为“超级统计数据”),并记录哪些计算机具有小于和大于超级统计数据的统计数据。这需要O(K)的时间。 ------N-----
N^.5
________________
| | s | <- computer
| | #=K s REDIST. | <- computer
| | s | <- computer
| K/N^.5|-----S----------| <- computer
| | s | <- computer
K | s | <- computer
| | s ELIMIN. | <- computer
| | s | <- computer
| | s | <- computer
| |_____s__________| <- computer
LEGEND:
s=statistic, S=superstatistic
#=K -- set of K largest elements
REDIST
矩形)重新分配给具有消除元素的计算机(ELIMIN
)。这是并行完成的,其中带宽瓶颈对应于REDIST
的短边长度(因为它们被等待其数据的ELIMIN
计算机所占多数)。因此,数据传输所需的时间与REDIST
矩形的长边长度相同(另一种思考方式:面积为K/√N * (N-√N)
,除以K/√N
每单位时间的数据,结果为O(N-√N
)时间)。N
的步骤中,我们能够将问题规模减小到K(2√N-1)
,代价是执行N + 3K + (N-√N)
工作。现在我们进行递归。将告诉我们性能的递归关系式为:T(N) = 2N+3K-√N + T(2√N-1)
>>> def T(n,k=None):
... return 1 if n<10 else sqrt(n)*(2*sqrt(n)-1)+3*k+T(2*sqrt(n)-1, k=k)
>>> f = (lambda x: x)
>>> (lambda n: T((10**5)*n,k=(10**5)*n)/f((10**5)*n) - T(n,k=n)/f(n))(10**30)
-3.552713678800501e-15
T(N)
函数在大规模上是线性函数x
的倍数,因此是线性的(输入翻倍输出也翻倍)。因此,这种方法几乎可以肯定地达到我们所推测的O(N)
上限。不过请参见附录中的有趣可能性。
...
补充
.
_
/ \
(.....) > s > (.....)
s
(.....) > s > (.....)
s
(.....) > s > (.....)
\_/
v
S
v
/ \
(.....) > s > (.....)
s
(.....) > s > (.....)
s
(.....) > s > (.....)
\_/
更新:为了明确起见,合并步骤不是排序。您只需从结果中选择前k个数字即可。有很多有效的方法来做到这一点。例如,您可以使用堆,推送每个列表的头部。然后您可以从堆中删除头部,并将元素所属的列表的头部推送出去。这样做k次会给您结果。所有这些都是O(k*log(k))。
按排序顺序在每台机器上取出k个最大的数字,O(Nk),其中N是每台机器上元素的数量
对这些k个元素的每个数组进行排序,按最大元素排序(您将获得k个按最大元素排序的k个元素数组:一个kxk的方阵)
取由这k个k个元素数组组成的矩阵的“上三角”(k个最大元素将在此上三角中)
现在,中央机器可以找到这k(k+1)/2个元素中的k个最大元素
1) 对每台机器上的项目进行排序 2) 在中央机器上使用k-二叉堆 a)用每台机器的第一个(最大)元素填充堆 b)提取第一个元素,并将从您提取元素的机器中的第一个元素放回堆中。 (当然,在添加元素后,对堆进行堆化)。
排序将是O(Nlog(N)),其中N是机器上的最大数组。 O(k)-构建堆 O(klog(k))以k次提取和填充堆为基础。
复杂度为max(O(klog(k)),O(Nlog(N)))
我认为MapReduce范例非常适合这样的任务。
每台机器都运行自己独立的映射任务,以查找其数组中的最大值(取决于所使用的语言),对于每台机器上的N个数字,这可能是O(N)复杂度。
归约任务将比较各个机器输出的结果,以给出最大的k个数字。