当前位置: 首页 > article >正文

java数据结构_Map和Set(一文理解哈希表)_9.3

目录

5. 哈希表

5.1 概念

5.2 冲突-概念

5.3 冲突-避免

5.4 冲突-避免-哈希函数的设计

5.5 冲突-避免-负载因子调节

5.6 冲突-解决

5.7 冲突-解决-闭散列

5.8 冲突-解决-开散列 / 哈希桶

5.9 冲突严重时的解决办法


5. 哈希表

5.1 概念

顺序结构以及平衡树中元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素的时候,关键码之间的多次比较是不可缺少的。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(logN),搜索的效率取决于搜索过程中元素的比较次数

理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。如果构造一种存储结构,通过某种函数,使得元素的存储位置和它的关键码之间建立一个一一映射的关系,那么在查找的时候通过该函数就可以很快找到该元素。

向该结构中:

        插入元素:根据插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放

        搜索元素:对元素的关键码进行同样的计算,把求得的函数值当作元素的存储位置,在结构

中按此位置取元素进行比较,若关键码相同,则搜索成功

该方法称为哈希(散列)方法哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(HashTable) / 散列表

例如:数据集合{1,7,6,4,5,9},哈希函数可以设置为:hash(key) = key % capacity capacitty为存储元素底层空间总的大小

用该方法进行搜索不必进行多次关键码的比较,效率会大大加快,但是,如果按照上述哈希方法,向集合中插入元素44,会出现什么问题?

5.2 冲突-概念

对于两个数据元素的关键字,数据元素的关键字并不相同,但是通过哈希函数计算出来的结果是相同的,即:不同关键字,通过哈希函数计算出相同的哈希地址,这种现象称为哈希冲突或者哈希碰撞

具有不同关键码 但是 具有相同哈希地址的数据元素称为“同义词”。

5.3 冲突-避免

首先,必须要明确的是,由于哈希表底层数组的容量往往是小于实际要存储的关键字的数量的,这就导致了一个问题,冲突是一定会发生的,我们要做的是,尽量降低冲突率

5.4 冲突-避免-哈希函数的设计

引起哈希冲突的一个原因可能是:哈希函数设计不够合理

哈希函数设计原则:

        1. 哈希函数的定义域必须包括需要存储的全部关键码,如果散列表允许有m个地址时候,哈希函数的至于必须在0到m - 1之间。

        2. 哈希函数计算出来的地址能均匀分布在整个空间中(降低冲突)

        3. 哈希函数应该比较简单

常见的哈希函数

        1.直接定址法:取关键字的某个线性函数为散列地址:Hash(Key) = A * Key + B 优点:简单、均与  缺点:需要事先直到关键字的分布情况,使用场景:适合查找比较小并且连续的情况

        2. 除留余数法: 设散列表中允许的地址数为m,取一个不大于m但最接近或者等于m的指数p作为除数,按照焊锡函数Hash(key) = key % p(p <= m),将关键码转换成哈希地址

        其他方法不作介绍

5.5 冲突-避免-负载因子调节

散列表的载荷因子定义为:α = 填入表中的元素个数 / 散列表的长度

α是散列表装满程度的标志因子。由于散列表的长度是定值,α与“填入表中的元素的个数”成正比,所以,α越大,表明填入表中元素越多,产生冲突的可能性就越大。反之,α越小,表明填入表中的元素越少,产生冲突的可能性就越小,实际上,散列表的平均查找长度载荷因子α的函数,只是不同处理冲突的方法有不同的函数。

负载因子和冲突率的关系粗略演示:

当冲突率达到一个无法忍受的高度时候,需要通过降低负载因子来变相的降低冲突率。

已知哈希表中已有的关键字个数是不可变的,那我们能调整的就只有哈希表中的数组的大小

5.6 冲突-解决

解决哈希冲突有两种常见的方法:闭散列开散列

5.7 冲突-解决-闭散列

