[数据结构]哈希表

目录

1、哈希表

1.1、概念

1.2、冲突

2、哈希函数设计

3、负载因子调节

4、闭散列

5、开散列/哈希桶(重点掌握)

6、实现哈希桶

6.1、put方法

6.2、HashMap的扩容机制

6.3、get方法

7、HashMap

8、HashSet

8.1、哈希表性能分析

9、hashcode

9.1、hashCode和equals的区别

10、实现哈希桶


1、哈希表

1.1、概念

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。

顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(logN),搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。


当向该结构中:
插入元素:根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
搜索元素:对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
该方式即为哈希(散列)方法

哈希方法中使用的转换函数称为哈希(散列)函数

构造出来的结构称为哈希表(HashTable)(或者称散列表)

按照上述哈希方式,向集合中插入元素14,会出现冲突的现象,这种现象称为哈希冲突

1.2、冲突

概念:

哈希冲突/哈希碰撞:不同的关键字KEY 通过相同的哈希函数计算出相同的哈希地址


避免:

由于哈希表底层数组的容量往往是小于实际要存储的关键字的数量的,这就导致一 个问题,冲突的发生是必然的,但我们能做的应该是尽量的降低冲突率


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

2、哈希函数设计

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

  • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
  • 哈希函数计算出来的地址能均匀分布在整个空间中
  • 哈希函数应该比较简单

常见哈希函数
1. 直接定址法--(常用)
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B

优点:简单、均匀

缺点:需要事先知道关键字的分布情况

使用场景:适合查找比较小且连续的情况

        例如:key : 91

        int index = key - minVal        minVal是一组数据中最小的数据
2. 除留余数法--(常用)
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址
3. 平方取中法--(了解)
假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址; 再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址 平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况
4. 折叠法--(了解)

5. 随机数法--(了解)

6. 数学分析法--(了解)

注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突

3、负载因子调节

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

α 是散列表装满程度的标志因子。由于表长是定值,α 与“填入表中的元素个数”成正比,所以,α 越大,表明填入表中的元素越多,产生冲突的可能性就越大;反之,α 越小,表明填入表中的元素越少,产生冲突的可能性就越小。

Java的系统库限制了负载因子为0.75,超过此值将使用扩容的方式重构散列表,将负载因子控制在 0.75 内

所以当冲突率达到一个无法忍受的程度时,我们需要通过降低负载因子来变相的降低冲突率。 已知哈希表中已有的关键字个数是不可变的,那我们能调整的就只有哈希表中的数组的大小

4、闭散列

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

1. 线性探测

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

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

插入:

  • 通过哈希函数获取待插入元素在哈希表中的位置
  • 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素
  • 采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素

2. 二次探测

二次探测法指采用前后跳跃式探测的方法,发生冲突时,向后1位探测,向前1位探测,向后2^2 位探测,向前2^2 位探测……以跳跃式探测,避免堆积。

二次探测的增量序列为di =1^2 , -1^2 , 2^2 , -2^2 , …, k^2 , -k^2(k ≤m /2),对应哈希函数中的 i = 1,2,3,4, …

研究表明:当表的长度为质数且表装载因子 α 不超过0.5 时,新的表项一定能够插入,而且任何一个位置都不会被探查两次

因此只要表中有一半的空位置,就不会存在表满的问题。

在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容。

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

5、开散列/哈希桶(重点掌握)

开散列法又叫链地址法(开链法)

核心思想:将所有元素存储在一个链表中,每个链表的头指针存储在哈希表的相应槽位中。当发生哈希冲突时,新的元素会被添加到对应链表的末尾,哈希表的每个元素就是各链表的头节点

插入链表的时,JDK1.7及以前采用的是头插法,1.8 开始采用尾插法

从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。

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

如果冲突严重,就意味着小集合的搜索性能其实也时不佳的,这个时候我们就可以将这个所谓的小集合搜索问题继续进行转化,例如:
1. 每个桶的背后是另一个哈希表
2. 每个桶的背后改变成一棵搜索树,比如说变成红黑树


Java的HashMap就是就是采用开散列的方式来解决哈希冲突的

数组+链表+红黑树(当数组长度 >= 64 && 链表长度 >=8 以后,就会把链表变成红黑树)

6、实现哈希桶

6.1、put方法

面试问题:

1.上图的这种扩容方式可不可行

答:不可以,因为数组长度变了,原来位置冲突的元素可能要放到新的数组的其他位置去

2. 扩容需要注意的是什么?

答:HashMap 扩容时,原来 HashMap 中数组下的每一个结点都要重新计算它在新数组中的位置(重新哈希)

