核心数据结构:数组 + 链表 / 红黑树

HashMap 的底层核心是一个 Node<K,V>[] table 数组(通常称为 桶数组 或 哈希桶数组)。这个数组的每个元素称为一个 

  1. Node<K,V> (链表节点):

    • 这是存储键值对的基本单位,当没有哈希冲突或冲突较少时使用。

    • 定义(简化):

      static class Node<K,V> implements Map.Entry<K,V> {final int hash; // 存储键的最终哈希值(经过扰动处理后的)final K key;    // 键,final 确保一旦放入,键引用不可变(但对象内容可变有风险!)V value;       // 值,可以修改Node<K,V> next; // 指向链表下一个节点的引用// ... 构造函数、getter/setter、equals、hashCode 等方法 ...
      }

    • 当两个不同的键通过 (table.length - 1) & hash 计算出的 桶索引(下标)相同时,就发生了 哈希冲突。冲突的键值对会以单向链表的形式存储在同一个桶中。新节点在 Java 8+ 中插入到链表尾部

  2. TreeNode<K,V> (红黑树节点):

    • 这是 Node 的子类,当链表过长时使用。

    • 定义(简化):

      static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {TreeNode<K,V> parent;  // 父节点TreeNode<K,V> left;    // 左子节点TreeNode<K,V> right;   // 右子节点TreeNode<K,V> prev;    // 前驱节点(在删除时有用,也可视为链表的上一个节点)boolean red;           // 节点颜色 (true=红, false=黑)// ... 树操作相关的方法(如旋转、插入平衡、删除平衡、查找等)...
      }

    • 树化条件: 当某个桶中的链表长度达到阈值 TREEIFY_THRESHOLD (默认为 8并且 当前桶数组 table 的长度达到 MIN_TREEIFY_CAPACITY (默认为 64) 时,这个链表会被转换为 红黑树

    • 退化条件: 当桶中的红黑树节点数量减少到阈值 UNTREEIFY_THRESHOLD (默认为 6) 时,红黑树会退化为单向链表。

    • 为什么引入红黑树? 在极端情况下(如大量键哈希冲突),链表会变得非常长,导致查询效率从理想的 O(1) 退化到 O(n)。红黑树是一种自平衡二叉查找树,它能将最坏情况下的查询、插入、删除操作的时间复杂度维持在 O(log n),显著提升了性能。

关键过程详解

  1. put(K key, V value) - 插入键值对

    1. 计算哈希 (hash(key)):

      • 首先调用键对象的 key.hashCode() 获取原始哈希值 h

      • 然后进行 扰动处理(h = key.hashCode()) ^ (h >>> 16)

      • 为什么扰动? 原始哈希码的高位变化通常更丰富,但桶索引计算 (n-1) & hash 只用到低位(因为 n 是 2 的幂,n-1 低位全是 1)。h ^ (h >>> 16) 将原始哈希码的高 16 位特性异或到低 16 位中,增加了低位的随机性,显著减少了不同哈希码但低位相同(导致冲突)的概率,使元素分布更均匀。

    2. 计算桶索引 (i = (n - 1) & hash):

      • n 是当前桶数组 table 的长度(table.length),总是 2 的幂

      • (n - 1) & hash:因为 n 是 2 的幂,n-1 的二进制表示是低位连续一串 1 (例如 15 -> 0b111131 -> 0b11111)。& 操作相当于取 hash 值低 log2(n) 位的值,效果等同于 hash % n,但位运算效率远高于取模运算。结果 i 就是键值对应该放入的桶的索引(0 到 n-1)。

    3. 处理目标桶 (table[i]):

      • 情况 1:桶为空 (table[i] == null)

        • 直接创建一个新的 Node 节点 newNode(hash, key, value, null),放入 table[i]

      • 情况 2:桶不为空

        • 遍历桶中的结构(可能是链表或树)。

        • 查找键是否存在: 对于每个节点 p

          • 先比较 p.hash == hash (快速失败,哈希不同肯定不是同一个键)。

          • 再比较 (p.key == key) 或 (key != null && key.equals(p.key)) (引用相等或逻辑相等)。

        • 找到匹配节点: 如果找到键相等的节点 p,用新值 value 覆盖 p.value,并返回旧值(put 方法本身有返回值)。

        • 未找到匹配节点:

          • 如果是链表:

            • 创建新 Node,插入到链表尾部 (Java 8+ 尾插法)。

            • 插入后检查链表长度是否 >= TREEIFY_THRESHOLD (8)。

              • 如果 >=8 且 table.length >= MIN_TREEIFY_CAPACITY (64),则调用 treeifyBin(tab, hash) 方法将此链表转换为红黑树。

              • 如果 >=8 但 table.length < 64,则只调用 resize() 进行扩容(扩容可能直接分散冲突,避免树化开销)。

          • 如果是红黑树: 调用红黑树的插入方法 putTreeVal(this, tab, hash, key, value) 来插入新节点。该方法内部会处理树的平衡。

    4. 修改计数 & 检查扩容:

      • 如果添加了新节点(不是覆盖旧值),则 modCount++(用于 fail-fast 机制)。

      • 检查当前元素总数 size 是否 > threshold (threshold = capacity * loadFactor)。如果超过,则调用 resize() 进行扩容

  2. get(Object key) - 获取值

    1. 计算哈希 (hash(key)): 同 put 过程。

    2. 计算桶索引 (i = (n - 1) & hash): 同 put 过程。

    3. 查找目标桶 (table[i]):

      • 桶为空: 返回 null

      • 桶不为空:

        • 检查第一个节点: 如果桶中第一个节点(链表头或树根)的 hash 和 key 匹配(比较规则同 put),直接返回该节点的 value

        • 后续节点:

          • 如果是链表: 遍历链表,用 hash 和 key 匹配查找节点。

          • 如果是红黑树: 调用红黑树的查找方法 getTreeNode(hash, key) 进行查找。

        • 找到匹配节点: 返回其 value

        • 未找到: 返回 null

  3. resize() - 扩容 (核心难点与优化点)

    • 触发条件: size > threshold 或初始化时 table == null

    • 目的: 增加桶的数量,减少哈希冲突,保持 O(1) 的访问性能。

    • 过程:

      1. 计算新容量和阈值:

        • 旧容量 oldCap,旧阈值 oldThr

        • 如果 oldCap > 0

          • 如果 oldCap >= MAXIMUM_CAPACITY:设置阈值为 Integer.MAX_VALUE,不再扩容,直接返回。

          • 否则,新容量 newCap = oldCap << 1 (扩大为 2 倍),新阈值 newThr = oldThr << 1 (也是 2 倍)。

        • 否则,如果是初始化 (oldCap == 0 && oldThr > 0):newCap = oldThr (使用构造时指定的初始容量)。

        • 否则,完全初始化 (oldCap == 0 && oldThr == 0):newCap = DEFAULT_INITIAL_CAPACITY (16), newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY) (12)。

        • 如果 newThr == 0:重新计算 newThr = (int)(newCap * loadFactor)

      2. 创建新数组: 创建一个新的 Node 数组 newTab,长度为 newCap

      3. 重新映射节点 (Rehashing - Java 8 优化精髓):

        • 遍历旧数组 table 的每个桶 oldTab[j]

        • 如果桶不为空:

          • 情况 1:桶中只有一个节点 (无冲突): 直接计算该节点在新数组中的位置 e.hash & (newCap - 1),放入 newTab

          • 情况 2:桶中是链表或红黑树:

            • Java 8 优化关键: 利用 newCap 是 oldCap 2 倍 (oldCap << 1) 的特性。旧数组长度 oldCap 是 2 的幂,其二进制表示是 1 后面跟着一串 0 (如 16: 0b10000)。因此,e.hash & oldCap 的结果只有两种可能:0 或 oldCap (非0)。这个位运算实际上是在检查 e.hash 在 oldCap 对应二进制位(即新增的最高位)上是 0 还是 1

            • 创建两个链表头节点 loHead/loTail (低位链) 和 hiHead/hiTail (高位链)。

            • 遍历旧桶中的每个节点 e

              • 如果 (e.hash & oldCap) == 0:则该节点在新数组中的位置 = j (原索引位置)。将其添加到 loHead/loTail 链表尾部。

              • 如果 (e.hash & oldCap) != 0:则该节点在新数组中的位置 = j + oldCap (原索引 + 旧容量)。将其添加到 hiHead/hiTail 链表尾部。

            • 链表处理:

              • 如果 loTail != null:将低位链 loHead 放入 newTab[j]

              • 如果 hiTail != null:将高位链 hiHead 放入 newTab[j + oldCap]

            • 红黑树处理: 如果旧桶是红黑树,会调用 split 方法。该方法同样使用 (e.hash & oldCap) == 0 规则将树节点拆分到 lo 和 hi 两个链表/树中。拆分后:

              • 如果链表长度 <= UNTREEIFY_THRESHOLD (6):调用 untreeify 退化成链表,放入新桶。

              • 否则,检查拆分后的两个链表是否还能构成树(通常长度>6会尝试树化),然后放入新桶的相应位置。

      4. 更新引用: 将 table 指向新数组 newTab,更新 threshold = newThr

