Java-数据结构-(HashMap HashSet)

一、Tree和Hash的区别

在上一篇文章中,我们讲到了"TreeMap""TreeSet",但当我们刷题的时候却会发现,实际应用Map和Set时,却常常都只会用"HashMap"和"HashSet",这是为什么呢?

① 效率不同

📚"TreeMap"和"TreeSet"

都是基于"红黑树"实现的,这种方法的实现就导致了无法直接查询到存储进去的数据,而是需要进去不断的查找,即便已经有了非常好的优化,树的遍历效率也只能达到O(logn)

📚"HashMap"和"HashSet"

是基于哈希表实现的,一般使用HashMap和HashSet进行存入和查找时,时间复杂度都能达到O(1)

而这种效率是远远高于O(logn)的,并且平时刷题时测试用例中都有大量难缠的数据,所以平时"Hash"的应用场景是多于"Tree"的。

② 适用场景不同

📚 "TreeMap"和"TreeSet"

这种数据结构会对存入的数据自动进行排序,适用于数据规模不太大或者需要有序数据或范围查询,使用TreeMap是一个很好的选择。

这里使用HashMap是没办法使用该方法的,因为HashMap并不会对数据进行排序

📚"HashMap"和"HashSet"

相对的,哈希表能做到快速存入和查询,肯定也有对应的缺点,那就是"不会对存入的数据进行自动排序"

但是实际中,对Map和Set的使用还是以"存入和查询"居多,所以"HashMap"和"HashSet"的使用还是会更多的。

二、哈希表

① 什么是哈希表?

通过上面我们能了解到,哈希表的存入和查询速率都是O(1)。

O(1)是什么概念?就是比较的次数非常少,甚至有时候可以忽略不计。

那么让我们回顾一下,之前学习的"排序算法"中,就有这么一种比较次数非常少的排序—"桶排序"
在哈希表的实现中,也使用了类似"桶排序"中的一种思想—"分桶的核心思想"

📕 分桶:哈希表通过某种规则将数据分散到多个容器(桶)中。

📕 映射规则:哈希表通过哈希函数映射键到桶。

哈希表除了使用了这种类似桶排序的分桶思想,剩下的操作比较类似于"计数排序"

📕 插入元素:根据待插入元素的关键码,计算出元素的存储位置。

📕 搜索元素:同样对关键码进行运算,并查找该位置,若关键码相同则搜索成功。

而其中对关键码进行操作就是通过"哈希函数"来进行转换的。

比如此时我们将哈希函数设置为:int index = key % elem.length;
那么对于元素的处理就会像这样

在这个存储过程中我们可以发现,并没有元素进行比较。这就是一种最理想的状态。但让我们再想想,如果再往表中插入14呢?24呢?34、44呢?

② 哈希冲突的概念

上面我们提到,如果在表中继续插入元素,如"14","24","34"等。它们经过哈希函数后,得到的对应位置与先前存入的"4"是一样的。

而这也就是"不理想的情况",因为遇到这种情况,我们的哈希表就需要进行"元素之间的比较"了,这种情况也被称为"哈希冲突"

③ 哈希冲突的避免

我们要知道,使用哈希表进行数据的存储时,造成"哈希冲突"是必然的

因为理想状态下我们通过哈希函数计算每个数据的对应键值并将数据存入哈希表中,但这也就意味着肯定会有些数据会计算出相同的键值并且哈希表的空间也是有限的(未扩容之前),当存入的数据达到一定的限度,则会出现"经常发生哈希冲突"的情况。

而为了避免这种情况发生,我们能做到的就是尽量设计一个合理的哈希函数。

哈希函数设计原则

📕 哈希函数的定义域必须包括需要存储的全部关键码,如果表中允许有n个地址,则哈希函数的值域必须在0到n-1之间

📕 哈希函数计算的值最好能均匀分布在整个空间中

④ 常见的哈希函数

📚 直接定制法:Hash(Key) = A * Key + B