6.2、HashMap的扩容机制

为什么需要扩容?

        随着哈希表中存储的元素不断增加,哈希冲突的概率也会越来越高,这会导致哈希表的性能下降,查找、插入和删除操作的时间复杂度可能会接近O(n)。为了保证哈希表的性能,当哈希表中的元素数量达到一定阈值时,就需要对哈希表进行扩容。


扩容的触发条件:

在 HashMap 中,有两个重要的参数:容量(capacity)和负载因子(loadFactor)。容量指的是哈希表中数组的长度,负载因子是一个介于 0 到 1 之间的浮点数,默认值为 0.75。当哈希表中的负载因子大小超过 0.75 时,就会触发扩容操作。


扩容的过程:

  • 创建新数组:将原数组的容量扩大为原来的 2 倍。
  • 重新哈希:遍历原数组中的每个元素,重新计算它们在新数组中的位置,并将元素插入到新数组中。

6.3、get方法

7、HashMap

    HashMap<String,Integer> map = new HashMap<>();
    map.put("hello",2);
    map.put("abcd",10);
    map.put("zhang",3);
    Integer val = map.get("abcd");
    System.out.println(val); // 10


    // 不是按顺序输出的,因为HashMap底层是哈希桶,是通过某种哈希函数确定的位置的

    System.out.println(map);


    // 遍历方式

    for(Map.Entry<String,Integer> entry :map.entrySet()){
         System.out.println("key: "+entry.getKey()+" val: "+entry.getValue());
    }


Map不支持迭代器遍历,因为支持迭代器遍历的都实现了 Iterable 接口,HashMap和TreeMap 都没有实现 Iterable 接口;要想用迭代器遍历,只能将Map转换为Set,再通过迭代器遍历Set


Map的一个注意事项:HashMap的key可以是对象,也可以是null,TreeMap则不行,因为TreeMap的key必须是可以比较的

     HashMap<Student,Integer> map2 = new HashMap<>();
     map2.put(new Student(),2); // hashcode
     map2.put(new Student(),2); 

     map2.put(null,2);

     // 上述代码均能通过

8、HashSet

1. Set不能存储相同的key,天然可以去重的

        HashSet<String> set = new HashSet<>();
        set.add("hello");
        set.add("abcd");
        set.add("hello");

        System.out.println(set); // 打印结果:[hello, abcd]

2. 不是按顺序输出的,因为HashSet底层是哈希桶,是通过某种哈希函数确定的位置的

3. 底层是一个HashMap 每次存储元素时,默认的value是一个Objcet对象

8.1、哈希表性能分析

虽然哈希表一直在和冲突做斗争,但在实际使用过程中,我们认为哈希表的冲突率是不高的,冲突个数是可控的, 也就是每个桶中的链表的长度是一个常数,所以,通常意义下,我们认为哈希表的插入/删除/查找时间复杂度是 O(1) 。

9、hashcode

通过上述可以知道,HashMap的key可以是对象,也可以是null,使用哈希函数的key一定得转化为整数,那么如何把对象转化为整数呢?  -- 使用hashcode方法可以把对象转化为整数

hashcode方法是Object类中的方法,所以可以直接调用

两个相同id的学生,正常情况下在HashMap中在同一个位置,但上图的运行结果得到的整数是不一样的;想要得到正确的结果,需要重写hashcode方法

在IDEA中直接生成,会根据id生成重写的 equals() 和 hashCode() 方法

重写后,再运行相同id的学生会输出相同的整数,这样再通过哈希之后就会在相同的位置

使用HashMap可以用关键字student2拿到student1的值,说明这里的HashMap调用的是自己重写后的hashcode,如果没有重写则找不到

所以,当以后在写一个自定义类型的时候,最好把equals() 和 hashCode() 方法都进行重写,这样就不会出现一些意料之外的事情;拓展:建议把toString也写一下

9.1hashCode和equals的区别

1. 如果两个对象相等,那么它们的hashCode()值一定相同。这里的相等是指,通过equals()比较两个对象时返回true。
2. 如果两个对象hashCode()相等,它们不一定相等。因为在散列表中,hashCode()相等,即两个键值对的哈希值相等。然而哈希值相等,并不一定能得出键值对相等,此时就出现所谓的哈希冲突场景。

3. hashCode和equals的最大区别就是:hashCode 用于找到关键字在哈希表中的位置,这个位置的关键字的hashCode都是相同的;找到位置后,从该位置下的所有关键字中找到某个关键字用 equals

总结:equals方法用于判断两个对象是否相等,hashCode方法用于生成对象的哈希码。equals方法要求两个对象相等时hashCode值也相等,而hashCode方法要求hashCode值相等时两个对象不一定是相等的