关键设计点与参数

  • 容量 (capacity): 桶数组 table 的长度。必须是 2 的幂。默认初始容量 DEFAULT_INITIAL_CAPACITY = 16。构造时可指定,但 HashMap 会自动调整为大于等于指定值的最小 2 的幂(如指定 10,实际容量为 16)。

  • 大小 (size): 当前 HashMap 中存储的键值对数量。

  • 负载因子 (loadFactor): 一个浮点数,默认 DEFAULT_LOAD_FACTOR = 0.75f。用于计算扩容阈值:threshold = capacity * loadFactor。当 size > threshold 时扩容。0.75 是空间和时间效率的一个较好平衡点。

  • 扩容阈值 (threshold): capacity * loadFactor。当 size 超过此值时触发 resize()

  • 树化阈值 (TREEIFY_THRESHOLD): 链表转换为红黑树的链表长度阈值,默认为 8。基于泊松分布统计,链表长度达到 8 的概率已经非常小(约千万分之一),此时树化的收益大于开销。

  • 退化阈值 (UNTREEIFY_THRESHOLD): 红黑树退化为链表的节点数阈值,默认为 6。避免在临界值附近频繁树化退化。

  • 最小树化容量 (MIN_TREEIFY_CAPACITY): 进行树化的最小桶数组容量要求,默认为 64。在桶数量太少时,优先扩容分散节点比树化更有效。

