【从零到Offer】- HashMap与HashSet

news2024/11/18 18:45:24

​ 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值?

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java并发体系-第二阶段-锁与同步-[3](仅做了解吧不好理解)

synchronized保证三大特性 synchronized保证原子性的原理 对num;增加同步代码块后&#xff0c;保证同一时间只有一个线程操作num;。就不会出现安全问题。 synchronized保证可见性的原理 synchronized保证可见性的原理&#xff0c;执行synchronized时&#xff0c;会对应lock…

下载STM32固件库

不想找的可直接输入下面的网址 https://www.st.com/en/embedded-software/stm32-standard-peripheral-libraries.html 官网下载的慢&#xff0c;阿里云链接 STM32固件 https://www.aliyundrive.com/s/e2Q3j19Bnkv 点击链接保存&#xff0c;或者复制本段内容&#xff0c;打开…

【UE】不规则物体外轮廓发光

效果 按下2键显示鼠标&#xff0c;将鼠标移动到指定的物体身上然后按下ctrl键就会使得指定物体高亮显示。 步骤 1. 创建一个材质并打开 材质域设置为后期处理 可混合位置改为“色调映射前” 添加如下节点&#xff1a; 2. 打开玩家控制的蓝图&#xff0c;添加如下节点 3. 场景…

新手装sql

windows sever 安装完了得装个sql&#xff0c;我也不会&#xff0c;按网上找到的资料&#xff0c;一步一步试吧 到这个地址去下载SQL Server https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 谁能救救我&#xff0c;全是英文&#xff0c;都看不懂&#xff0…

sentinel持久化

sentinel规则推送模式 原始模式 在sentinel源码中是不支持规则持久化的&#xff0c;一旦sentinel服务宕机&#xff0c;匹配的所有规则将彻底消失。在上篇博客中展示了sentinel的源码流程图&#xff0c;在sentinel dashboard新增一条流控规则&#xff08;或者其他规则&#xf…

Boost开发指南-1.3date_time

date_time date_time库勇敢地面对了这个挑战&#xff0c;并成功地解决了大部分问题。它是一个非常全面且灵活的日期时间库&#xff0c;基于我们日常使用的公历&#xff08;即格里高利历)&#xff0c;可以提供时间相关的各种所需功能&#xff0c;如精确定义的时间点、时间段和时…

CSS入门学习笔记+案例【一】

目录 一、CSS 是什么 二、引入方式 2.2 行内样式表 2.3 外部样式 三、 代码风格 3.1 样式格式 3.2 样式大小写 3.3 空格规范 四、 选择器 4.1 选择器的功能 4.2 选择器的种类 复合选择器小结 看完这篇博客 你将 掌握 CSS 基本语法规范和代码书写风格 掌握 CSS 选择…

ARM微架构

目录 1.流水线 2.指令流水线 3. 多核处理器​编辑 4. 工程搭建 4.1为Keil软件配置编译工具链 5.程序编写 5.1 数据处理指令 5.2 带标志位的加法ADC ADDS 5.3 跳转指令B\BL 5.4 单寄存器内存访问 5.5 批量寄存器内存访问 5.6 满减操作 1.流水线 2.指令流水线 3.…

Ansible从入门到精通【三】

大家好&#xff0c;我是早九晚十二&#xff0c;目前是做运维相关的工作。写博客是为了积累&#xff0c;希望大家一起进步&#xff01; 我的主页&#xff1a;早九晚十二 专栏名称&#xff1a;Ansible从入门到精通 立志成为ansible大佬 ♣ansible的高级指令 ansible-playbook写一…

Java 集合中 ArrayList 的扩容机制原理(面试+读源码)

在 Java 中&#xff0c;ArrayList 内部是通过一个数组来存储元素的&#xff0c;是一个数组结构的存储容器。当向一个 ArrayList 中添加元素时&#xff0c;如果当前数组已经满了&#xff0c;就需要扩容。 集合的继承关系图 一、面试回答 ( ArrayList 的扩容机制原理 ) 面试…