优点:简单、均匀
缺点:需要事先知道关键字的分布情况使用场景

📚 除留余数法:Hash(key) = key % p(p<=m)

⑤ 负载因子调节

上面我们提到过

哈希表的空间也是有限的(未扩容之前),当存入的数据达到一定的限度,则会出现"经常发生哈希冲突"的情况。

这种情况会大大降低我们的哈希表的存取效率,而为了避免这种情况发生,我们就需要在每次存入数据时,计算一下此时哈希表的负载因子,如果此时的负载因子超过了我们希望的限定值,那么此时我们将对哈希表进行扩容。

1. 负载因子的定义

负载因子表示哈希表中已存储元素数量与当前总容量的比值

如:此时哈希表容量为 10 ,已存入 7 个元素,则此时的负载因子为 0.7 。

2. 负载因子的作用

📕 衡量哈希表填充程度:

负载因子越高,哈希表填充越满,发生哈希冲突的概率越大。

📕 触发扩容的阈值:

当负载因子超过预设值(java中默认为0.75,后续我们模拟实现哈希表也会采取这个阈值)时,哈希表自动扩容,以降低冲突概率。

📕 平衡时间与空间开销:

低负载因子:冲突少,操作效率高,但内存利用率低。
高负载因子:内存利用率高,但冲突频繁,操作效率下降。

⑥ 冲突的解决方案(开放寻址法)

1. 线性探测

📕 规则

若当前的桶已经被占用,则顺序查询下一个桶(如 index = (index + 1) % size),直到找到空桶。

📕 优点:实现简单,空间利用率高,缓存性能好(连续存储)。

📕 缺点:产生聚集现象(大量连续占用桶),导致查找效率下降。

📕 适用场景:负载因子较低时。

2. 二次探测

📕 规则:

使用第二个哈希函数计算下一个空位置(如:index1 = (index0 + i ^ 2) % m)

📕 优点:冲突分布更均匀,减少聚集。

📕 缺点:装载因子不能太大,否则性能会急剧下降,容易发生二次聚集。

📕 适用场景:对性能要求高的场景。

⑦ 冲突的解决方案(链地址法)

又叫做"开散列法",我们需要对传进的数据关键码通过散列函数计算出散列地址,将具有相同地址的关键码放入同一个子集合中,每一个子集合都是一个桶,每个桶中的元素都通过一个单链表进行连接,然后将每个链表的结点都存储在哈希表中。

三、哈希表的模拟实现

 ① 基本框架

在这里我们采用"开散列法"

所以我们需要用到链表结构,因此我们需要在基本框架中实现一个结点类

同时,我们还需要设定一个触发扩容的阈值(负载因子),上面我们提到java中默认为0.75,所以我们这里也使用0.75作为触发扩容的阈值。

📖 代码示例

public class HashBucket {
    public static class Node {
        public int key;
        public int val;
        public Node next;
        public Node(int key, int val) {
            this.key = key;
            this.val = val;
        }
   }
   //初始哈希表
    public Node[] elem = new Node[10];
    public int usedSize;
    //负载因子
    public static final double LOAD_FACTOR = 0.75;
}

② 插入元素

实现插入元素,我们需要考虑很多种情况,比如:如何避免哈希冲突,将结点插入链表的何处,在何时计算负载因子,如何进行扩容等。

这里我们一个个的进行讲解

📕 如何避免哈希冲突

我们采用"开散列法",首先通过哈希函数寻找对应链表的index

int index = key % elem.length;

然后判断当前index指向的链表中是否含有key,如果存在key,则修改结点的值为新的val。

📕 将结点插入链表何处

如果不存在key,则将新节点插入链表(尾插和头插都可以,这里我们采取头插法)

📕 在何时计算负载因子

当新元素加入后,计算此时的负载因子,如果超过阈值则扩容

