【问题标题】:How to calculate median of a Map<Int,Int>?如何计算 Map<Int,Int> 的中位数?
【发布时间】:2011-03-04 10:25:20
【问题描述】:

对于一个映射,其中键表示序列的数字,值表示该数字在序列中出现的频率,java 中的算法实现如何计算中位数?

例如:

1,1,2,2,2,2,3,3,3,4,5,6,6,6,7,7

在地图中:

Map<Int,Int> map = ...
map.put(1,2)
map.put(2,4)
map.put(3,3)
map.put(4,1)
map.put(5,1)
map.put(6,3)
map.put(7,2)

double median = calculateMedian(map);
print(median);

会导致:

> print(median);
3
>

所以我正在寻找的是calculateMedian 的java 实现。

【问题讨论】:

  • 如果这是作业,请标记。
  • 这是作业吗?如果是这样,请标记它。
  • @danben:对我来说这不是家庭作业,但我敢肯定对某人来说是
  • 也就是说,你是老师? :)
  • @BalusC:不,我不是,我只是对可能的解决方案感兴趣。几乎对于我过去提出的每个问题,我在这里写问题时都已经有了解决方案。其他人如何解决它通常很有趣。顺便说一句,这是一个谷歌面试问题,尽管他们以不同的方式提问;)

标签: java algorithm median


【解决方案1】:

线性时间

如果您知道数字的总数(在您的情况下为 16),您可以从地图的开头或结尾开始计算计数,直到您到达第 (n/2) 个元素,或者如果总和等于 floor(n/2)th 和 ceil(n/2)th 元素的平均值 = median.

如果您不知道总数,则必须至少检查一次。

次线性时间

如果您可以决定数据结构并可以进行预处理,请参阅selection algorithm 上的维基百科,您甚至可以获得亚线性算法。 如果您对数据的分布有所了解,也可以获得亚线性时间。

编辑: 因此,假设我们有一个带有计数的序列,我们可以做的是

  • 在插入 key -&gt; count 对时维护另一个映射 - key -&gt; running_total
  • 这样您将拥有一个结构,您可以在其中通过查看最后一个键的 running_total 来获取 total_count
  • 并且您将能够进行二分搜索以找到运行总计接近 total_count/2 的元素

这将使内存使用量翻倍,但中位数的性能为 O(log n),total_count 的性能为 O(1)。

【讨论】:

  • +1 实际上,我有时会使用这种方法来计算中位数,因为不需要额外的排序。如果您处理有界的离散值(具有较低的上限),您甚至可以进行桶排序(例如,创建直方图)。
  • @Rafał,实际上这假设对密钥的访问是 O(1) 而不是很多,(OP 指定密钥值等于某个范围,我假设没有漏洞 => 排序);同样重要的是running_total,我只是保持数据结构与OP中的相同。
  • 如果地图大小可变,计算运行总数是否需要遍历整个地图?
【解决方案2】:

使用Guava

Multiset<Integer> values = TreeMultiset.create();
Collections.addAll(values, 1,1,2,2,2,2,3,3,3,4,5,6,6,6,7,7);

现在你的问题的答案是:

return Iterables.get(values, (values.size() - 1) / 2);

真的。就是这样。(或者检查大小是否均匀并平均两个中心值,准确地说。)

如果计数特别大,使用多重集的entrySet 并保持运行总和会更快,但最简单的方法通常没问题。

【讨论】:

  • 当然,在这个特殊的玩具示例中,您最好创建一个ArrayList 然后对其进行排序,而不是使用TreeMultiset,但在现实生活中这可能对内存不友好。
  • 地图的示例会是什么样子?抱歉,但我知道如何计算不需要框架的简单序列的中位数。
  • 为什么要使用地图? TreeMultiset 实现在内部使用地图,但向您提供了一个更适合您正在做的事情的 API。这不是一个“简单的序列”,只要你愿意,它就可以看起来像一个。
【解决方案3】:
  • 使用SortedMap,即TreeMap
  • 遍历地图一次以计算元素的总数,即所有出现的总和
  • 再次迭代并累加出现次数,直到达到总数的一半。导致总和超过总数一半的数字是中位数
  • 广泛测试非一错误