Vue 脚手架(打包工具)的理解 - 配置文件理解

序言 Vue 脚手架是 Vue 作为一个前端开发项目的最核心点&#xff0c;将JavaScript、CSS、HTML这几种前端自动整合&#xff0c;极大的简化了前端开发工作。 没有 Vue 脚手架&#xff0c;就没有 Vue &#xff0c;这是一定的&#xff0c;Java 语言和C语言都需要编译&#xff0c;…

【论文阅读】Analyzing group-level emotion with global alignment kernel based approach

【论文阅读】Analyzing group-level emotion with global alignment kernel based approach 摘要1.介绍与相关工作2.方法3.实验 摘要 本篇博客参考IEEE于2022年收录的论文Analyzing group-level emotion with global alignment kernel based approach&#xff0c;对其主要内容…

new一个ObjectInputStream为什么会出现java.io.EOFException

一、举例代码 package com.softeem.wolf.homework06;import java.io.*;/*** Created by 苍狼* Time on 2023-05-24*/ public class App {public static void main(String[] args) throws IOException {ObjectInputStream ois null;ObjectOutputStream oos null;ois new Obj…

功率信号源的特点和用途是什么

功率信号源是一种电子测量仪器&#xff0c;它集信号发生器与功率放大器为一体&#xff0c;具有高电压、大功率的特点。在电子实验室中&#xff0c;功率信号源可以帮助用户驱动压电陶瓷、换能器以及电磁线圈等&#xff0c;有效地解决了驱动负载和放大功率的问题。下面我们来具体…

Linux:LAMP的架构与环境配置

这里写目录标题 一、LAMP1.1 LAMP是什么1.2 安装顺序 二、编译安装Apache httpd服务2.1 关闭防火墙&#xff0c;将安装Apache所需软件包传到/opt目录下2.2 安装环境依赖包2.3 配置软件模块2.4 编译及安装2.5 优化配置文件路径2.6 添加httpd系统服务2.7 修改httpd 服务配置文件2…

MySql基础学习(2)

MySql基础学习 一、函数1.1 字符串函数1.2 数值函数1.3 日期函数1.4 流程控制语句 二、约束2.1 约束基本分类2.2 外键约束2.3 删除/更新行为 三、多表查询3.1 多表关系3.2 多表查询概述3.3 多表查询分类3.3.1 内连接3.3.2 外连接3.3.3 连接查询-自连接 3.4 联合查询-union&…

[SpringBoot]xml写mapper

创建工程[SpringBoot框架]如何使用SpringBoot框架_万物更新_的博客-CSDN博客 实现步骤: 测试: <?xml version"1.0" encoding"UTF-8" ?> <!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd…

Visual Studio || Visual Studio Code 连接 SQL Server 和 mysql

使用Visua Studio链接本地SQL Server和服务器上的mysql。 软件版本&#xff1a; Visual Studio 2022&#xff1b;Visual Studio Code 2023&#xff1b;SQL Server 2019mysql8.0 一、软件准备二、连接SQL Server2.1 使用Visual Studio 连接SQL Server2.2 使用VS Code连接SQL Ser…

【qemu】将vmdk转换为img镜像教程

qemu软件下载地址&#xff1a; 64位下载地址&#xff1a;QEMU for Windows – Installers (64 bit) 32位下载地址&#xff1a;QEMU for Windows – Installers (32 bit) 找到qemu的目录&#xff0c;然后使用cmd打开&#xff08;qemu软件没有封装exe包&#xff0c;所以只能用…

伺服系统使用S曲线

在之前文章《S形曲线规划方式汇总》 介绍过贝塞尔曲线方式&#xff0c;并且在Marlin开源工程中也有贝塞尔曲线步进系统的实现方式。本篇介绍伺服系统中基于时间分割法实现的贝塞尔S曲线。 1 贝塞尔曲线路程规划 上文中推导过贝塞尔曲线&#xff0c;本文直接用结论&#xff1a…