面试问题:从大量的整数中找到中位数

时间:2010-08-26 06:44:16

标签: algorithm

有一个包含10G(1000000000)个整数的文件,请找到这些整数的中位数。你有2G内存来做这件事。任何人都可以想出一个合理的方式吗?谢谢!

9 个答案:

答案 0 :(得分:37)

创建一个8字节长的数组,其中包含2 ^ 16个条目。获取输入数字,移出最后16位,然后创建直方图。

现在你在直方图中计算,直到你到达覆盖值中点的bin。

再次通过,忽略所有没有相同顶部位的数字,并制作底部位的直方图。

通过该直方图向上计数,直至到达覆盖(整个列表)值中点的bin。

现在您知道O(n)时间和O(1)空间(实际上,1 MB以下)的中位数。

以下是一些示例Scala代码:

def medianFinder(numbers: Iterable[Int]) = {
  def midArgMid(a: Array[Long], mid: Long) = {
    val cuml = a.scanLeft(0L)(_ + _).drop(1)
    cuml.zipWithIndex.dropWhile(_._1 < mid).head
  }
  val topHistogram = new Array[Long](65536)
  var count = 0L
  numbers.foreach(number => {
    count += 1
    topHistogram(number>>>16) += 1
  })
  val (topCount,topIndex) = midArgMid(topHistogram, (count+1)/2)
  val botHistogram = new Array[Long](65536)
  numbers.foreach(number => {
    if ((number>>>16) == topIndex) botHistogram(number & 0xFFFF) += 1
  })
  val (botCount,botIndex) =
    midArgMid(botHistogram, (count+1)/2 - (topCount-topHistogram(topIndex)))
  (topIndex<<16) + botIndex
}

这里正在处理一小组输入数据:

scala> medianFinder(List(1,123,12345,1234567,123456789))
res18: Int = 12345

如果存储了64位整数,则可以在4次传递中使用相同的策略。

答案 1 :(得分:12)

您可以使用Medians of Medians algorithm

答案 2 :(得分:4)

如果文件是文本格式,您可以将内容放入内存中,只需将内容转换为整数即可,因为存储为字符的整数可能比整数存储的整数占用更多空间,取决于整数的大小和文本文件的类型。编辑:您编辑了原始问题;我现在可以看到你无法将它们读入内存,见下文。

如果你无法将它们读入内存,我就会想到这一点:

  1. 计算出你有多少整数。你可能从一开始就知道这一点。如果没有,那么它只需要一次通过该文件。我们假设这是S。

  2. 使用2G的内存来查找x个最大的整数(无论多少都可以)。您可以在文件中进行一次传递,将x最大值保存在某种排序列表中,随时丢弃其余部分。现在你知道了第x个最大的整数。你可以丢弃所有这些,除了第x个最大的,我称之为x1。

  3. 进行另一次传递,找到下一个x最大的整数小于 x1,其中最小的是x2。

  4. 我想你可以看到我的目标。几次通过后,您将读入(S / 2)最大整数(您必须跟踪您找到的整数),这是您的中位数。如果S是偶数,那么你将平均两个中间值。

答案 3 :(得分:3)

传递文件并查找整数计数以及最小和最大整数值。

取最小值和最大值的中点,并获取中点两侧值的计数,最小值和最大值 - 再次读取文件。

分区计数&gt; count =&gt;中位数位于该分区内。

重复分区,考虑到“左侧分区”的大小(易于维护),还要观察min = max。

我相信这也适用于任意数量的分区。

答案 4 :(得分:3)

  1. 对文件执行磁盘上的external mergesort以对整数进行排序(如果尚未知道则对它们进行计数)。
  2. 一旦文件被排序,寻找中间数字(奇数情况),或平均文件中的两个中间数字(甚至大小写)以获得中位数。
  3. 使用的内存量是可调整的,不受原始文件中整数数量的影响。外部排序的一个警告是需要将中间排序数据写入磁盘。

    给定n =原始文件中的整数数:

    • 运行时间:O(nlogn)
    • 记忆:O(1),可调节
    • 磁盘:O(n)

答案 5 :(得分:1)

在这里查看Torben的方法:http://ndevilla.free.fr/median/median/index.html。它还在文档底部的C中实现。

答案 6 :(得分:0)

