【Java集合篇】HashMap 是如何扩容的

在这里插入图片描述

HashMap 是如何扩容的

  • ✔️ 为什么需要扩容?
  • ✔️ 桶元素重新映射
  • ✔️链表重新链接
  • ✔️ 取消树化
  • ✔️拓展知识仓
    • ✔️除了rehash之外,哪些操作也会将树会退化成链表?


✔️ 为什么需要扩容?


HashMap在Java等编程语言中被广泛使用,用于存储键值对数据。HashMap的实现原理是基于哈希表,通过哈希函数将键转化为桶的位置,从而实现快速查找、插入和删除操作。


然而,当HashMap中的元素数量增加时,哈希冲突的概率也会随之增加,这可能导致HashMap的性能下降。具体来说,当一个桶中的元素过多时,查询、插入和删除操作的时间复杂度会变为O(n),这违背了HashMap设计的初衷。为了解决这个问题,HashMap会在必要时进行扩容。


扩容的目的是为了增加HashMap的容量,从而减少哈希冲突的概率,提高查询、插入和删除操作的性能。扩容的过程涉及到重新计算所有元素的哈希值和重新分配元素到新的桶中,这是一个相对耗时的操作。因此,为了平衡性能和扩容的开销,通常会将HashMap的容量设定为一个较大的初始值,并在需要时进行扩容。


另外,扩容还可以帮助维持HashMap的性能稳定性。由于负载因子是存储元素的数量与哈希表容量的比值,当负载因子过高时,哈希表的性能会下降。通过适时地扩容,可以控制负载因子在一个合适的范围内,从而保证HashMap的性能。


HashMap需要扩容是为了减少哈希冲突的概率,提高查询、插入和删除操作的性能,以及维持性能稳定性。


当我们在Java 中使用 HashMap 时,可以通过以下代码示例来理解扩容的原理:

