我会循环使用以下逻辑将集合插入到 Map 数据结构中:
- 如果整数尚未插入映射,则插入 key=integer, value=1。
- 如果键存在,则增加值。
您可以使用 Java 中的两个 Map - HashMap 和 TreeMap - 比较如下:
HashMap 与 TreeMap
如果您愿意,可以跳过详细说明直接跳转到摘要。
HashMap 是一种将键值对存储在数组中的 Map。用于键 k 的索引是:
- h.hashCode() % map.size()
有时两个完全不同的键最终会出现在同一个索引中。为了解决这个问题,数组中的每个位置实际上都是一个链表,这意味着每次查找都必须遍历链表并使用 k.equals(other) 方法检查是否相等。最坏的情况是,所有的键都存储在同一个位置,HashMap 变成了一个未索引的列表。
随着 HashMap 获得更多条目,这些冲突的可能性增加,并且结构的效率降低。为了解决这个问题,当条目数达到临界点(由构造函数中的 loadFactor 参数确定)时,结构会被调整大小:
- 分配的新数组大约是当前大小的两倍
- 循环遍历所有现有键
如您所见,如果有很多调整大小,这可能会变得相对昂贵。
如果您可以在开始之前以适当的大小预先分配 HashMap,则可以解决此问题,例如 map = new HashMap(input.size()*1.5)。对于大型数据集,这可以显着减少内存流失。
因为键在 HashMap 中基本上是随机定位的,所以键迭代器将以随机顺序遍历它们。 Java 确实提供了 LinkedHashMap,它将按照插入键的顺序进行迭代。
HashMap 的性能:
- 考虑到正确的大小和良好的散列分布,查找是固定时间的。
- 如果分布不好,性能下降到(在最坏的情况下)线性搜索 - O(n)。
- 如果初始大小设置不当,性能就会变成重新散列的性能。这个我没法简单计算,但是不好。
OTOH TreeMap 将条目存储在平衡树中 - 一种动态结构,随着键值对的添加而逐渐建立。插入取决于树的深度 (log(tree.size()),但可预测 - 与 HashMap 不同,没有中断,也没有性能下降的边缘条件。
考虑到分布良好的 HashMap,每次插入和查找的成本都更高。
此外,为了在树中插入键,每个键都必须与其他所有键可比较,这需要 Comparable 接口中的 k.compare(other) 方法。显然,鉴于问题是关于整数的,这不是问题。
TreeMap 的性能:
- n 个元素的插入是 O(n log n)
- 查找时间为 O(log n)
总结
第一想法:数据集大小:
- 如果很小(即使在 1000 和 10,000 中),在任何现代硬件上都无关紧要
- 如果大到导致机器内存不足的地步,那么 TreeMap 可能是唯一的选择
- 否则,大小可能不是决定因素
在这种特定情况下,一个关键因素是与整体数据集大小相比,预期的唯一整数数量是大还是小?
- 如果很小,那么总时间将由 small set 中的 key lookup 支配,因此优化无关紧要(您可以在此处停止)。
- 如果很大,那么总时间将由 insert 支配,而决定取决于更多因素:
- 数据集大小已知?
- 如果是:可以预先分配 HashMap,从而消除内存流失。如果 hashCode() 方法很昂贵(在我们的例子中不是),这一点尤其重要
- 如果否:TreeMap 提供更可预测的性能,可能是更好的选择
- 是否需要无需大停顿的可预测性能,例如在实时系统中或在 GUI 的事件线程上?
- 如果是:TreeMap 提供了更好的可预测性,没有停滞
- 如果否:HashMap 可能为整个计算提供更好的整体性能
如果从上方没有压倒性的一点,最后一点:
- 是一个排序的值键列表吗?
- 如果是(例如打印直方图):TreeMap 已经对键进行了排序,方便
但是,如果性能很重要,唯一的决定方法是实现 Map 接口,然后 profile HashMap 和 TreeMap 以查看哪个在您的情况下实际上更好。 Premature optimization 是万恶之源 :)