我将使用以下逻辑循环插入到 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 可能是唯一的选择
- 否则,大小可能不是决定因素
在这种特定情况下,一个关键因素是与整体数据集大小相比,唯一整数的预期数量是大还是小?
- 如果很小,那么整体时间将由小集合中的键查找主导,因此优化无关紧要(您可以在这里停止)。
- 如果很大,那么总时间将由insert控制,并且决定取决于更多因素:
- 数据集大小已知?
- 如果是:可以预先分配 HashMap,从而消除内存流失。如果 hashCode() 方法很昂贵(在我们的例子中不是),这一点尤其重要
- 如果否:TreeMap 提供更可预测的性能,可能是更好的选择
- 是否需要无需大停顿的可预测性能,例如在实时系统中或在 GUI 的事件线程上?
- 如果是:TreeMap 提供了更好的可预测性,没有停顿
- 如果否:HashMap 可能为整个计算提供更好的整体性能
最后一点,如果上面没有压倒性的一点:
- 是一个排序的值键列表吗?
- 如果是(例如打印直方图):TreeMap 已经对键进行了排序,因此很方便
但是,如果性能很重要,那么唯一的决定方法是实现 Map 接口,然后分析HashMap 和 TreeMap 以查看在您的情况下哪个实际上更好。过早的优化是万恶之源 :)