找到k个机器中存储的k个数组中的最大k数

时间:2012-03-26 12:27:08

标签: algorithm data-structures

这是一个面试问题。我有K台机器,每台机器连接到一台中央机器。每台K机器都有一个4字节数字的数组。您可以使用任何数据结构将这些数字加载到这些机器上的内存中,并且它们适合。 K机器上的数字并不是唯一的。找到所有K台机器中数字组合中的K个最大数字。我能做到的最快的是什么?

7 个答案:

答案 0 :(得分:12)

(这是一个有趣的问题,因为它涉及并行性。由于我以前没有遇到过并行算法优化,它很有趣:你可以逃避一些非常复杂的高复杂步骤,因为你可以以后弥补它。无论如何,回答......)

> " 我能做到的最快的是什么?"

你能做的最好的是O(K)。下面我将说明一个简单的O(K log(K))算法和更复杂的O(K)算法。


第一步:

每台计算机都需要足够的时间来阅读每个元素。这意味着除非元素已经在内存中,否则时间上的两个边界之一是O(最大数组大小)。例如,如果你的最大数组大小变化为O(K log(K))或O(K ^ 2)或其他东西,那么任何数量的算法技巧都不会让你走得更快。因此,技术上实际的最佳运行时间为O(max(K, largestArraySize))

让我们说阵列的最大长度为N,即<= K.通过上述警告,我们允许绑定N<K,因为每台计算机必须至少查看一次其每个元素(每台计算机进行O(N)预处理),每台计算机都可以选择最大的K元素(这被称为查找k次序统计,请参阅这些线性时间algorithms)。此外,我们可以免费提供(因为它也是O(N))。


界限和合理期望:

让我们首先考虑一些最坏情况,并估算所需的最少工作量。

  • 一个最小工作必要的估计值是O(K * N / K)= O(N),因为我们需要至少查看每个元素。但是,如果我们变得聪明,我们可以在所有K台计算机上均匀分配工作(因此按K划分)。
  • 另一个必要的最小工作量估计是O(N):如果一个数组大于所有其他计算机上的所有元素,我们返回该集合。
  • 我们必须输出所有K元素;这至少是O(K)打印出来的。如果我们只是知道元素的位置,我们可以避免这种情况,在这种情况下,O(K)界限不一定适用。

是否可以达到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  .           

您还可以将此设想为来自计算几何的sweep-line algorithm,或者是&#39; merge&#39;的有效变体。从mergesort步骤。带括号的元素代表了我们用来初始化我们潜在的候选解决方案的元素&#34; (在某些中央服务器中)。该算法将通过转储两个未选择的o的{​​{1}}个答案来收集正确的(x)响应。