📖 代码示例

    //新增元素
    public void put(int key,int val){
        //1.通过哈希函数,找到对应链表的index
        int index = key % elem.length;
        //2.判断当前的链表是否有key
        Node cur = elem[index];
        while(cur != null){
            //3.找到key,修改改结点的val
            if(cur.key == key){
                cur.val = val;
                return;
            }
            cur = cur.next;
        }
        //4.如果没有key,则将新结点插入链表(这里采取头插法)
        Node newCur = new Node(key,val);
        newCur.next = elem[index];
        elem[index] = newCur;
        usedSize++;
        //5.计算当前的负载因子,如果超过则扩容
        if(getLoadFactor() >= LOAD_FACTOR){
            upsize();
        }
   }
   //计算当前负载因子
    public double getLoadFactor(){
        return usedSize * 1.0 / elem.length;
    }

③ 哈希表扩容

当为哈希表进行扩容时,并不是简单的将数组大小扩大一倍即可,因为可能会发生如下情况:

当扩容之后,其实14应该放在新的空间14内,而不是还处在4的位置,所以哈希表的扩容其实是一个(再次哈希)的过程,这个过程的时间复杂度是O(n)的,需要我们遍历所有元素,重新创建一个哈希表:

📖 代码示例

    //哈希表扩容(再次哈希)
    public void upsize(){
        //1.创建新的哈希表
        Node[] newElem = new Node[elem.length * 2];
        for(int i = 0;i < elem.length;i++){
            Node cur = elem[i];
            while(cur != null){
                Node curN = cur.next;
                int index = cur.key % newElem.length;
                cur.next = newElem[index];
                newElem[index] = cur;
                cur = curN;
            }
        }
        elem = newElem;
    }

④ 获取元素

这个就很简单了,经过之前我们对各种数据结构的学习,对于大家来说肯定也是不必多说,只需要计算出对应链表的index并遍历链表求目标元素即可。

📖 代码示例

    //通过key获取元素
    public int get(int key) {
        //找到key的对应下标
        int index = key % elem.length;
        Node cur = elem[index];
        //查找链表
        while (cur != null) {
            if (cur.key == key) {
                return cur.val;
            }
            cur = cur.next;
        }
        return -1;
    }

⑤ 测试

我们设定初始大小为10,扩容阈值为0.75,此时我们向哈希表中存入八个元素:

当存储到14时,14存在index = 4的链表中

当存入第八个元素,也就是19的时候,就会触发扩容,此时我们的14应该从index = 4的链表,移动到新的index = 14的位置上:

四、自定义类使用哈希表

📖 People:

class People {
    public int id;
    public String name;
    public People(int id,String name) {
        this.id = id;
        this.name = name;
    }
}

有些时候,我们希望对自定义的一些类也是使用哈希表进行存储,但是有时会发生这样的情况

明明姓名和id都相同,但却找不到对应的人,这是为什么呢?

这就取决于java内部哈希函数如何计算了

📕 与 equals() 的一致性:若两个对象相等,则它们的哈希值必须相同。

📕 基于内存地址:默认返回对象内部地址的整数表示

📕 问题:不同对象即使内容相同,哈希值也不同

是的,所以找不到对应的对象是因为地址不同,那么如果想查找到对应的对象,我们就需要在自定义类中重写 hashCode() 和 equals() 方法

那么这篇关于哈希表(HashMap,HashSet)的文章到这里就结束啦,作者能力有限,如果有哪里说的不够清楚或者不够准确,还请各位在评论区多多指出,我也会虚心学习的,我们下次再见啦

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

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

相关文章

DeepSeek在linux下的安装部署与应用测试

结合上一篇文章&#xff0c;本篇文章主要讲述在Redhat linux环境下如何部署和使用DeepSeek大模型&#xff0c;主要包括ollama的安装配置、大模型的加载和应用测试。关于Open WebUI在docker的安装部署&#xff0c;Open WebUI官网也提供了完整的docker部署说明&#xff0c;大家可…

ShenNiusModularity项目源码学习(9:项目结构)