我最好的猜测中位数的概率中位数是最快的。配方:

  1. 取下一组N个整数(N应该足够大,比如1000或10000个元素)
  2. 然后计算这些整数的中位数并将其分配给变量X_new。
  3. 如果迭代不是第一次 - 计算两个中位数的中位数:

      

    X_global =(X_global + X_new)/ 2

  4. 当您看到X_global波动不大时 - 这意味着您找到了近似的数据中位数。

  5. 但有一些注意事项:

    • 问题出现 - 中位数错误是否可以接受。
    • 整数必须以统一的方式随机分布,才能使解决方案正常工作

    修改 我已经用这个算法玩了一下,改变了一点想法 - 在每次迭代中我们应该将X_new与减重相加,例如:

      

    X_global = k * X_global +(1.-k)* X_new:

         

    k来自[0.5 .. 1.],并且每次迭代都会增加。

    要点是计算中值,以便在极少量的迭代中快速收敛到某个数。因此,仅在252次迭代中,在100000000个数组元素之间找到非常近似的中位数(大误差)!!! 检查此C实验:

    #include <stdlib.h>
    #include <stdio.h>
    #include <time.h>
    
    #define ARRAY_SIZE 100000000
    #define RANGE_SIZE 1000
    
    // probabilistic median of medians method
    // should print 5000 as data average
    // from ARRAY_SIZE of elements
    int main (int argc, const char * argv[]) {
        int iter = 0;
        int X_global = 0;
        int X_new = 0;
        int i = 0;
        float dk = 0.002;
        float k = 0.5;
        srand(time(NULL));
    
        while (i<ARRAY_SIZE && k!=1.) {
            X_new=0;
            for (int j=i; j<i+RANGE_SIZE; j++) {
                X_new+=rand()%10000 + 1;
            }
            X_new/=RANGE_SIZE;
    
            if (iter>0) {
                k += dk;
                k = (k>1.)? 1.:k;
                X_global = k*X_global+(1.-k)*X_new;
    
            }
            else {
                X_global = X_new;
            }
    
            i+=RANGE_SIZE+1;
            iter++;
            printf("iter %d, median = %d \n",iter,X_global);
        }
    
        return 0;
    
    }
    

    Opps似乎在谈论平均而不是中位数。如果是这样,你需要确切的中位数,而不是意味着 - 忽略我的帖子。在任何情况下,均值和中位数都是非常相关的概念。

    祝你好运。

答案 7 :(得分:0)

这是由Java实现的@Rex Kerr描述的算法。

/**
 * Computes the median.
 * @param arr Array of strings, each element represents a distinct binary number and has the same number of bits (padded with leading zeroes if necessary)
 * @return the median (number of rank ceil((m+1)/2) ) of the array as a string
 */
static String computeMedian(String[] arr) {

    // rank of the median element
    int m = (int) Math.ceil((arr.length+1)/2.0);

    String bitMask = "";
    int zeroBin = 0;

    while (bitMask.length() < arr[0].length()) {

        // puts elements which conform to the bitMask into one of two buckets
        for (String curr : arr) {
            if (curr.startsWith(bitMask))
                if (curr.charAt(bitMask.length()) == '0')
                    zeroBin++;
        }

        // decides in which bucket the median is located
        if (zeroBin >= m)
            bitMask = bitMask.concat("0");
        else {
            m -= zeroBin;
            bitMask = bitMask.concat("1");
        }

        zeroBin = 0;
    }

    return bitMask;
}

可以找到一些测试用例和算法更新here

答案 8 :(得分:0)

我也被问到了同样的问题,我无法给出确切的答案,所以在面试之后,我浏览了一些有关面试的书,这就是我从《破解编码》面试书中发现的东西。

  

示例:数字是随机生成的,并存储在(扩展的)数组中。怎么样   您会跟踪中位数吗?

     

我们的数据结构头脑风暴可能类似于以下内容:

     

•链接列表?可能不会。链接列表在访问和   排序数字。

     

•数组?也许可以,但是您已经有一个数组。你能以某种方式保留元素吗   排序?那可能很贵。让我们暂缓此操作,然后在需要时返回。

     

•二叉树?这是可能的,因为二叉树在排序方面做得很好。实际上,如果二叉搜索树完全平衡,则顶部可能是中位数。但是请注意,如果元素数量为偶数,则中位数实际上是平均值   中间的两个元素。中间的两个元素不能同时位于顶部。这可能是一个可行的算法,但让我们回到它上面。

     

•堆?堆真的很擅长基本排序并跟踪最大和最小。   这实际上很有趣-如果您有两个堆,则可以跟踪更大的堆   元素的一半和较小的一半。较大的一半保留在最小堆中,例如   表示较大的一半中的最小元素位于根。较小的一半保留在   最大堆,使得较小一半的最大元素位于根。现在,有了   在这些数据结构中,您的根有潜在的中位数元素。如果   堆的大小不再相同,您可以通过弹出来快速“重新平衡”堆   一个元素从一个堆中移出并推到另一个堆上。

     

请注意,您做的问题越多,对哪些数据的直觉就越发展   适用的结构。您还将开发出更精细的本能,以了解哪种方法最有用。