算法:

  • 所有计算机均以&#39;活动&#39;
  • 开头
  • 每台计算机对其元素进行排序。 (并行O(N logN))
  • 重复直到所有计算机都处于非活动状态
    • 每台活动计算机找到次高的元素(自排序后的O(1))并将其提供给中央服务器。
    • 服务器巧妙地将新元素与旧K元素组合在一起,并从组合集中删除相同数量的最低元素。为了有效地执行此步骤,我们有一个固定大小为K的全局优先级队列。我们插入新的可能更好的元素,坏元素不属于集合。每当元素落在集合之外时,我们告诉发送该元素的计算机永远不会发送另一个元素。 (理由:这总是提升候选集的最小元素。

(旁注:添加回调挂钩以退出优先级队列是O(1)操作。)

我们可以用图形方式看到这将执行最多2K *(findNextHighest_time + queueInsert_time)操作,并且当我们这样做时,元素将自然地落在优先级队列之外。 findNextHighest_time是O(1),因为我们对数组进行了排序,因此为了最小化2K * queueInsert_time,我们选择具有O(1)插入时间的优先级队列(例如基于Fibonacci堆的优先级队列)。这给了我们一个O(log(queue_size))提取时间(我们不能有O(1)插入和提取);但是,我们永远不需要使用提取操作!完成后,我们只将转储优先级队列作为无序集合,这需要O(queue_size)= O(K)时间。

因此,我们具有O(N log(N)+ K)总运行时间(并行排序,然后是O(K)* O(1)优先级队列插入)。在N = K的最坏情况下,这是O(K log(K))。


更好的方法 - O(N + K)= O(K):

然而,我提出了一种更好的方法,达到了O(K)。它基于median-of-median selection algorithm,但是并行化。它是这样的:

如果我们确定所有计算机中某处至少有K(不是严格地说)较大的数字,我们就可以删除一组数字。

算法:

  • 每台计算机找到其集合中o个最高元素,并将该集合拆分为元素&lt;和&gt;它。这需要并行O(N)时间。
  • 计算机协作将这些统计信息合并到一个新的设置中,并找到设置的sqrt(N)个最高元素(让我们称之为“超级主义&” #39;),并注意哪些计算机具有统计信息&lt;和&gt;超级主义。这需要O(K)时间。
  • 现在,在统计数据低于超群体的计算机上,考虑所有元素低于计算机的统计数据。 可以消除这些元素。这是因为在统计数据大于超级统计的计算机上,大于计算机统计数据的元素是一组较大的K元素。 (见视觉here)。
  • 现在,具有uneliminated元素的计算机将其数据均匀地重新分发给丢失数据的计算机。
  • 递归:你还有K台计算机,但是N的值减少了。一旦N小于预定常数,使用我在&#34中提到的先前算法;简单方法 - O(NlogN + K)&#34 ;;除了这种情况,它现在是O(K)。 =)

事实证明,减少的总数是O(N)(令人惊讶的不是K阶),除了最后一步可能是O(K)。因此,该算法是O(N + K)= O(K)总数。

分析和模拟下面的O(K)运行时间。统计数据允许我们将世界划分为四个无序集合,在此表示为一个分为四个子框的矩形:

K/sqrt(N)

(我在这里绘制了无序行和s列之间的关系,但它会使事情变得混乱;现在请快速查看附录。)

对于此分析,我们将考虑N减少。

在给定的步骤中,我们可以消除标记为 ------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 的元素;这已经从上面的矩形表示中删除了区域,将问题大小从K * N减少到enter image description here,其中搞笑简化为enter image description here

现在,具有uneliminated元素的计算机将其数据(上面的ELIMIN矩形)重新分发到具有已消除元素(REDIST)的计算机。这是并行完成的,其中带宽瓶颈对应于ELIMIN的短大小的长度(因为它们的数量超过等待其数据的REDIST计算机)。因此,数据传输的时间与ELIMIN矩形的长度一样长(另一种思考方式:REDIST是区域,除以K/√N * (N-√N)每次数据,导致O(K/√N)时间。

因此,在大小N-√N的每个步骤中,我们都可以将问题大小减少到N,但代价是执行K(2√N-1)工作。我们现在递归。将告诉我们的表现的复发关系是:

N + 3K + (N-√N)

子问题大小的抽取比正常的几何系列快得多(√N而不是像你通常从普通的分而治之的N / 2那样)。不幸的是,主定理和强大的Akra-Bazzi定理都不起作用,但我们至少可以通过模拟来说服自己它是线性的:

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的界限。虽然看到附录中有一个有趣的可能性。


...


附录

  • 一个陷阱是意外排序。如果我们做任何意外地对我们的元素进行分类的事情,我们至少会受到对数(N)的惩罚。 因此最好将数组视为集合,以避免认为它们已被排序的陷阱。
  • 我们最初可能会认为,在3K的每一步都有不断的工作量,所以我们必须做3K log(log(N))工作。但-1在问题规模的抽取中扮演着强大的角色。运行时间实际上略高于线性,但肯定比N log(log(log(log(N)))小得多。例如,它可能类似于O(N * InverseAckermann(N)),但在测试时我达到了递归限制。
  • O(K)可能只是因为我们必须将它们打印出来;如果我们只满足于知道数据的位置,我们甚至可以拉出O(N)(例如,如果数组长度为O(log(K)),我们可能能够实现O(log(K) )))......但那是另一个故事。
  • 无序集之间的关系如下。会解释一些事情。

O(N)

答案 1 :(得分:11)

  • 在每台机器上找到k个最大的数字。为O(n *日志(k))的
  • 合并结果(在集中式服务器上,如果k不大,否则您可以将它们合并到服务器群集中的树层次结构中。)

更新:为了清楚起见,合并步骤不是一种排序。您只需从结果中选择前k个数字。有效地有很多方法可以做到这一点。例如,您可以使用堆,推动每个列表的头部。然后你可以从堆中移除头部并从元素所属的列表中推出头部。这样做k次可以得到结果。所有这些都是O(k * log(k))。

答案 2 :(得分:3)

  • 在集中式服务器中维护一个大小为“k”的最小堆。
  • 最初将第一个k元素插入最小堆。
  • 剩下的元素
    • 检查(查看)堆中的min元素(O(1))
    • 如果min元素小于当前元素,则从堆中删除min元素并插入当前元素。
  • 最后,min heap将拥有'k'个最大元素
  • 这需要n(log k)时间。

答案 3 :(得分:1)

我会建议这样的事情:

  • 按照排序顺序O(Nk)取每台机器上的k个最大数字,其中N是每台机器上的元素数

  • 按最大元素对这些k元素的每个数组进行排序(你将得到k个元素的k个数组,按最大元素排序:方形矩阵kxk)

  • 取k个元素的k个数组构成的矩阵的“上三角”,(k最大的元素将在这个上三角形中)

  • 中央机器现在可以找到这些k(k + 1)/ 2个元素的k个最大元素

答案 4 :(得分:1)

  1. 让机器找到k个最大的元素将它复制到一个 datastructure(stack),对其进行排序并将其传递给Central 机。
  2. 在中央机器上接收来自所有机器的堆栈。找 堆栈顶部最大的元素。
  3. 从堆栈中弹出最大元素并将其复制到'TopK list'。 保持其他堆栈完整。
  4. 重复第3步,k次以获得前K个数字。

答案 5 :(得分:0)

1)对每台机器上的物品进行分类 2)在中央机器上使用k - 二进制堆 a)使用每台机器的第一个(最大)元素填充堆 b)提取第一个元素,并将从您提取元素的机器中的第一个元素放回堆中。 (当然,在添加元素之后堆积你的堆)。

排序将为O(N log(N)),其中N是计算机上的最大数组。 O(k) - 构建堆 O(k log(k))提取并填充堆k次。

复杂度为max(O(k log(k)),O(N log(N)))

答案 6 :(得分:-1)

我认为MapReduce范例非常适合这样的任务。

每台机器运行它自己的独立map任务,以找到其数组中的最大值(取决于所使用的语言),这可能是每台机器上N个数字的O(N)复杂度。

reduce任务会比较各台机器输出的结果,为您提供最大的k数。