为什么容量必须是 2 的幂?

  1. 高效计算桶索引: 使用 (n - 1) & hash 代替 hash % n,位运算 & 比取模 % 快得多。

  2. 优化扩容时的节点迁移 (Java 8 精髓): 扩容时,节点的新位置只可能是原位置 j 或 j + oldCap。这个结论成立的关键就是 oldCap 是 2 的幂,使得 e.hash & oldCap 的结果只有 0 或非 0 两种可能。这避免了重新计算每个节点的哈希值,极大提高了扩容效率

总结

HashMap 的底层核心是一个动态扩容的桶数组。它通过计算键的哈希值(经过扰动)确定桶的位置。使用链表解决哈希冲突,并在链表过长(且桶数组足够大)时将其转换为红黑树以保证最坏情况下的性能。其设计精髓在于:

  1. 扰动函数: 减少哈希冲突。

  2. 2 的幂容量 & 位运算索引: 高效定位桶。

  3. 链表 + 红黑树: 平衡普通情况和最坏情况下的性能。

  4. 扩容优化 (Java 8+): 利用 e.hash & oldCap 快速确定节点在新数组中的位置(只有两种可能),避免重新哈希,大幅提升扩容效率。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/86465.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/86465.shtml
英文地址,请注明出处:http://en.pswp.cn/pingmian/86465.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

