1. HashMap 的底层数据结构

HashMap 是 Java 中实现了 Map 接口的一个常用类,主要用来存储键值对(Key-Value)。它底层依赖于 哈希表(Hash Table)实现,主要使用 数组链表(或 红黑树)两种数据结构。

主要组成:

  • 数组HashMap 使用一个数组来存储所有的桶(bucket),每个桶可以存储一个或多个键值对。
  • 链表:当多个键值对的哈希值相同时(即哈希冲突),这些元素会存储在同一个桶的位置,通过链表来组织。
  • 红黑树:当链表的长度超过一定阈值(默认为 8),链表会转换为红黑树,以提高查找效率。

数据结构演变:

  • JDK 1.7 及之前版本:哈希冲突通过链表解决。
  • JDK 1.8 及以后版本:链表长度超过阈值时,链表会转为红黑树。

2. put 操作的执行流程

put 方法用于将一个键值对插入 HashMap 中,具体过程如下:

(1) 计算哈希值

  • 调用 key.hashCode() 计算键的哈希值。为了减少哈希冲突,哈希值会进行扰动处理(位运算)。
int hash = key.hashCode();

(2) 计算数组索引位置

  • 通过 hash 与数组长度(n)的位运算来确定该元素应该存储在数组的哪个桶位置。该操作可以帮助降低哈希冲突。
int index = (n - 1) & hash;  // 计算桶的索引位置

(3) 检查该位置是否为空

  • 如果目标桶位置为空,则直接插入该键值对。
  • 如果桶位置已经有元素(发生哈希冲突),则需要进行冲突解决。

(4) 处理哈希冲突

  • 链表法:如果目标位置有元素(发生哈希冲突),则会通过链表(或红黑树)进行处理。每个链表节点存储一个键值对,发生冲突时,新的键值对会被添加到链表末尾。
  • 红黑树法:当链表长度超过阈值(默认 8)时,链表会转为红黑树,以提高查找效率。

(5) 扩容判断

  • 如果 HashMap 的元素数量超过负载因子(默认为 0.75)与当前容量的乘积时,HashMap 会进行扩容操作,将数组的容量加倍,并重新分配桶的位置。
if (++size > threshold) {
    resize();  // 扩容
}

3. put 操作流程图示

假设我们要插入两个元素:

put("key1", "value1");
put("key2", "value2");
  • 计算哈希值并通过哈希值计算桶的位置:

    • hash(key1) -> index0
    • hash(key2) -> index1
  • 将元素插入相应的位置:

    • [桶0] -> (key1, value1)
    • [桶1] -> (key2, value2)
  • 如果发生冲突,则会将新元素插入到链表的末尾。
[桶0] -> (key1, value1)
[桶1] -> (key2, value2)

[哈希冲突] -> 链表/红黑树

4. 性能分析

  • 平均时间复杂度put 操作的平均时间复杂度是 O(1),即常数时间。前提是哈希表负载均衡,哈希冲突较少。
  • 最坏时间复杂度:最坏情况下,如果所有元素都被存储在同一个桶中(发生严重冲突),则时间复杂度是 O(n),其中 n 是 HashMap 中的元素数量。但这种情况通常较少发生,通过良好的哈希函数和扩容机制可以避免。

5. 扩容机制

HashMap 会在元素数量超过负载因子(默认是 0.75)与当前容量的乘积时进行扩容。扩容是将数组的大小翻倍,并将现有元素重新映射到新的数组位置。扩容过程会消耗较多时间,因此,初始化时尽量合理配置 HashMap 的初始容量和负载因子,以避免频繁扩容。

if (++size > threshold) {
    resize();  // 扩容
}

6. 总结

  • HashMap 的底层是基于数组和链表(或红黑树)来存储元素的。
  • put 操作的主要步骤包括计算哈希值、确定桶的位置、处理哈希冲突、扩容等。
  • HashMap 提供高效的存储和查询操作,在大多数情况下,put 的时间复杂度为 O(1)。
  • 在极端情况下,哈希冲突较多时,最坏时间复杂度可能是 O(n),但通过合理的哈希函数和扩容机制可以降低冲突的发生概率。

今夜有点儿凉
40 声望1 粉丝

今夜有点儿凉,乌云遮住了月亮。


引用和评论

0 条评论