【问题标题】:Finding mean and median in constant time在恒定时间内找到平均值和中位数
【发布时间】:2017-03-28 14:45:04
【问题描述】:

这是一个常见的面试问题。 你有一个数字流进来(假设超过一百万)。数字在 [0-999] 之间)。

Implement a class which supports three methods in O(1) 

* insert(int i); 
* getMean(); 
* getMedian(); 

这是我的代码。

public class FindAverage {

  private int[] store;
  private long size;
  private long total;
  private int highestIndex;
  private int lowestIndex;

  public FindAverage() {
    store  = new int[1000];
    size = 0;
    total = 0;
    highestIndex = Integer.MIN_VALUE;
    lowestIndex = Integer.MAX_VALUE;

  }

  public void insert(int item) throws OutOfRangeException {
    if(item < 0 || item > 999){
      throw new OutOfRangeException();
    }
    store[item] ++;
    size ++;
    total += item;
    highestIndex = Integer.max(highestIndex, item);
    lowestIndex = Integer.min(lowestIndex, item);
  }

  public float getMean(){
    return (float)total/size;
  }

  public float getMedian(){

  }
}

我似乎想不出在 O(1) 时间内获得中位数的方法。 任何帮助表示赞赏。

【问题讨论】:

  • 为什么不能像使用total 一样更新insert 的中位数(保存为(值,值中的数字))?
  • 鉴于您的 store 具有固定 (1000) 个元素,几乎您编写的任何用于计算中位数的代码都是 O(1)。
  • @PaulHankin 它没有固定数量的元素。您可能想再次阅读该问题。
  • @Abstraction 这就是我想要做的,但似乎找不到办法。
  • @MelissaStewart Paul 是对的,store 具有固定数量的元素 (1000)。插入多少值无关紧要。见my answer

标签: java algorithm data-structures


【解决方案1】:

您已经通过构建store 计数器完成了所有繁重的工作。加上size 值,就很简单了。

您只需开始迭代store,将计数相加,直到达到size 的一半。如果size 是奇数,那就是你的中值。即使是size,您也将获取两个周围的值并获得它们的平均值。

性能平均为O(1000/2),这意味着O(1),因为它不依赖于n,即性能不变即使n 达到数十亿。

请记住,O(1) 并不意味着即时,甚至是快速。正如Wikipedia 所说:

如果 T(n) 的值受一个不依赖于输入的大小

在你的情况下,这个界限是 1000。

【讨论】:

  • 这可能很挑剔,但 O(1) 并不意味着与 n 无关。例如,for (i = 0; i &lt; n % 10000; i++) putchar('.'); 是 O(1),因为它从不输出超过 10000 个项目,但运行时间取决于 n(具体而言,n 模 10000)。
  • 也许我不理解你,但是迭代一个列表并不能使算法 O(1) 正确。
  • @MelissaStewart 迭代有界大小的列表是 O(1)。做 1000 件事情是一个恒定的工作量,所以 O(1)。
  • @PaulHankin 好的,这有帮助,我可以想办法使用它。
  • "如果 n 达到数十亿" - 值将是 1000000000/2。所以它绑定到 n,因为在这种情况下 n 是 1000。我们会得到线性时间,而不是常数
【解决方案2】:

您可以读取的可能值非常有限 - 只有 1000。因此您可以考虑实现类似 counting sort 的东西 - 每次输入数字时,您都会增加该值的计数器。

要在恒定时间内实现中位数,您需要两个数字 - 中位数索引(即中位数的值)和您已读取且位于中位数左侧(或右侧)的值的数量.我将在这里停下来,希望您能够自己弄清楚如何继续。

编辑(如 cmets 中所指出的):您已经拥有带有排序元素的数组(stored),并且您知道中位数左侧的元素数量(size/2)。您只需要将逻辑粘合在一起。我想指出,如果您使用线性附加内存,则无需在每次插入时遍历整个数组。

【讨论】:

  • 有问题的代码已经具有您在第一段中谈论的计数器。见字段store
  • @Andreas 它缺少一个值 - 左侧(或右侧)到中位数的值的数量。使用它(以及一些思考)我们不需要遍历所有可能的值
  • 没关系。这可以在getMedian() 方法中完成。相对于n,迭代store 数组是常数时间。
  • 是的。常数是1000-ish。使用我的建议可以显着优化
  • @Andreas 实际上你是对的。不需要第二个值
【解决方案3】:

对于一般情况,元素的范围是无限的,这样的数据结构不存在基于任何基于比较的算法,因为它将允许O(n)排序.

证明:假设存在这样的 DS,让它成为D
A 成为排序的输入数组。 (假设A.size() 甚至为了简单起见,可以通过添加垃圾元素并稍后丢弃它来轻松放松)。

sort(A):
  ds = new D()
  for each x in A:
    ds.add(x)
  m1 = min(A) - 1
  m2 = max(A) + 1
  for (i=0; i < A.size(); i++):
    ds.add(m1)
  # at this point, ds.median() is smallest element in A
  for (i = 0; i < A.size(); i++):
    yield ds.median()
    # Each two insertions advances median by 1
    ds.add(m2)
    ds.add(m2)

声明 1:此算法在 O(n) 中运行。
证明:由于我们有 add() 和 median() 的常量操作,所以每次迭代它们都是O(1),并且迭代次数是线性的——复杂度是线性的。

声明 2:输出已排序 (A)。
证明(准则):插入n次m1后,中位数是A中最小的元素。每两次插入后它使中位数前移一项,并且由于前移是排序的,所以总输出是排序的。

由于上述算法在O(n)中排序,在比较模型下是不可能的,所以不存在这样的DS。

QED。

【讨论】:

  • 您的证明与问题的范围完全不同,但仍然非常优雅。
  • @MelissaStewart 更新了解决方案以捕获更广泛的计算模型(减少排序而不是元素独特性)。
  • @amit 关键是因为只有 1000 个可能的值,所以我们不在一般情况下。
  • 你为什么说排序?您是否暗示在一般情况下不可能找到 O(n) 中的中位数?快速选择产生 O(n) 平均情况中位数选择。
猜你喜欢
  • 2014-05-24
  • 1970-01-01
  • 1970-01-01
  • 2022-01-08
  • 2021-05-12
  • 2016-08-01
  • 2021-08-22
  • 2013-02-18
  • 1970-01-01
相关资源
最近更新 更多