历史项目依赖库Bugfix技巧-类覆盖

在项目维护过程中&#xff0c;我们可能会遇到历史项目依赖的第三方库出现BUG而需要修复的情况&#xff0c;而这些第三方库可能来源于公司自主开发或开源项目&#xff0c;但由于各种原因&#xff0c;这些库可能已无人维护。 此时&#xff0c;解决这个问题有三个办法 1、基于源…

多模态大型语言模型最新综述

多模态大型语言模型&#xff08;Multimodal Large Language Models&#xff0c;MLLMs&#xff09;已迅速发展&#xff0c;超越了文本生成的范畴&#xff0c;如今能够覆盖图像、音乐、视频、人类动作以及三维物体等多种输出模态。它们通过在统一架构下将语言与其他感知模态整合&…

使用ASIO的协程实现高并发服务器

使用ASIO的协程实现高并发服务器 在 C 网络编程领域&#xff0c;Asio 库提供了两种主要的异步编程范式&#xff1a;传统的回调模式和基于协程的现代模式&#xff0c;传统的回调模式大家都很清楚&#xff0c;这里不多做介绍&#xff0c;本文主要介绍基于协程的模式&#xff0c;…

OpenCV——轮廓检测

轮廓检测 一、轮廓检测二、轮廓的层级三、轮廓的特征3.1、轮廓面积3.2、轮廓周长3.3、边界矩形3.4、最小外接圆3.5、近似轮廓3.6、凸包 一、轮廓检测 轮廓可以简单的描述为具有相同颜色或灰度的连续点连在一起的一条曲线&#xff0c;轮廓通畅会显示出图像中物体的形状。关于轮…

高等概率论题解-心得笔记【15】

文章目录 拓扑参考文献 拓扑 参考文献 《测度论基础与高等概率论》

Windows 10关闭自动更新功能

Windows 10关闭自动更新功能&#xff0c;大家是不是经常用下面的几个步骤&#xff1a; 1、禁用Windows Update服务&#xff1b; 2、在组策略里关闭Win10自动更新相关服务&#xff1b; 3、禁用任务计划里边的Win10自动更新&#xff1b; 4、在注册表中关闭Win10自动更新&…

[Meetily后端框架] 配置指南 | 后端API网关 | API文档体系

链接: https://github.com/Zackriya-Solutions/meeting-minutes docs&#xff1a;会议纪要管理系统 本项目是一个专门用于**处理会议记录**的后端系统。 系统接收会议文本内容&#xff0c;利用先进的AI模型自动识别关键信息&#xff0c;包括行动项、决策内容以及截止期限。 处…

Flink2.0 配置 historyserver