ShenNiusModularity源码主要有11个project&#xff08;其实还有officialweb、test两个文件夹&#xff0c;大致有4、5个project&#xff0c;但看着跟主要项目代码没太大关系&#xff0c;暂时不管&#xff09;&#xff0c;这11个project的依赖关系如下图所示&#xff0c;其中最下…

用deepseek学大模型08-cnn残差网络

残差网络 参考&#xff1a;https://blog.csdn.net/2301_80750681/article/details/142882802 以下是使用PyTorch实现的三层残差网络示例&#xff0c;包含三个残差块和完整的网络结构&#xff1a; import torch import torch.nn as nnclass BasicBlock(nn.Module):expansion…

【C++】36.C++IO流

文章目录 1. C语言的输入与输出2. 流是什么3. CIO流3.1 C标准IO流3.2 C文件IO流 4. stringstream的简单介绍 1. C语言的输入与输出 C语言中我们用到的最频繁的输入输出方式就是scanf ()与printf()。 scanf(): 从标准输入设备(键盘)读取数据&#xff0c;并将值存放在变量中。pri…

#渗透测试#批量漏洞挖掘#Apache Log4j反序列化命令执行漏洞

免责声明 本教程仅为合法的教学目的而准备,严禁用于任何形式的违法犯罪活动及其他商业行为,在使用本教程前,您应确保该行为符合当地的法律法规,继续阅读即表示您需自行承担所有操作的后果,如有异议,请立即停止本文章读。 目录 Apache Log4j反序列化命令执行漏洞 一、…

JCRQ1河马算法+消融实验!HO-CNN-LSTM-Attention系列四模型多变量时序预测

JCRQ1河马算法消融实验&#xff01;HO-CNN-LSTM-Attention系列四模型多变量时序预测 目录 JCRQ1河马算法消融实验&#xff01;HO-CNN-LSTM-Attention系列四模型多变量时序预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 基于HO-CNN-LSTM-Attention、CNN-LSTM-Attent…

[STM32 - 野火] - - - 固件库学习笔记 - - - 十五.设置FLASH的读写保护及解除

一、选项字节与读写保护 1.1 为什么要设置读写保护 防止内部FLASH中的程序被非法读取。 在实际发布的产品中&#xff0c;STM32芯片的内部FLASH存储了控制程序。然而&#xff0c;如果不对内部FLASH采取任何保护措施&#xff0c;用户可以使用下载器直接读取其内容&#xff0c;并…

【算法与数据结构】字典树(Trie)详解

目录 一&#xff0c;字典树的定义 二&#xff0c;字典树的代码实现 完整代码详细注释&#xff1a; 测试用例测试结果&#xff1a; 三&#xff0c;处理其他字符 四&#xff0c;内存优化与扩展 1. 内存优化 2. 扩展功能 五&#xff0c;扩展功能支持通配符匹配 六&…

MySQL 之存储引擎(MySQL Storage Engine)

MySQL 之存储引擎 常见存储引擎及其特点 ‌InnoDB‌&#xff1a; ‌特点‌&#xff1a;支持事务处理、行级锁定、外键约束&#xff0c;使用聚簇索引&#xff0c;适合高并发读写和事务处理的场景‌。‌适用场景‌&#xff1a;需要高可靠性、高并发读写和事务处理的场景‌。 ‌M…

CXL ALMP(ARB/MUX Link Management Packet)理解

前言&#xff1a; ALMP&#xff08;ARB/MUX Link Management Packet&#xff09; 是CXL协议中由ARB/MUX层生成和处理的专用管理报文&#xff0c;用于协调链路电源状态切换&#xff08;如L0s/L1&#xff09;和虚拟链路状态机&#xff08;vLSM&#xff09;同步。以下是其核心特性…

002 SpringCloudAlibaba整合 - Feign远程调用、Loadbalancer负载均衡

前文地址&#xff1a; 001 SpringCloudAlibaba整合 - Nacos注册配置中心、Sentinel流控、Zipkin链路追踪、Admin监控 文章目录 8.Feign远程调用、loadbalancer负载均衡整合1.OpenFeign整合1.引入依赖2.启动类添加EnableFeignClients注解3.yml配置4.日志配置5.远程调用测试6.服务…

