【从零到Offer】- HashMap与HashSet

​ HashMap与HashSet是我们日常最常使用的两个集合类。在实现上,两者也有很大的相似性。HashSet基本就是对HashMap的一个简单包装。

​ 为了更好的理解Hash结构的实现原理,从而更好的指导我们的代码使用,本文就主要对HashMap的实现及设计做分析介绍。

底层数据结构

​ HashMap属于经典的K-V数据结构,因此,在HashMap中定义了一个**Node<K,V>**对象,借助于泛型的编程实现,hashMap可以轻易地用于记录当前需要保存的Key及Value对象。

​ 同时,由于这类对象是多个,因此hashMap的最底层结构是基于一个Node<K,V>[]对象进行操作的。对于出现哈希冲突的情况,常见的哈希处理方法有两种:开放寻址法链地址法。HashMap的实现,主要基于链地址法实现的。

综上,我们可以简单绘制出hashMap的底层结构如下所示:

image-20230514191307273

​ 结合着上述对于hashMap结构的理解,我们可以很简单地得出一个元素,其添加到hashMap中的过程大致如下所示:

image-20230514191322931

​ 当然,实际情况中hashMap会更复杂一些,由于考虑到哈希冲突的性能问题,过长的哈希冲突产生的链表会使得检索效率从原本的O(1)降低至最差的O(n)的情况(如果每个元素都冲突的情况。)。

​ 为此,HashMap在长度超过8时候会将链表结构转换成红黑树进行处理,而在长度小于6的时候,又会重新恢复成链表结构。具体的实现内容,我会在接下来的关键方法的源码解析中逐一介绍。

​ 言归正传,从上述的流程来看,归纳起来hash表存储的最主要的过程无非以下几个:

1、计算哈希值。

2、根据哈希值检索位置。

为此,我们后续主要围绕这两个部分展开学习。

关键方法源码

hash计算

​ 在hashMap中,最重要的一个函数当属hash(Object)函数。

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

​ 首先我们主要知道h>>>16代表哈希值无符号右移动了16位,这里其实是hashMap设计的一个小技巧,无符号右移16位,就相当于h%16,通过位运算加快了操作运算的过程。

​ 但是在取模以后,hashmap还用前十六位同后十六位做了一个异或操作。这里我们举一个具体例子来看。假设咱们的哈希值是二进制4294967295(2的32次方-1):11111111111111111111111111111110。无符号右移16位后,就会变成2的16次方,也就是:00000000000000001111111111111111;两者做位异或运算可以得到如下的结果:
image-20230420205145214

​ 那么为什么hashMap要这么做呢?其实是为了将对象的哈希值尽可能的散列开。假设我们取模后直接用前16位来作为哈希值,那么相当于后16位的数据,hashMap是根本用不上的,也就意味着有一半的数据会出现冲突,因此,hashMap将后16位也纳入到考虑范围内,从而充分将数据散列开来,减少冲突。

哈希值检索

​ 在理解了哈希值是如何计算的之后,下一步自然是需要了解如何根据hash值做插入、删除。这里我们以put()方法为例,逐步揭开hashmap的神秘面纱。

​ 从put方法的源码分析,put方法主要涉及到三种情况:数组为空、数组非空但未哈希冲突、哈希冲突。我们逐一来看这三个内容在hashMap中的处理及实现。

数组为空

​ 针对于当前数组长度为空的情况,此时首先需要进行数组的扩容。通常情况下,哈希数组默认的长度是16。同时,hashmap还会保存一个叫做负载因子的变量,这个就是hashMap设计中的其中一个精华所在。

​ 负载因子的作用很简单,即长度若达到的负载因子控制的上限,那么此时就让hashMap进行扩容。默认情况下,负载因子为0.75,意味着默认情况下在数组长度达到12(16*0.75 = 12)的时候,就需要进行第一次扩容。那么问题来了,为什么是0.75呢?

​ 首先假设我们的扩容因子如果设置成1,那么意味着需要整个数组都被填充完,才会进行扩容。但是这样一来会带来一个问题,即如果多次哈希都无法命中最后一个数组节点,那么其余的节点冲突会越来越多,也就会导致红黑树的层深、链表的长度都过长,进而影响查询的效率。

image-20230529202929700

​ 而假设如果负载因子设置的过小,那么就会频繁的触发数组扩容,同时,也意味着有极大部分的数组空间是无法被使用到的,造成了资源上的浪费。基于此,hashMap开发人员们经过反复的测试、比较,最终选择了0.75作为负载因子,最大程度平衡时间效率和空间利用率。

数组非空但未哈希冲突

​ 数组为空的情况我们就介绍完了,接下来简单介绍下数组为空,但是没有出现哈希冲突的情况。在该种情况下,hashmap要处理的逻辑则相对简单,只需要在索引到具体的哈希位置,填充一个新的Node值即可。具体的源代码如下所示:

if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null); 

​ 从源码中不难看到,hashMap找寻数组下标的方式,是通过用先前计算好的哈希值同数组的长度进行位与,这里(n-1)&hash,其实就等于hash%n,也算是在源码中使用到的一个小技巧吧。

哈希冲突

​ 介绍完了上述的两种情况,还剩下最为复杂的一种情况,即出现哈希冲突的情况。哈希冲突的情况我们也可以拆分成三种情况考虑:

首节点即待替换节点: 如果首节点即待替换的节点,那么此时只需要直接替换首节点即可。

需要遍历红黑树查找插入节点:调用红黑树插入的方法进行遍历查找,这里就不展开介绍了。

需遍历列表搜索节点:思路上相对简单,即逐个遍历节点,如果相同则将该节点的值做替换,否则就创建一个节点做尾插法。需要注意的是,如果当前长度大于等于了阈值-1(即6),那么就会从链表结构转换成红黑树结构。如果后续节点数因为删除或扩容又小于8了,那么又会回退成链表的结构。

​ put方法实现的源代码如下所示,有兴趣的小伙子可以看看:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;         // 数组扩容 
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null); // 通过用长度-1 与 哈希值进行 位与运算,得到具体的下标。如果下标为空,代表此时并没有发生冲突,那么就直接新建一个存放。
    else { // 长度不为0,且存在冲突
        Node<K,V> e; K k;
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            //首先判断当前第一个冲突的node节点同待插入节点是否相同,如果相同则不做处理。
            e = p;
        else if (p instanceof TreeNode)
            // 如果是红黑树结构,那么此时按照红黑树结构进行插入
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 否则按照链表形式进行插入
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        //插入完成节点后,覆盖插入节点的值val
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

参考文献

为什么HashMap使用高16位异或低16位计算Hash值?

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

10 款最常用的Sketch在线插件!

Sketch 是一款高效、小巧的界面设计工具&#xff0c;在设计领域广受设计团队喜爱&#xff0c;帮助设计师创造了许多令人惊叹的作品。在使用 Sketch 时&#xff0c;辅助使用一些插件可以更高效地完成设计任务。Windows 也能用的「协作版 Sketch」即时设计&#xff0c;可作为网页…

《数据库应用系统实践》------ 校友会信息系统

系列文章 《数据库应用系统实践》------ 校友会信息系统 文章目录 系列文章一、需求分析1、系统背景2、 系统功能结构&#xff08;需包含功能结构框图和模块说明&#xff09;3&#xff0e;系统功能简介 二、概念模型设计1&#xff0e;基本要素&#xff08;符号介绍说明&#x…

Linux Kernel RTC驱动使用hwclock调试

hwclock hwclock的源码路径&#xff1a;sys-utils/hwclock.c 源码&#xff1a; if (opt & HWCLOCK_OPT_HCTOSYS)to_sys_clock(&rtcname, utc);else if (opt & HWCLOCK_OPT_SYSTOHC)from_sys_clock(&rtcname, utc);else if (opt & HWCLOCK_OPT_SYSTZ)set_…

Redis 的数据类型和命令帮助

文章结构 Redis 数据类型1. Redis全局命令&#xff08;跟key有关系&#xff0c;而跟value无关&#xff09;2. StringsGetting and setting StringsManaging counters 3. Lists(L)Basic commandsBlocking commands 4. Sets(S)Basic commands 5. Hashes(H)Basic commands 6. Sort…

软考A计划-试题模拟含答案解析-卷九

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例 &#x1f449;关于作者 专注于Android/Unity和各种游戏开发技巧&#xff0c;以及各种资源分享&am…

【WPF】数据绑定,资源字典

数据绑定 将数据与视图分开,创建MainViewModel .cs 作为数据源的处理 MainViewModel using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Runtime.CompilerServices; using System.Text; using System.Threading…

机器学习基础知识之多模型性能对比评价方法

文章目录 1、交叉验证t检验2、Friedman检验与Nemenyi后续检验 在进行预测或分类对比实验时&#xff0c;通常需要比较两个或两个以上的模型性能&#xff0c;因此&#xff0c;下面将介绍两个常用的多模型性能对比评价方法&#xff0c;一种是交叉验证t检验&#xff0c;该方法主要用…

如何用二极管实现不同电压的输出?

利用二极管的单向导电性可以设计出好玩、实用的电路。本文分析限幅电路和钳位电路&#xff0c;是如何用二极管来实现的。 限幅电路 如下图所示&#xff0c;当在正半周期&#xff0c;并且VIN大于等于0.7V&#xff0c;二极管正向导通。此时&#xff0c;VOUT会被钳位在0.7V上。 …

Linux网络服务:PXE高效批量网络装机

目录 一、理论 1.PXE批量网络装机概述 2.搭建 PXE 远程安装服务器 3.实现Kickstart无人值守安装 二、实验 1.搭建PXE远程安装服务器 2.安装Kickstart无人值守安装 3.安装图形化界面 三、问题 1.please complete all spokes before continuing 提示 一、理论 1.PXE批…

供应链|供应商库存服务水平对零售商需求的影响

作者&#xff1a;Nathan Craig, Nicole DeHoratius, Ananth Raman 引用&#xff1a;Craig N, DeHoratius N, Raman A. The impact of supplier inventory service level on retailer demand[J]. Manufacturing & Service Operations Management, 2016, 18(4): 461-474. 文…

【JavaSE】Java基础语法(八)

文章目录 &#x1f353;1. 类和对象&#x1f379;&#x1f379;1.1 类和对象的关系&#x1f379;&#x1f379;1.2 类的定义 &#x1f353;2. 对象内存图&#x1f379;&#x1f379;2.1 单个对象内存图&#x1f379;&#x1f379;2.2 多个对象内存图2.3 多个对象指向相同内存图…

服务器性能优化方法

文章目录 服务器性能优化方法什么是服务器并发处理能力&#xff1f;什么方法衡量服务器的并发能力&#xff1f;怎么提高服务器的并发处理能力&#xff1f;**1&#xff0c;提高CPU并发计算能力**&#xff08;1&#xff09;多进程&多线程&#xff08;2&#xff09;减少进程切…

【Unity】实现无缝地图

无缝地图是沙盒游戏的标配,可以极大提升玩家体验和沉浸感。 无缝地图的实现过程还是比较复杂的,在这里做一下实现笔记 1、地图分块: 将地图划分为较小的块,例如瓦片或区块。每个块可以是一个独立的游戏对象或地形块。确定每个块的大小和位置。你可以使用Unity的Tilemap工具…

二次元的登录界面

今天还是继续坚持写博客&#xff0c;然后今天给大家带来比较具有二次元风格的登录界面&#xff0c;也只是用html和css来写的&#xff0c;大家可以来看看&#xff01; 个人名片&#xff1a; &#x1f60a;作者简介&#xff1a;一名大一在校生&#xff0c;web前端开发专业 &…

【小程序】封装时间选择组件:用单元格van-cell和插槽slot,包括起始时间和终止时间

效果 可以选择起始时间和终止时间&#xff0c;并显示。 时间选择器放在van-cell的value插槽中。 用的库&#xff1a; https://vant-contrib.gitee.io/vant-weapp/#/home https://dayjs.fenxianglu.cn/category/ 用的组件&#xff1a;Cell单元格、DatetimePicker时间选择、Pop…

Linux——gdb调试器

目录 前言&#xff1a; 二.gdb定义及指令&#xff1a; 如何查看该exe文件是否为Debug版本?两种方法: 三.gdb调试&#xff1a; 调试指令1&#xff1a;l指令(小写L) run指令&#xff1a;运行程序&#xff0c;相当于VS中的直接运行不调试——可简化输入r break指令&#xff1…

解析云盘存储的优缺点:安全靠谱还是存在风险?

云盘是一种基于云计算技术的在线存储服务&#xff0c;用户可以通过互联网将文件上传到云端&#xff0c;并可以随时随地通过网络访问这些文件。 相较于传统的本地存储&#xff0c;云盘具有以下优势&#xff1a; 1.数据安全性更高&#xff1a;云盘使用专业的云计算技术和安全措施…

安装stable-diffusion

安装流程&#xff1a; 下载stable-diffusion源码 <https://github.com/AUTOMATIC1111/stable-diffusion-webui/releases/tag/v1.2.1>安装python <https://www.python.org/ftp/python/3.10.6/python-3.10.6-amd64.exe>添加host 打开C:\Windows\System32\drivers\etc…

01Redis单线程 VS 多线程

不同版本&#xff0c;情况不同 Redis的版本很多3.x、4.x、6.x&#xff0c;版本不同架构也是不同的&#xff0c;不限定版本问是否单线程也不太严谨。 版本3.x &#xff0c;最早版本&#xff0c;也就是大家口口相传的redis是单线程 数据结构简单避免锁的开销和上下文切换可以有…

chatgpt赋能python:Python中的乘方操作

Python中的乘方操作 作为一种流行的编程语言&#xff0c;Python内置了许多强大的数学运算工具。其中&#xff0c;乘方操作是一个非常常见的数学操作&#xff0c;它可以快速地计算一个数的任意次幂。本文将介绍Python中乘方操作的用法&#xff0c;并提供了一些相关的示例代码。…