Flink2.0 配置 historyserver 主要是去修改config.yaml配置文件 主要修改的点有两个 网上很多文档都是写的只配置一个 都是坑啊 historyserver :历史服务器 运行 Flink job 的集群一旦停止(例如yarn模式&#xff0c;程序一旦停止&#xff0c;集群也就关闭了)&#xff0c;只能去…

LLM的训练过程

一般而言&#xff0c;训练一个完整的 LLM 需要经过图1中的三个阶段——Pretrain、SFT 和 RLHF。 1.预训练 Pretrain&#xff0c;即预训练&#xff0c;是训练 LLM 最核心也是工程量最大的第一步。LLM 的预训练和传统预训练模型非常类似&#xff0c;同样是使用海量无监督文本对随…

用 AI + Canvas 生成图形、动画与图表

摘要 随着人工智能&#xff08;AI&#xff09;技术与 Web 可视化的结合&#xff0c;前端开发者可以通过自然语言生成复杂的图表、动画和交互式画布&#xff0c;极大地提升了开发效率和用户体验。本文作为《AI 前端&#xff1a;构建智能化 Web 应用的未来》专栏的第七篇&#…

SQL Server for Linux 如何实现高可用架构

关键词&#xff1a;SQL Server for Linux、高可用、读写分离、动态扩容、Always On、可用性组 &#x1f4cb; 文章目录 前言&#xff1a;Linux上的SQL Server不再是梦高可用架构设计 Always On 可用性组故障转移集群实例 读写分离架构 可用性组读写分离应用层读写分离 动态扩…

【51单片机流水灯控制4种造型,按下1,2,3,4时,数码管对应显示键号,同时流水灯对应四种造型】2022-6-1

缘由流水灯控制4种造型&#xff0c;按下1,2,3,4时&#xff0c;数码管对应显示键号&#xff0c;同时流水灯对应四种造型-编程语言-CSDN问答 #include "REG52.h" unsigned char code smgduan[]{0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5…

设计模式 - 工厂方法

工厂方法是一种设计模式&#xff0c;对工厂制造方法进行接口规范化&#xff0c;允许子类工厂决定具体知道哪类产品的实例&#xff0c;最终降低系统耦合&#xff0c;使系统的可维护性、可扩展性等得到提升。 一、工厂的多元化与专业化 要实例化对象&#xff0c;就得用到关键词“…

数据应该如何组织,才能让Excel“读懂”?

前言&#xff1a;如果你希望Excel能“读懂”你的数据&#xff0c;就得学会让排序、筛选、数据透视表、函数等这些功能为我们服务。 假设你在和一个非常聪明但有点“死板”的机器人&#xff08;Excel&#xff09;对话&#xff0c;你必须用它能理解的语言来组织信息。 “一维表”…

js防止重复提交的3种解决方案

防止 javascript 重复点击和提交的关键方法有三种&#xff1a;1. 禁用按钮法&#xff0c;点击后立即禁用按钮并更改文本提示&#xff0c;请求完成后恢复&#xff1b;2. 节流函数&#xff08;throttle&#xff09;&#xff0c;限制函数在设定时间间隔内仅执行一次&#xff0c;适…

【信创-k8s】银河麒麟V10国防版+鲲鹏/飞腾(arm64架构)在线/离线部署k8s1.30+kubesphere

银河麒麟作为国家核高基专项的重要成果&#xff0c;国防版凭借其卓越的安全性和可靠性&#xff0c;已成为军工领域的首选操作系统。之前我们在适配麒麟V4国防版的过程中已发现诸多安全性要求&#xff0c;而麒麟V10国防版在安全防护等级上又达到了更高的级别。 本文将主要演示离…

解锁单周期MIPS硬布线:Logisim实战全攻略

目录 一、引言二、MIPS 架构与单周期设计原理2.1 MIPS 架构概述2.2 单周期设计原理剖析 三、Logisim 工具基础3.1 Logisim 简介3.2 基本操作与组件认识 四、单周期 MIPS 硬布线设计步骤4.1 了解 MIPS 指令集4.2 搭建数据通路4.3 设计硬布线控制器4.4 在 Logisim 中创建电路 五、…

7.4.2B+树

B树&#xff1a; (1)每个分支节点最多有m个子树(孩子节点)。 阶&#xff1a;即看当前的B树是几阶B树&#xff0c;就看每个分支节点最多有几个子树&#xff0c;还是看最下一层有几个分叉就是几阶&#xff1f;&#xff1f;&#xff1f; 叶子节点&#xff1a;最下边的一层叫叶子…

MFC获取本机所有IP、局域网所有IP、本机和局域网可连接IP

获取本机所有IP地址 // 获取本机所有IP地址 int CMachine::GetLocalIPs(std::vector<CString>& vIPValue) {//返回IP数量&#xff0c; -1表示获取失败vIPValue.clear();int IpNum 0;//1.初始化wsa WSADATA wsaData;int ret WSAStartup(MAKEWORD(2, 2), &wsaD…

【C语言】贪吃蛇小游戏

1. 所需知识 C语言函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API... 2. Win32 API介绍 2.1 Win32 API windows这个多作业系统除了协调应用程序的执行、分配内存、管理资源之外&#xff0c;它同时也是一个很大的服务中心&#xff0c;调用这个服务中心的各种…