计算机网络(3)TCP格式/连接

1、TCP三大特点&#xff1a;面向连接、可靠、基于字节流 2、如何唯一确定一个TCP连接&#xff1f;TCP四元组&#xff1a;源地址、源端口、目的地址、目的端口 源地址和目标地址的字段(32 位)是在 IP 头部中&#xff0c;作用是通过 IP 协议发送报文给对方主机源端口和目标端口…

vscode远程报错:Remote host key has changed,...

重装了Ubuntu系统之后&#xff0c;由20.04改为22.04&#xff0c;再用vscode远程&#xff0c;就出现了以上报错。 亲测有效的办法 gedit ~/.ssh/known_hosts 打开这个配置文件 删掉与之匹配的那一行&#xff0c;不知道删哪一行的话&#xff0c;就打开第一行这个 /.ssh/confi…

无符号整数和带符号整数的相互转换

无符号字符数x转换为带符号字符数时&#xff0c;当时&#xff0c;转换后仍然为x&#xff1b;当时&#xff0c;转换后变为。 带符号字符数y转换为无符号字符数时&#xff0c;当时&#xff0c;转换后变为&#xff1b;当时&#xff0c;转换后仍然为y。 无符号整数和带符号整数的…

浏览器报错:无法访问此网站 无法找到xxx.xxx.net的DNS地址。正在诊断该问题。尝试运行Windows网络诊断。DNS_PROBE_STARTED

&#x1f91f;致敬读者 &#x1f7e9;感谢阅读&#x1f7e6;希望我的文章能帮到您&#x1f7ea;如有兴趣可点关注了解更多内容 &#x1f4d8;博主信息 点击标题&#x1f446;有惊喜 &#x1f4c3;文章前言 &#x1f537;文章均为学习和工作中整理的笔记&#xff0c;分享记录…

2025-02-18 学习记录--C/C++-PTA 7-25 念数字

一、题目描述 ⭐️ 二、代码&#xff08;C语言&#xff09;⭐️ /*** 输入一个整数&#xff0c;输出每个数字对应的拼音。当整数为负数时&#xff0c;先输出fu字。*/#include <stdio.h>// 输出 正数 中 各位数 对应的 拼音 void getLetter(int num) {// 10个数字&#x…

VirtualBox 中使用 桥接网卡 并设置 MAC 地址

在 VirtualBox 中使用 桥接网卡 并设置 MAC 地址&#xff0c;可以按照以下步骤操作&#xff1a; 步骤 1&#xff1a;设置桥接网卡 打开 VirtualBox&#xff0c;选择你的虚拟机&#xff0c;点击 “设置” (Settings)。进入 “网络” (Network) 选项卡。在 “适配器 1” (Adapt…

Fiddler笔记

文章目录 一、与F12对比二、核心作用三、原理四、配置1.Rules:2.配置证书抓取https包3.设置过滤器4、抓取App包 五、模拟弱网测试六、调试1.线上调试2.断点调试 七、理论1.四要素2.如何定位前后端bug 注 一、与F12对比 相同点&#xff1a; 都可以对http和https请求进行抓包分析…

【数据结构初阶第十节】队列(详解+附源码)

好久不见。。。别不开心了&#xff0c;听听喜欢的歌吧 必须有为成功付出代价的决心&#xff0c;然后想办法付出这个代价。云边有个稻草人-CSDN博客 目录 一、概念和结构 二、队列的实现 Queue.h Queue.c test.c Relaxing Time&#xff01; ————————————《有没…

idea无法联网,离线安装插件

插件地址&#xff1a;https://plugins.jetbrains.com/ JetBrains Marketplace 如果无法进入&#xff0c;可以试试 配置hosts 3.163.125.103 plugins.jetbrains.com ip 变了&#xff0c;可以查询个最新的&#xff1a; https://tool.chinaz.com/speedtest/plugins.jetbrai…