import java.util.HashMap;public class HashMapExample {public static void main(String[] args) {// 创建一个初始容量为16,负载因子为0.75的HashMapHashMap<String, Integer> map = new HashMap<>(16, 0.75f);// 添加元素到HashMap中map.put("Alice", 25);map.put("Bob", 30);map.put("Charlie", 35);map.put("David", 40);// 输出当前HashMap的容量和已使用容量System.out.println("Initial capacity: " + map.capacity());System.out.println("Used buckets: " + map.size());// 添加更多元素,触发扩容map.put("Eve", 45);map.put("Frank", 50);map.put("Grace", 55);map.put("Henry", 60);// 再次输出当前HashMap的容量和已使用容量System.out.println("Current capacity: " + map.capacity());System.out.println("Used buckets: " + map.size());}
}

在上述代码中,我们创建了一个初始容量为16,负载因子为0.75的HashMap。当我们向HashMap中添加元素时,如果已使用容量超过了负载因子与当前容量的乘积,HashMap就会触发扩容。扩容后,HashMap的容量会增加,从而减少哈希冲突的概率,提高查询、插入和删除操作的性能。


参考前两篇博文:

【Java集合篇】负载因子和容量的关系
【Java集合篇】为什么HashMap的Cap是2^n,如何保证?


假设现在散列表中的元素已经很多了,但是现在散列表的链化已经比较严重了,哪怕是树化了,时间复杂度也没有O(1)好,所以需要扩容来降低Hash冲突的概率,以此来提高性能。


我们知道,当 ++size >threshold 之后详见java.util.HashMap#putVal 方法),HashMap就会初始化新的新的桶数组,该桶数组的size为原来的两倍,在扩大桶数组的过程中,会涉及三个部分:


1 . 如果某桶节点没有形成链表,则直接rehash到其他桶中


2 . 如果桶中形成链表,则将链表重新链接


3 . 如果桶中的链表已经形成红黑树,但是链表中的元素个数小于6,则进行取消树化的操作


✔️ 桶元素重新映射


如果桶中只有一个元素,没有形成链表,则将原来的桶引用置为null,同时,将该元素进行 rehash 即可,如下代码所示:


if (e.next == null) {newTab[e.hash & (newCap - 1)] = e;
}

在Java的HashMap中,当元素数量增加到一定阈值时,为了提高性能和减少哈希冲突,会触发桶的重新映射(rehashing)。这是通过扩容来实现的。

桶的重新映射过程涉及到以下步骤:

  1. 计算新的容量:扩容时,新的容量通常是旧容量的一个固定倍数(例如,默认情况下是原容量的两倍)。
  2. 生成新的桶数组:根据新的容量,创建一个新的桶数组。这个数组的长度是原来的两倍(在默认情况下)。
  3. 重新计算哈希值:遍历旧桶中的每个元素,并使用新的哈希函数重新计算它们的哈希值。这是为了确保元素能够均匀分布在新的桶数组中,减少哈希冲突。
  4. 重新放置元素:根据新计算的哈希值,将每个元素放置在新的桶数组中的适当位置。
  5. 更新HashMap状态:更新HashMap的容量、大小以及相关的内部状态。

这个过程是必要的,因为随着元素的增加,哈希冲突的概率也会增加,导致性能下降。通过扩容和重新映射,可以维持HashMap的高效性能。


✔️链表重新链接


假设有4个key,分别为a,b,c,d,且假定他们的hash值如下:


hash(a) = 3;  hash(a) & 7 = 3;    hash(a) & 8 = 0;
hash(b) = 11;  hash(b) & 7 = 3;   hash(b) & 8 = 8;
hash(c) = 27;  hash(c) & 7 = 3;   hash(c) & 8 = 8;
hash(d) = 59;  hash(d) & 7 = 3;hash(d) & 8 = 8;

假如此时HashMap的cap为 8,某个桶中已经形成链表,则可得到: table[3]=a->b->c->d。


如果此时扩容,将newCap设为16,我们可以看到如下结果:


hash(a) = 3; hash(a) & 15 = 3;
hash(b) = 11; hash(b) & 15 = 11;
hash(c) = 27; hash(c) & 15 = 11;
hash(d) = 59; hash(d) & 15 = 11;

我们会发现,当hash(k) & oldCap = 0 (即hash(a) = 3;的这个记录)时,这些链表的节点还是在原来的节点中(扩容后他的结果还是3) ,同时如果hash(k) & oldCap != 0时(11 27 59这几条记录),这些链表的节点会到桶中的其他的位置中 (从3变成了11)。


所以,对于链表来说,我们就不用逐人节点重新映射,而是直接通过hash(k) & ldCap进行分类,之后统一移动他们的位置即可。源码如下:


Node<K,V> loHead = null,loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {next = e.next;if ((e.hash & oldCap) == 0) {if (loTail == null) {loHead = e;} else {loTail.next = e;}loTail = e;} else {if (hiTail == null) {hiHead = e;} else {hiTail.next = e;}hiTail = e;}
} while((e = next) != null);
if (loTail != null) {loTail.next = null;newTab[j] = loHead;
}if (hiTail != null)  {hiTail.next = null;newTab[j + oldCap] = hiHead;
}

✔️ 取消树化


有了上面链表重新连接的经验,我们会发现,其实树化后的节点,也可以使用该操作来降低红黑树每人节点rehash时的时间复杂度,所以红黑树的 TreeNode 继承了链表的Node类,有了next字段,这样就可以像链表一样重新链接,源码如下:


TreeNode<K,V> loHead = null,loTail= null;
TreeNode<K,V> hiHead = null, hiTail = null;
for (TreeNode<K,V> e = b, next; e != null; e = next)  {next = (TreeNode<K,V>)e.next;e.next = null;if ((e.hash & bit) == 0) {if ((e.prev = loTail) == null)loHead = e;else loTail.next = e;loTail = e;++lc;} else {if ((e.prev = hiTail) == null)hiHead = e;elsehiTail.next = e;hiTail = e;++hc;}
}

当上面的操作完结后,HashMap会检测两个链表的长度,当元素小于等于6的时候,就会执行取消树化的操作,否则就会将新生成的链表重新树化。


取消树化非常简单,因为之前已经是条链表了,所以只需要将里面的元素由TreeNode转为Node即可。


至于重新树化的过程,请听下回分解~


✔️拓展知识仓


✔️除了rehash之外,哪些操作也会将树会退化成链表?


remove 元素的时候,这个过程中也会做退化的判断,如以下代码中,也会在这个分支中执行退化的操作(untreeify),如下代码所示:


if (root == null (movable 8& (root.right == null  (rl = root.left) == null rl.left == null))) {tab[index] = first.untreeify(map); // too smallreturn;
}

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

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

相关文章

WebofScience快速检索文献的办法

Web of Science为什么老是搜不到文章&#xff0c;原来是要在这个地方全部勾选 如果是搜标题的话&#xff0c;选Title&#xff0c;输入你要搜的文章标题 另外&#xff0c;也可以在浏览器直接搜文章标题&#xff0c;得到文章的DOI&#xff0c;然后选DOI&#xff0c;直接搜DOI也行…

CLIP is Also an Efficient Segmenter

表1 复现结果–Seed&#xff1a;70.7245673447014&#xff0c;dCRF&#xff1a;74.85437742935268 误差小于0.5个点&#xff0c;可以接受 表4 复现结果–训练300轮&#xff0c;Val&#xff1a;58.76741354153312&#xff0c;Test&#xff1a;59.18210 感想 VOC全部复现完成&…

【EAI 004】LLM+P:借助LLM和PDDL赋予机器人最优规划能力

论文标题&#xff1a;LLMP: Empowering Large Language Models with Optimal Planning Proficiency 论文作者&#xff1a;Bo Liu, Yuqian Jiang, Xiaohan Zhang, Qiang Liu, Shiqi Zhang, Joydeep Biswas, Peter Stone 作者单位&#xff1a;Department of Computer Science, Th…

关于使用统一服务器,vscode和网页版jupyter notebook的交互问题

autodl 查看虚拟环境 在antodl上租借了一个服务器&#xff0c;通过在网页上运行jupyter notebook和在vscode中运行&#xff0c;发现环境都默认的是miniconda3。 conda info --envs 当然环境中所有的包都是一样的。 要查看当前虚拟环境中安装的所有包&#xff0c;可以使用以…

用通俗易懂的方式讲解:使用 Mistral-7B 和 Langchain 搭建基于PDF文件的聊天机器人

在本文中&#xff0c;使用LangChain、HuggingFaceEmbeddings和HuggingFace的Mistral-7B LLM创建一个简单的Python程序&#xff0c;可以从任何pdf文件中回答问题。 一、LangChain简介 LangChain是一个在语言模型之上开发上下文感知应用程序的框架。LangChain使用带prompt和few…

揭秘六大热门认证考试

六大热门认证考试是什么❓今天为大家详细解读PMP、ACP、CDGA、软考中项、软考高项、NPDP、CISP等热门认证考试&#xff0c;让你不再彷徨&#x1f447; 1️⃣PMP &#x1f451;PMP认证是全qiu公ren的项目管理专业认证&#xff0c;旨在评估项目管理人员在项目过程中所需的知识、技…

Java学习苦旅(二十二)——MapSet

本篇博客将详细讲解Map和Set。 文章目录 搜索概念模型 MapMap.Entry<K, V>Map的常用方法说明TreeMap和HashMap的区别 Set常用方法说明TreeSet和HashSet的区别 结尾 搜索 概念 Map和set是一种专门用来进行搜索的容器或者数据结构&#xff0c;其搜索的效率与其具体的实例…

C++流媒体服务器 ZLMediaKit框架ZLToolKit源码解读

ZLMediaKit是国人开发的开源C流媒体服务器&#xff0c;同SRS一样是主流的流媒体服务器。 ZLToolKit是基于C11的高性能服务器框架&#xff0c;和ZLMediaKit是同一个作者&#xff0c;ZLMediaKit正是使用该框架开发的。 ZLMediaKit开源地址&#xff1a;https://github.com/ZLMedi…

RT-DETR Gradio 前端展示页面

效果展示 使用方法 Gradio 是一个开源库,旨在为机器学习模型提供快速且易于使用的网页界面。它允许开发者和研究人员轻松地为他们的模型创建交互式的演示,使得无论技术背景如何的人都可以方便地试用和理解这些模型。使用Gradio,你只需几行代码就可以生成一个网页应用程序,…

route 命令

格式&#xff1a; route [-f] [-p] [Command] [Destination] [mask Netmask] [Gateway] [metric Metric] [if Interface] 功能&#xff1a; route 命令是用于操作基于内核ip路由表&#xff0c;它的主要作用是创建一个静态路由让指定一个主机或者一个网络通过一个网络接口。 …

CSS效果(工作中常用)

1、css文字溢出省略号 overflow: hidden; // 溢出隐藏 text-overflow: ellipsis; // 溢出用省略号显示 white-space: nowrap; // 规定段落中的文本不进行换行 overflow: hidden; // 溢出隐藏 text-overflow: ellipsis; // 溢出用省略…

【Docker基础二】Docker安装Mysql8

下载镜像 安装mysql&#xff08;版本&#xff1a;8.0.35&#xff09; # 拉取镜像 docker pull mysql:8.0.35 # 查看镜像是否已经下载 docker images 创建挂载目录 # 宿主机上创建挂载目录 (可以不创建&#xff0c;docker run -v配置了挂载目录&#xff0c;docker会自动…