在内部,Hashmaps
使用hashfunction
查找查询bin
所属的key
。这些bins
中的每一个本身都是LinkedList
。
我不明白如果这些LinkedLists
可能会变得很长,并且LinkedLists
没有持续的访问时间,而是线性访问时间,那么访问时间是如何保持不变的。
即使由于某些原因垃圾箱太大,Java Collections
库如何设法保证持续的访问时间?内部发生了什么? Java在内部做了什么来最大限度地减少这种负面影响?
答案 0 :(得分:5)
每个bin中的平均元素数量由一个小常量绑定。通过保持箱的数量至少与条目总数乘以载荷因子(其默认值为0.75)一样高来维持这一点。
为了保持这种不变性,容器的数量会随着条目的数量而增加。
以下是相关代码(Java 7):
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}
size
是条目数,table.length
是区间数,threshold
是table.length * loadFactor
。
如果您使用默认加载因子0.75(或任何加载因子&lt; 1),则bin的数量将始终高于条目数,因此除非您的密钥类具有非常糟糕的hashCode, bin平均不会有多个条目。
答案 1 :(得分:2)
documentation tells you,如果负载系数过高,会发生什么:
HashMap的一个实例有两个影响其性能的参数:初始容量和负载因子。容量是哈希表中的桶数,初始容量只是创建哈希表时的容量。加载因子是在自动增加容量之前允许哈希表获取的完整程度的度量。 当哈希表中的条目数超过加载因子和当前容量的乘积时,哈希表被重新哈希(即重建内部数据结构),以便哈希表具有近似值桶数量的两倍。
此外,您还可以查看源代码,其中列出了implementation notes的完整列表。最重要的是:
此映射通常用作分区(分区)散列表,但是当分区变得太大时,它们会转换为TreeNodes的分区,每个分区的结构与java.util.TreeMap中的类似。
并进一步:
由于TreeNodes的大小约为常规节点的两倍,因此我们仅在bin包含足够的节点以保证使用时使用它们(请参阅TREEIFY_THRESHOLD)。当它们变得太小(由于移除或调整大小)时,它们会转换回普通垃圾箱。
简而言之:
O(ln(t))
搜索bin的大小为t
。所以大型箱子有一个悬挂它们的二叉树。答案 2 :(得分:2)
我不明白如果这些链接列表可能会变得非常长,访问时间是如何不变的
HashMap
不提供有保证的常量访问时间。它提供了分摊的常量时间,这是另一回事:n
项的总体访问平均为O(1),但每个访问可能是O(n)。< / p>
此外,仅当散列函数为“好”时才实现摊销的常数时间。当散列函数很糟糕时(例如,返回一个常量,这是一个有效但非常糟糕的散列函数),库很无奈:无论实现的是什么,访问时间都是线性的。
当多个哈希码相同时,链表将增长,以桶的数量为模。但是,由于HashMap
为其桶数选择素数,因此链表变得非常长的最常见情况是许多哈希码实际上是相同的,而不考虑模数。因此,简单地将桶的数量增加到更大的素数不会减少列表的长度:它会将列表移动到不同的桶,或者将其保留在旧的位置,但列表的长度不会减少。
答案 3 :(得分:1)
如果哈希表太满,则需要重新哈希。要重新散列表,将创建另一个包含更多存储桶的表,并将所有元素插入到新表中。原始表格被丢弃。
负载系数决定何时重新散列。默认值为0.75,因此当表格大于75%时,它会自动重新加载两倍的桶。
要在表中查找位置,计算哈希码并以桶的数量为模。这个想法是散列函数应该在某种程度上随机分布对象,因此碰撞的数量应该很少,因此不应该进行太多的比较。