闭散列也叫开放定址法当发生哈希冲突的时候,如果哈希表未被装满,说明哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个”空位置中。寻找空位置的方法:

        1.线性探测

比如上面的场景,现在要插入元素44,先通过哈希函数计算出哈希地址,下标为4,因此,理论上44应该插在该位置,但该位置已经放了值为4的元素,即此时发生了哈希冲突。

线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

但是采取线性探测的缺陷是,数据会堆积到一起,更会增加了发生哈希冲突的可能性

        2.二次探测

线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,线性探测中,就是载挨着往后逐个去找。二次探测为了避免该问题,找下一个空位置的方法为:H(i) = ( H(0) + i^2 )% m, 或者:H(i) = ( H(0)- i^2 )% m,H(0)是通过散列函数Hash(x)对元素的关键码key进行计算得到的位置,m是表的大小,i = 1,2,3....

研究表明:当表的长度为质数且表装载因子α不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容

因此:闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺点。

5.8 冲突-解决-开散列 / 哈希桶

开散列法又叫连地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于一个集合每一个子集合称为一个桶各个桶中的元素通过一个单链表连接起来,各链表的头结点存储在哈希表中。就相当于是数组中的每一个位置都存储一个单链表的头结点。

开散列,可以认为是把一个大集合中的搜索问题转化在小集合中做搜索了。

代码实现:

首先还是需要定义一个内部类Node,然后创建一个类型为Node[]的数组,定义usedSize

接下来是put方法,参数为要插入的数据的key,val,然后根据val值计算出对应的下标,index = key % array.length,定义cur,Node cur = array[index],然后用cur遍历该索引下的链表,检查是否存在当前的key(key是不能重复的),如果没有key,则使用头插法/尾插法插入(此处使用头插法)

添加完成之后,可以判断一下负载因子,如果负载因子大于0.75,则对散列表进行扩容

loadFactor方法:

在resize方法中,要将散列表的容量进行扩容,在扩容后,就要对整个散列再进行 !!!重新哈希!!!例如之前 4 和 14 都在散列表索引为4的位置,扩容后,14可能就不再索引为4的位置了,要进行重新哈希!!!

resize方法如下:

接下来是get方法,和put一样,得到对应的index,然后检查key值是否相同,如果找到就返回对应的val,如果没有找到就返回-1,下面是get方法的代码:

下面是全部代码:

/*** @author : zzr* @description :* @date :*/
public class HashBuck {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[] array;public int usedSize;public HashBuck() {array = new Node[10];}public void put(int key, int val) {int index = key % array.length;Node cur = array[index];//遍历该索引下的链表,看一下是否存在的当前key(不重复)while (cur != null) {if(key == cur.key) {cur.val = val;return;}cur = cur.next;}//没有这个key//头插法插入单链表中Node node = new Node(key, val);node.next = array[index];array[index] = node;usedSize++;//如果负载因子大于等于0.75 就进行扩容 --> 重新哈希if(loadFactor() >= 0.75) {resize();}}private void resize() {//在扩容的时候 要对整个哈希表都重新哈希//例如之前 4 和 14都在索引为4的位置时候,扩容后,14 可能就不在索引为4的位置了Node[] tmpArr = new Node[array.length * 2];//遍历原来的数组 将所有的元素 !!!重新哈希!!! 到新的数组当中for (int i = 0; i < array.length; i++) {Node cur = array[i];while(cur != null) {Node curNext = cur.next;//记录cur的next位置int newIndex = cur.key % tmpArr.length;//头插法:cur.next = tmpArr[newIndex];tmpArr[newIndex] = cur;//cur继续向后走cur = curNext;}}}private double loadFactor() {return usedSize * 1.0 / array.length;}public int get(int key) {int index = key % array.length;Node cur = array[index];while (cur != null) {if(key == cur.key) {return cur.val;}cur = cur.next;}return -1;}
}

测试符合预期:

这样实现的方法,在散列表中对数据进行增删改查的时间复杂度就都是O(1)了,这里可能会有疑问,因为我们的实现相当于是在数组中的每一个位置添加了一个单链表,我们在操作数据的时候,还是需要在单链表中对数据进行查找,才能进行操作,其时间复杂度不可能仅仅为O(1)。这里需要解释的是,我们在put方法中是由resize方法和loadFactor方法的,因为由负载因子的出现,是不会使得单链表的长度十分长的,单链表的长度一定会保持在一个常数级的大小,所以可以将该方法的时间复杂度记为O(1)。虽然时间复杂度降低为O(1),但是空间复杂度会上升,哈希表就是一个典型的浪费空间,节省时间的例子。

但上面的方法中,我们默认key和val都是int类型的数据,才能在计算index中直接index = key % array.length,但是在实际使用中,key,val类型通常是引用类型,则无法计算index了,那该怎么办呢?

如下:如果有一个类是Person类呢,其成员变量为String类型的id

这时候就要提出hashCode方法了,hasCode方法在JDK中底层是使用C++实现的,无法显示源码,我们只需要直到hashCode可以将引用类型转换成一个int类型并且返回。

在Person类中,我们并没有实现hashCode方法,则hashCode是其默认父类Object中的方法。我们在定义Person中id则代表一个人的身份证号,如果person1和person2的id相同,就说明他们是同一个人,但我们在直接打印person1和person2的hasCode的时候,发现其结果并不相同:

但是当我们在Person类中自动重写hashCode方法之后,再进行打印就发现其值相同了

这样就可以重写刚刚的put和get方法啦!

与之前的方法对比:

put方法:

get方法:

完整代码如下:

import java.util.Objects;/*** @author : zzr* @description :* @date :*/
class Person{String id;public Person(String id) {this.id = id;}@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;Person person = (Person) o;return Objects.equals(id, person.id);}@Overridepublic int hashCode() {return Objects.hashCode(id);}
}public class HashBuck2<K,V> {static class Node<K, V> {public K key;public V val;public Node next;public Node(K key, V val) {this.key = key;this.val = val;}}public Node<K,V>[] array;public int usedSize;public HashBuck2() {array = (Node<K, V>[]) new Node[10];}public void put(K key, V val) {int hash = key.hashCode();int index = hash % array.length;Node cur = array[index];while(cur != null) {if(key.equals(cur.key)) { //只能用equals方法而不是==cur.val = val;return;}cur = cur.next;}Node node = new Node<>(key, val);node.next = array[index];array[index] = node;usedSize++;}public V get(K key) {int hash = key.hashCode();int index = hash % array.length;Node<K,V> cur = array[index];while(cur != null) {if(key.equals(cur.key)) {return cur.val;}cur = cur.next;}return null;}}

5.9 冲突严重时的解决办法

刚才我们提到了,哈希桶其实可以看做将大集合的搜索问题转化为小集合的搜索问题,那如果冲突严重,就意味这小集合的搜索性能有时候也不佳,这个时候就可以把所谓的小集合搜索问题继续进行转化,例如:1.每个桶的背后是另一个哈希表 2.每个桶的背后是一棵搜索树

完!!!

相关文章:

java数据结构_Map和Set(一文理解哈希表)_9.3

目录 5. 哈希表 5.1 概念 5.2 冲突-概念 5.3 冲突-避免 5.4 冲突-避免-哈希函数的设计 5.5 冲突-避免-负载因子调节 5.6 冲突-解决 5.7 冲突-解决-闭散列 5.8 冲突-解决-开散列 / 哈希桶 5.9 冲突严重时的解决办法 5. 哈希表 5.1 概念 顺序结构以及平衡树中&#x…...

基于SpringBoot的“数据驱动的资产管理系统站”的设计与实现(源码+数据库+文档+PPT)

基于SpringBoot的“数据驱动的资产管理系统站”的设计与实现&#xff08;源码数据库文档PPT) 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;SpringBoot 工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 系统功能结构图 局部E-R图 系统登录界…...

excel 斜向拆分单元格

右键-合并单元格 右键-设置单元格格式-边框 在设置好分割线后&#xff0c;你可以开始输入文字。 需要注意的是&#xff0c;文字并不会自动分成上下两行。 为了达到你期望的效果&#xff0c;你可以通过 同过左对齐、上对齐 空格键或使用【AltEnter】组合键来调整单元格中内容的…...

深入理解推理语言模型(RLM)

大语言模型从通用走向推理&#xff0c;万字长文解析推理语言模型&#xff0c;建议收藏后食用。 本文基于苏黎世联邦理工学院的论文《Reasoning Language Models: A Blueprint》进行整理&#xff0c;你将会了解到&#xff1a; 1、RLM的演进与基础&#xff1a;RLM融合LLM的知识广…...

2025年具有百度特色的软件测试面试题

百度业务场景 如何测试一个高并发的搜索系统(如百度搜索)?如何测试一个在线地图服务(如百度地图)?如何测试一个大型推荐系统(如百度推荐)的性能?百度技术栈 你对百度的 PaddlePaddle 框架有了解吗?如何测试基于 PaddlePaddle 的服务?如何测试百度云的 API 服务?你对…...

HOW - 在Windows浏览器中模拟MacOS的滚动条

目录 一、原生 CSS 代码实现模拟 macOS 滚动条额外优化应用到某个特定容器 二、使用第三方工具/扩展 如果你想让 Windows 里的滚动条 模拟 macOS 的效果&#xff08;细窄、圆角、隐藏默认轨道&#xff09;。 可以使用以下几种方案&#xff1a; 一、原生 CSS 代码实现 模拟 m…...

Lua | 每日一练 (5)

&#x1f4a2;欢迎来到张胤尘的技术站 &#x1f4a5;技术如江河&#xff0c;汇聚众志成。代码似星辰&#xff0c;照亮行征程。开源精神长&#xff0c;传承永不忘。携手共前行&#xff0c;未来更辉煌&#x1f4a5; 文章目录 Lua | 每日一练 (5)题目参考答案浅拷贝深拷贝使用场景…...

C# Unity 唐老狮 No.5 模拟面试题

本文章不作任何商业用途 仅作学习与交流 安利唐老狮与其他老师合作的网站,内有大量免费资源和优质付费资源,我入门就是看唐老师的课程 打好坚实的基础非常非常重要: 全部 - 游习堂 - 唐老狮创立的游戏开发在线学习平台 - Powered By EduSoho 如果你发现了文章内特殊的字体格式,…...

云原生事件驱动架构:构建实时响应的数字化神经系统

引言&#xff1a;重塑企业实时决策能力 Uber实现事件驱动架构升级后&#xff0c;实时供需匹配延迟降至8ms&#xff0c;动态定价策略响应速度提升1200倍。Netflix通过事件流处理实现个性化推荐&#xff0c;用户点击率提高34%&#xff0c;事件处理吞吐量达2000万/秒。Confluent基…...

Metasploit multi/handler 模块高级选项解析

multi/handler 是 Metasploit 框架中至关重要的模块&#xff0c;主要用于监听目标机的连接并处理来自目标的反向 shell 或会话。它可以灵活地适应不同渗透测试场景&#xff0c;提供高度的自定义选项以优化监听器的行为。 在 Metasploit msf6 框架中&#xff0c;当使用 exploit…...

WPF高级 | WPF 应用程序部署与发布:确保顺利交付到用户手中

WPF高级 | WPF 应用程序部署与发布&#xff1a;确保顺利交付到用户手中 一、前言二、部署与发布基础概念2.1 部署的定义与目的2.2 发布的方式与渠道2.3 部署与发布的关键要素 三、WPF 应用程序打包3.1 使用 Visual Studio 自带的打包工具3.2 使用第三方打包工具 四、发布到不同…...

Spring MVC 程序开发(1)

目录 1、什么是 SpringMVC2、返回数据2.1、返回 JSON 对象2.2、请求转发2.3、请求重定向2.4、自定义返回的内容 1、什么是 SpringMVC 1、Tomcat 和 Servlet 分别是什么&#xff1f;有什么关系&#xff1f; Servlet 是 java 官方定义的 web 开发的标准规范&#xff1b;Tomcat 是…...

JavaWeb后端基础(6)

主键返回 例子&#xff1a; /** * 新增员工数据 */ Options(useGeneratedKeys true, keyProperty "id") Insert("insert into emp(username, name, gender, phone, job, salary, image, entry_date, dept_id, create_time, update_time) " "value…...

C# Unity 唐老狮 No.4 模拟面试题

本文章不作任何商业用途 仅作学习与交流 安利唐老狮与其他老师合作的网站,内有大量免费资源和优质付费资源,我入门就是看唐老师的课程 打好坚实的基础非常非常重要: 全部 - 游习堂 - 唐老狮创立的游戏开发在线学习平台 - Powered By EduSoho 如果你发现了文章内特殊的字体格式,…...

集群、分布式与微服务架构 区别

集群、分布式与微服务架构&#xff1a;概念解析与核心差异 在构建现代软件系统时&#xff0c;集群架构、分布式系统和微服务架构是三种常见的技术方案。它们常被混淆&#xff0c;但各自解决的问题、设计理念和应用场景截然不同。本文将从基础概念出发&#xff0c;深入分析三者…...

Protocol Buffers在MCU上的nanopb介绍及使用详解

在嵌入式系统和资源受限的环境中&#xff0c;传统的Protocol Buffers 可能显得过于庞大。因此&#xff0c;nanopb 应运而生&#xff0c;它是一个轻量级的 Protocol Buffers 生成器&#xff0c;专为嵌入式系统设计c语言设计。本文将介绍如何安装和使用 nanopb&#xff0c;以及通…...

【Elasticsearch】自定义内置的索引生命周期管理(ILM)策略。

以下是对 Elasticsearch 官方教程《Customize built-in ILM policies》的详细解读&#xff0c;结合原文内容&#xff0c;帮助您更好地理解如何自定义内置的索引生命周期管理&#xff08;ILM&#xff09;策略。 --- Elasticsearch 教程&#xff1a;自定义内置 ILM 策略 1.背景…...

测试工程师Ai应用实战指南简例prompt

以下是一个真实具体的案例,展示测试工程师如何在不同阶段结合DeepSeek提升效率。案例基于电商平台"订单超时自动关闭"功能测试: 案例背景 项目名称:电商平台订单系统V2.3 测试目标:验证"用户下单后30分钟未支付,订单自动关闭并释放库存"功能 技术栈:…...

(十 二)趣学设计模式 之 享元模式!

目录 一、 啥是享元模式&#xff1f;二、 为什么要用享元模式&#xff1f;三、 享元模式的实现方式四、 享元模式的优缺点五、 享元模式的应用场景六、 总结 &#x1f31f;我的其他文章也讲解的比较有趣&#x1f601;&#xff0c;如果喜欢博主的讲解方式&#xff0c;可以多多支…...

Trae:国内首款AI原生IDE,编程效率大提升

今年一月&#xff0c;在新闻上看到字节跳动面向海外市场推出了一款名为Trae的AI集成开发环境&#xff08;IDE&#xff09;。起初&#xff0c;我并未给予过多关注&#xff0c;因为市面上已有不少IDE集成了AI插件&#xff0c;功能也非常全面&#xff0c;而字节跳动自家的MarsCode…...

深入解析 Vue Router 的 beforeEach:功能、用法与实践指南

什么是 beforeEach&#xff1f;基本语法与参数解析next() 的 4 种调用方式常见使用场景与代码示例动态路由加载的实践技巧常见陷阱与避坑指南总结 1. 什么是 beforeEach&#xff1f; beforeEach 是 Vue Router 提供的 全局前置守卫&#xff08;Global Before Guards&#xff0…...

RocketMQ定时/延时消息实现机制

RocketMQ 的延迟消息是其核心特性之一&#xff0c;允许消息在指定延迟时间后才被消费者消费。 定时消息生命周期 一、延迟消息的核心机制 RocketMQ&#xff08;5.0之前&#xff09; 不支持任意时间精度的延迟&#xff0c;而是通过预定义的 延迟级别&#xff08;Delay Level&a…...

基于SpringBoot的校园二手交易平台(源码+论文+部署教程)

运行环境 校园二手交易平台运行环境如下&#xff1a; • 前端&#xff1a;Vue • 后端&#xff1a;Java • IDE工具&#xff1a;IntelliJ IDEA&#xff08;可自行更换&#xff09; • 技术栈&#xff1a;SpringBoot Vue MySQL 主要功能 校园二手交易平台主要包含前台和…...

如何快速写出国内外现状的内容并且引用对应的参考文献(近三年的论文)

解决方法: 1.首先从知网或者谷歌学术中搜索相关关键字的论文根据时间排列(最新的在前面)。然后多选选中自己想要引用的论文(一般近三年的论文要占2/3),然后导出参考文献 [19] Lu L, Jin P, Karniadakis G E. DeepONet: Learning nonlinear operators for identifying dif…...

SQL的select语句完整的执行顺序

SQL的SELECT语句的执行顺序可以用"做菜流程"来类比理解。虽然我们写SQL时按SELECT…FROM…WHERE…顺序写&#xff0c;但数据库执行顺序完全不同。以下是通俗易懂的讲解&#xff08;附流程图和示例&#xff09;&#xff1a; &#x1f527; 执行顺序流程图&#xff1a…...

开源操作系统纷争:CentOS停服后的新战场

开源操作系统纷争&#xff1a;CentOS停服后的新战场 引言 2020年12月&#xff0c;Red Hat宣布将停止维护CentOS Linux&#xff0c;转而专注于CentOS Stream。这一决策在开源社区掀起轩然大波&#xff0c;尤其是那些依赖CentOS作为生产环境操作系统的企业和开发者们&#xff0…...

【知识】torchrun 与 torch.multiprocessing.spawn 的对比

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhagn.cn] 如果本文帮助到了你&#xff0c;欢迎[点赞、收藏、关注]哦~ 来自ChatGPT、DeepSeek 有点干&#xff0c;可仅做了解。 torchrun 和 torch.multiprocessing.spawn 都是在 PyTorch 中用于并行化和分布式训练的工具&a…...

利用 LangChain 和一个大语言模型(LLM)构建一个链条,自动从用户输入的问题中提取相关的 SQL 表信息,再生成对应的 SQL 查询

示例代码&#xff1a; from langchain_core.runnables import RunnablePassthrough from langchain.chains import create_sql_query_chain from operator import itemgetter from langchain.chains.openai_tools import create_extraction_chain_pydantic# 系统消息&#xff…...

力扣hot 100之矩阵四题解法总结

本期总结hot100 中二维矩阵的题&#xff0c;时空复杂度就不分析了 1.矩阵置零 原地标记&#xff0c;用第一行和第一列作为当前行列是否为0的标记&#xff0c;同时用两个标签分别记录0行、0列的标记空间中原本是否有0 class Solution:def setZeroes(self, matrix: List[List[…...

使用python运行网格世界环境下 TD算法

一、概述 本代码实现了在网格世界环境中使用 TD (0)&#xff08;Temporal Difference (0)&#xff09;算法进行策略评估&#xff0c;并对评估结果进行可视化展示。通过模拟智能体在网格世界中的移动&#xff0c;不断更新状态值函数&#xff0c;最终得到每个状态的价值估计。 二…...