【讨论】:

  • 总数的一半?如果幸运的话,一半的总数将使您接近几乎但不完全平均的元素。如果您的 SortedMap 中有“n”个元素,则中位数将是“n/2”处的元素。
  • 不错的方法,但需要更多改进...如果您有一个列表 1,2,2,4,4,5,您的算法将根据插入顺序返回 2 或 4,当正确的中值应该是 3。
  • @McBeth:意思不是我们想要的。由于出现次数,中位数不一定是 n/2 处的元素
  • @kasperjj:是的,这是一种罕见的特殊情况,如果您真的需要支持它,则需要额外的代码。
  • +1 似乎基本上我已经写了同样的东西(我确实提到了奇数/偶数的测试);既然 OP 谈到了序列和计数,你真的需要 SortedMap 吗?
【解决方案4】:

对于简单但可能效率不高的算法,我会这样做:

1.将地图展开为列表。

实际上:遍历地图并将键“值时间”添加到新列表中。最后对列表进行排序。

//...
List<Integer> field = new ArrayList<Integer>();
for (Integer key:map) {
  for (int i = 0; i < map.get(key); i++) {
    field.add(key);
  }
}
Collections.sort(field);

2。计算中位数

现在你必须实现一个方法int calculateMedian(List&lt;Integer&gt; sorted)。这取决于您需要的中位数类型。如果它只是样本中位数,那么结果要么是最中间的值(对于具有奇数个元素的列表),要么是两个中间值的平均值(对于具有偶数长度的列表)。注意,列表需要排序!

(参考:Sample Median / wikipedia


好的,好的,尽管 Chris 没有提到效率,但这里有一个想法如何在不扩展地图的情况下计算样本中位数 (!)...

Set<Integer> sortedKeys = new TreeSet<Integer>(map.keySet()); // just to be sure ;)
Integer median = null;  // Using Integer to have a 'invalid/not found/etc' state
int total = 0;
for (Integer key:sortedKeys) {
  total += map.get(key);
}
if (isOddNumber(total)) { // I don't have to implement everything, do I?
  int counter = total / 2;  // index starting with 0
  for (Integer key:sortedKeys) {
    middleMost -= map.get(key);
    if (counter < 0) {
      // the sample median was in the previous bin
      break;
    }
    median = key;
  }
} else {
  int lower = total/2;
  int upper = lower + 1;
  for (Integer key:sortedKeys) {
    lower -= map.get(key);
    upper -= map.get(key);
    if (lower < 0 && upper < 0) {
      // both middlemost values are in the same bin
      break;
    } else (lower < 0 || upper < 0) {
      // lower is in the previous, upper in the actual bin
      median = (median + key) / 2; // now we need the average
      break;
    }
    median = key;
  }
}

(我手头没有编译器 - 如果它有很多语法错误,请将其视为伪代码;))

【讨论】:

  • -1:我认为,关键是 Chris想要扩展列表,因为它确实可能非常低效。
  • 我同意迈克尔的观点,虽然答案很清楚,它只是不必要地扩展列表,杀死大量内存,而提供解决方案而不扩展列表的算法非常简单(因此我根本不能见理由)。
  • 简单吗?也许就代码行而言,但基于扩展列表的算法更容易阅读和理解。并且 - 现在我们有一个非常专业的特殊地图算法。中位数计算部分的复用性接近于零。
  • 重新复杂,我确实同意你的方法是更简单,但是第二个实现复杂:两个循环和一些计数。第一种方法的简单性是内存要求高 x 倍(x 是所有计数的平均值;从有问题的样本数据来看,这约为 2.3,在实际情况下,这可以是任何整数,让我们假设 100)。此外,如果扩展排序的复杂度增加 log x,则循环时间增加 x。所以算法使用的内存更多,速度也更慢。
  • 可重用性,鉴于 x 可以是任何整数,让我们假设一些更大的值,例如 100,000;同时想象几千把钥匙。如果您想重用代码,这一点尤其重要,因为它必须应用于您想要重用它的数据结构的典型域(在这种情况下,我会说使用 OP 中的映射的典型场景正是您想节省存储为列表时所占用的空间,因此参数中相对较大的值并不是罕见的情况,而是典型的情况)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-08-10
  • 2019-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-01-24
  • 1970-01-01
相关资源
最近更新 更多