10、实现哈希桶<K,V>

public class HashBuck2<K,V> {static class Node<K,V> {public K key;public V val;public Node<K,V> next;public Node(K key,V val) {this.key = key;this.val = val;}}public Node<K,V>[] array;public int usedSize;public static final float DEFAULT_LOAD_FACTOR = 0.75f;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<K,V> cur = array[index];while (cur != null) {if(cur.key.equals(key)) {//更新valuecur.val = val;return;}cur = cur.next;}Node<K,V> node = new Node(key,val);node.next = array[index];array[index] = node;usedSize++;}public V getValue(K key) {int hash = key.hashCode();int index = hash % array.length;Node<K,V> cur = array[index];while (cur != null) {if(cur.key.equals(key)) {return cur.val;}cur = cur.next;}return null;}
}

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

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

相关文章

VS-Code创建Vue3项目

1 创建工程文件 创建一个做工程项目的文件夹 如&#xff1a;h5vue 2 cmd 进入文件 h5vue 3 输入如下命令 npm create vuelatest 也可以输入 npm create vitelatest 4 输入项目名称 项目名称&#xff1a;自已输入 回车 可以按键盘 a (全选) 回车&#xff1a; Playwright…

linux休眠唤醒流程

1、框架 2、休眠流程 应用层通过echo mem > /sys/power/state写入休眠状态&#xff0c;给一张大概流程图 这个操作对应在kernel/power/main.c的state这个attr的store操作 static ssize_t state_store(struct kobject *kobj, struct kobj_attribute *attr, …

Mysql--基础知识点--93--两阶段提交

1 两阶段提交 以update语句的具体执行过程为例&#xff1a; 具体更新一条记录 UPDATE t_user SET name ‘xiaolin’ WHERE id 1;的流程如下&#xff1a; 1.执行器负责具体执行&#xff0c;会调用存储引擎的接口&#xff0c;通过主键索引树搜索获取 id 1 这一行记录&#…

Windows 环境下 Apache 配置 WebSocket 支持

目录 前言1. 基本知识2. 实战前言 🤟 找工作,来万码优才:👉 #小程序://万码优才/r6rqmzDaXpYkJZF 爬虫神器,无代码爬取,就来:bright.cn 原先写过apache的http配置:Apache httpd-vhosts.conf 配置详解(附Demo) 1. 基本知识 🔁 WebSocket 是 HTTP 的升级协议 客户…

UMAEA论文阅读

Preliminaries MMKG为一个五元组G{E, R, A, V, T}&#xff0c;其中E、R、A和V分别表示实体集、关系集、属性集和图像集。 T⊆ERE是关系三元组集。 给定两个MMKG G1 {E1, R1, A1, V1, T1} 和 G2 {E2, R2, A2, V2, T2}&#xff0c; MMEA旨在识别每个实体对&#xff08;e1…

AIGC-十款知识付费类智能体完整指令直接用(DeepSeek,豆包,千问,Kimi,GPT)

Unity3D特效百例案例项目实战源码Android-Unity实战问题汇总游戏脚本-辅助自动化Android控件全解手册再战Android系列Scratch编程案例软考全系列Unity3D学习专栏蓝桥系列AIGC(GPT、DeepSeek、豆包、千问、Kimi)👉关于作者 专注于Android/Unity和各种游戏开发技巧,以及各种资…

Qt界面卡住变慢的解决方法

本质原因: 当Qt界面出现卡顿或无响应时&#xff0c;通常是因为主线程&#xff08;GUI线程&#xff09;被耗时操作阻塞。 完全忘了。。。 Qt Creater解决方法 1. 定位耗时操作 目标&#xff1a;找到阻塞主线程的代码段。 方法&#xff1a; 使用QElapsedTimer测量代码执行时间…

【LangChain4j快速入门】5分钟用Java玩转GPT-4o-mini,Spring Boot整合实战!| 附源码

【LangChain4j快速入门】5分钟用Java玩转GPT-4o-mini&#xff0c;Spring Boot整合实战&#xff01; 前言&#xff1a;当Java遇上大模型 在AI浪潮席卷全球的今天&#xff0c;Java开发者如何快速拥抱大语言模型&#xff1f;LangChain4j作为专为Java打造的AI开发框架&#xff0c…

Vue 3 reactive 和 ref 区别及 失去响应性问题

在 Vue 3 中&#xff0c;reactive 和 ref 是实现响应式数据的两个核心 API&#xff0c;它们的设计目标和使用场景有所不同。以下是两者的详细对比&#xff1a; 1. 基本定义与核心功能 特性reactiveref作用创建对象类型的响应式代理&#xff08;对象、数组、Map 等&#xff09…

第一节:Vben Admin 最新 v5.0初体验

系列文章目录 基础篇 第一节&#xff1a;Vben Admin介绍和初次运行 第二节&#xff1a;Vben Admin 登录逻辑梳理和对接后端准备 第三节&#xff1a;Vben Admin登录对接后端login接口 第四节&#xff1a;Vben Admin登录对接后端getUserInfo接口 第五节&#xff1a;Vben Admin权…

Nginx部署spa单页面的小bug

没部署过&#xff0c;都是给后端干的&#xff0c;自己尝试部署了一个下午终于成功了 我遇到的最大的bug是进入后只有首页正常显示 其他页面全是404&#xff0c;于是问问问才知道&#xff0c;需要这个 location / { try_files $uri $uri/ /index.html; } 让…

面试算法高频08-动态规划-01

动态规划 递归知识要点 递归代码模板&#xff1a;提供递归代码的标准形式public void recur(int level, int param) &#xff0c;包含终止条件&#xff08;if (level> MAX_LEVEL)&#xff09;、当前层逻辑处理&#xff08;process(level, param)&#xff09;、向下一层递归…

若依框架前后端分离版部署全流程详解(本地+服务器+高级配置)

若依框架前后端分离版部署全流程详解&#xff08;本地服务器高级配置&#xff09; 若依&#xff08;RuoYi&#xff09;作为一款基于SpringBoot和Vue的权限管理系统&#xff0c;凭借其模块化设计和开箱即用的特性广受开发者欢迎。本文将从本地部署、服务器部署、高级配置三个维…

医疗设备预测性维护合规架构:从法规遵循到技术实现的深度解析

在医疗行业数字化转型加速推进的当下&#xff0c;医疗设备预测性维护已成为提升设备可用性、保障医疗安全的核心技术。然而&#xff0c;该技术的有效落地必须建立在严格的合规框架之上。医疗设备直接关乎患者生命健康&#xff0c;其维护过程涉及医疗法规、数据安全、质量管控等…

LLMs基础学习(七)DeepSeek专题(4)

LLMs基础学习&#xff08;七&#xff09;DeepSeek专题&#xff08;4&#xff09; 文章目录 LLMs基础学习&#xff08;七&#xff09;DeepSeek专题&#xff08;4&#xff09;DeepSeek-R1 训练过程的四个阶段具体流程小结 “规则化奖励”具体原因小结 “自我认知”&#xff08;se…

SQL 速查手册

前言&#xff1a;SQL&#xff08;Structured Query Language&#xff09;是用于管理关系型数据库的标准语言&#xff0c;广泛应用于数据查询、更新、定义和管理等操作。本文将为你提供一份详细的 SQL 速查手册&#xff0c;涵盖从基础到高级的各种 SQL 操作&#xff0c;帮助你快…

IDEA 中 Scala 项目远程连接虚拟机 Spark 环境

IDEA 中 Scala 项目远程连接虚拟机 Spark 环境 1. 环境准备 确保虚拟机 Spark 环境正常运行 虚拟机中已安装并启动 Spark记录虚拟机的 IP 地址和 Spark 端口&#xff08;默认 7077&#xff09;确保虚拟机防火墙允许相关端口访问 本地 IDEA 环境配置 安装 Scala 插件安装 Spar…

.net core 项目快速接入Coze智能体-开箱即用-全局说明

目录 一、Coze智能体的核心价值 二、开箱即用-效果如下 三 流程与交互设计 为什么要分析意图&#xff0c;而不是全部交由AI处理。 四 接入前的准备工作 五&#xff1a;代码实现----字节Coze 签署 JWT和获取Token .net core 项目快速接入Coze智能体-开箱即用 .net core快…

网店运营精细化突破新路径

内容概要 电商战场越来越卷&#xff0c;单纯靠低价和流量轰炸已经玩不转了。今天想要站稳脚跟&#xff0c;精细化运营才是破局密码——从商品怎么选、用户怎么留&#xff0c;到供应链怎么跑得更快&#xff0c;每个环节都得抠细节。比如用数据给选品“开天眼”&#xff0c;把用…

数据结构学习笔记 :线性表的链式存储详解

目录 单链表 1.1 无头单链表 1.2 有头单链表单向循环链表双链表 3.1 双链表 3.2 双向循环链表总结与对比 一、单链表 1. 无头单链表&#xff08;Headless Singly Linked List&#xff09; 定义&#xff1a;链表无头结点&#xff0c;直接由头指针指向第一个数据节点。 特点&…