Java数据结构第二十三期:Map与Set的高效应用之道(二)



专栏:Java数据结构秘籍
个人主页:手握风云
目录
一、哈希表
1.1. 概念
1.2. 冲突
1.3. 避免冲突
1.4. 解决冲突
1.5. 实现
二、OJ练习
2.1. 只出现一次的数字
2.2. 随机链表的复制
2.3. 宝石与石头
一、哈希表
1.1. 概念
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找⼀个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为,平衡树中为树的⾼度,即
,搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索⽅法:可以不经过任何比较,⼀次直接从表中得到要搜索的元素。如果构造⼀种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
当向该结构中,根据待插⼊元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放;对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功。该方式即为哈希(散列)方法,哈希方法中使⽤的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)。
哈希函数设置为:hash(key) = key % capacity;其中capacity为存储元素底层空间总的大小。
我们设一个整数集合{1,7,6,4,5,9},把capacity设置为10,那我们就可以按照下图来存储。如果我们再想存放一个元素12,我们可以直接通过哈希函数存进下标2中,要想搜索,直接通过2下标来找到12,这样时间复杂度为,从而提高效率。

1.2. 冲突
不同关键字通过相同哈希函数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。把具有不同关键码⽽具有相同哈希地址的数据元素称为“同义词”。例如我们要想存放一个14,通过上面的哈希函数应该存到4下标,但此时4下标已经存了一个4,就会造成哈希冲突。
出现了哈希冲突,我们就要想办法避免冲突。由于我们哈希表底层数组的容量往往是小于实际要存储的关键字的数量的,就会导致冲突的发⽣是必然的,但我们能做的应该是尽量的降低冲突率。
1.3. 避免冲突
第一种方法可以设计合理的哈希函数。设计原则:定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间;计算出来的地址能均匀分布在整个空间中;比较简单。
直接订制法:取关键字的某个线性函数为散列地址:Hash(Key) = A*Key + B。优点:简单、均匀。缺点:需要事先知道关键字的分布情况。使用场景:适合查找比较小且连续的情况。
除留余数法:设散列表中允许的地址数为m,取⼀个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址。
哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突。
我们还有另外一种就是调节负载因子。哈希表的载荷因子为:ą=填入表中元素个数/哈希表长度。当冲突率达到⼀个⽆法忍受的程度时,我们需要通过降低负载因子来变相的降低冲突率。已知哈希表中已有的关键字个数是不可变的,那我们能调整的就只有哈希表中的数组的大小。

1.4. 解决冲突
解决哈希冲突两种常⻅的⽅法是:闭散列和开散列。
闭散列:也叫开放地址法,当发⽣哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下⼀个” 空位置中去。那如何寻找下⼀个空位置呢?此时我们就需要应用线性探索。从发生冲突的位置开始,依次向后探测,直到寻找到下⼀个空位置为止。但这样还是会有一个缺点,就是会使得冲突元素聚集在一起,并且如果我们把4删除了,又如何寻找14、24、34这些元素。因此线性探测采⽤标记的伪删除法来删除⼀个元素。

⼆次探测为了避免该问题,找下⼀个空位置的⽅法为:Hi = (H0+i^2)%m,i表示冲突的次数,m为表的大小。
开散列:开散列法⼜叫链地址法(开链法),⾸先对关键码集合⽤散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每⼀个⼦集合称为⼀个桶,各个桶中的元素通过⼀个单链表链接起来,各链表的头结点存储在哈希表中。
1.5. 实现
由于我们需要节点数组来创建哈希表,利用内部类来表示节点对象。
public class HashBucket {//创建节点数组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 = new Node[10];public int UsedSize;//表示还系统中存放的元素public static final float LOAD_FACTOR = 0.75f;//负载因子表示为常数
}
我们先来模拟哈希表中放元素的方法。我们要想把元素放入,首先得是一个结点。比如key=14,如果表中已经有14了,就不能再放14并且更新val,所以我们首先需要遍历数组判断key是否相同,如果相同,更新val。下面再使用头插法来把节点元素放入哈希表中。插入元素之后,我们还需要重新计算负载因子是否超过了我们规定的LOAD_FACTOR。如果超过了,就需要进行扩容操作。扩容的时候还需要注意,比如我们要插入的元素的key为14,扩容前需要插入下标为4的位置,扩容2倍后,就需要插入下标为14的位置。
完整代码实现:
public void put(int key, int val) {int index = key % array.length;//先遍历index数组下的链表,如果有相同的key,则更新valNode cur = array[index];while (cur != null) {if (cur.key == key) {cur.val = val;return;}cur = cur.next;}//头插法Node node = new Node(key, val);node.next = array[index];array[index] = node;UsedSize++;//重新计算负载因子是不是超过了我们规定的值if (CalculateLoadFactor() >= LOAD_FACTOR) {//扩容ReSize();}}private float CalculateLoadFactor() {return UsedSize * 1.0f / array.length;}private void ReSize() {Node[] newArray = new Node[array.length*2];for (Node node : array) {Node cur = node;while (cur != null) {int newIndex = cur.key % newArray.length;//把当前节点放入新数组的位置,再次使用头插法Node curNext = cur.next;cur.next = newArray[newIndex];newArray[newIndex] = cur;cur = curNext;}}array = newArray;}
get方法也是一样,也是通过索引下标来寻找目标值。
public int get(int key){int index = key % array.length;Node cur = array[index];while(cur != null){if(cur.key == key){return cur.val;}cur = cur.next;}return -1;}
我们在Test类里面进行实例化并调试。
public class Test {public static void main(String[] args) {HashBucket hashBucket = new HashBucket();hashBucket.put(11,99);hashBucket.put(2,99);hashBucket.put(43,99);hashBucket.put(4,99);hashBucket.put(14,99);hashBucket.put(24,99);hashBucket.put(7,99);hashBucket.put(8,99);}
}


我们上面的方法key是整型,那如果key是引用类型呢,比如String或者Person类。那我们就把整型换作是泛型K、V。需要注意的是,key换成了泛型,不能直接进行%操作,我们可以使用hashCode方法转成整型,并且进行比较要使用equals方法。
/*** @author: gao* @create-date: 2025/3/15 16:32*/public class HashBucket<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 = (Node<K, V>[]) new Node[10];public int UsedSize;//表示还系统中存放的元素public static final float LOAD_FACTOR = 0.75f;//负载因子表示为常数public void put(K key, V val) {int hashcode = key.hashCode();int index = hashcode % array.length;//先遍历index数组下的链表,如果有相同的key,则更新valNode<K, V> cur = array[index];while (cur != null) {if (cur.key == key) {cur.val = val;return;}cur = cur.next;}Node<K, V> node = new Node<K, V>(key, val);node.next = array[index];array[index] = node;UsedSize++;}public V get(K key) {int hashcode = key.hashCode();int index = hashcode % array.length;Node<K, V> cur = array[index];while (cur != null) {if (cur.key.equals(key)) {return cur.val;}cur = cur.next;}return null;}
}
如果我们要判断是否为同一个人,我们可以判断身份证号码是否相等。如果我们按照这种方法去写,发现比较结果为false。这是因为我们没有重写equals和hashCode方法,编译器默认调用Object方法。
class Person {public String id;public Person(String id) {this.id = id;}
}public class Test {public static void main(String[] args) {Person person1 = new Person("1234");Person person2 = new Person("1234");System.out.println(person1.equals(person2));System.out.println(person1.hashCode());System.out.println(person2.hashCode());}
}

public boolean equals(Object obj) {return (this == obj);}
我们在Person类里面右击,点击Generate,再点击equals() and hashCode(),就可以重写。
@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.hash(id);}
二、OJ练习
2.1. 只出现一次的数字

我们的基本思路是:利用HashSet,先遍历一遍数组,把集合中没有的数字放入,如果有,再移除,最后集合中剩下的元素就是只出现一次的数字,再遍历一遍数组,匹配HashSet中的数组。
完整代码实现:
class Solution {public int singleNumber(int[] nums) {Set<Integer> set = new HashSet<>();for (int i = 0;i < nums.length;i++) {if(! set.contains(nums[i])){set.add(nums[i]);}else{set.remove(nums[i]);}}for (int i = 0;i < nums.length;i++) {if(set.contains(nums[i])){return nums[i];}}return -1;}
}
执行时间还是比较高,因为使用了两次for循环遍历数组。
2.2. 随机链表的复制

题目比较长,大概题意就是复制出一份与原来相同的链表。这道题的难点在于比单链表多了一个可以指向任意节点或者空的random域。起初,很多人会去想定义一个Node cur去遍历一遍链表,一个一个节点进行拷贝,但一拷贝就会发现问题,因为我们我们不知道cur.next和cur.random是哪一个节点的地址。既然遍历一遍链表不行,那就遍历两遍。第一遍遍历,所有节点的val域全都拷贝过来,next域以及random域全都默认为null,每遍历一个链表,就新实例化一个节点。然后我们<K,V>结构来建立老节点与新节点之间的映射关系。

我们每获取一个节点的地址,都可以修改它的next域与random域。
完整代码实现:
class Solution {public Node copyRandomList(Node head) {Map<Node,Node> map = new HashMap<>();//第一遍遍历链表Node cur = head;while(cur != null){Node node = new Node(cur.val);map.put(cur,node);cur = cur.next;}//第二遍遍历链表cur = head;while(cur != null){map.get(cur).next = map.get(cur.next);map.get(cur).random = map.get(cur.random);cur = cur.next;}return map.get(head);}
}
2.3. 宝石与石头

题目很简单,就是查找stones中含有jewels中的字符的个数。我们先遍历jewels字符串,将里面的字符放入集合中,再去遍历stones中的字符,最后返回宝石个数。
完整代码实现:
class Solution {public int numJewelsInStones(String jewels, String stones) {Set<Character> set = new HashSet<>();for (int i = 0; i < jewels.length(); i++) {char ch = jewels.charAt(i);set.add(ch);}int count = 0;for (int i = 0; i < stones.length(); i++) {char ch = stones.charAt(i);if(set.contains(ch)){count++;}}return count;}
}相关文章:
Java数据结构第二十三期:Map与Set的高效应用之道(二)
专栏:Java数据结构秘籍 个人主页:手握风云 目录 一、哈希表 1.1. 概念 1.2. 冲突 1.3. 避免冲突 1.4. 解决冲突 1.5. 实现 二、OJ练习 2.1. 只出现一次的数字 2.2. 随机链表的复制 2.3. 宝石与石头 一、哈希表 1.1. 概念 顺序结构以及平衡树中…...
linux系统命令——权限
一、有哪些权限 读(r)——对应数字4 写(w)——对应数字2 执行(x)——对应数字1 二、权限及数字的对应 4对应r-- 2对应-w- 1对应--x 5对应r-x 6对应rw- 7对应rwx 三、文件的基本属性 如图&#…...
设计模式-工厂模式、策略模式、代理模式、责任链模式
目录 1 工厂模式 1.1 简单工厂模式 1.2 工厂方法模式 1.3 抽象工厂模式 1.4 工厂模式适用的场合 1.5 三种工厂模式的使用选择 2 策略模式 2.1 定义 2.2 结构 3 代理模式 3.1 啥是代理模式 3.2 为啥要用代理模式 3.3 代理模式分类 3.3.1 静态代理 3.3.2 动态代理…...
nginx中间件部署
普通权限账户安装NGINX中间件 1、拥有高级权限的账户安装必要的插件 sudo yum install -y gcc-c make pcre pcre-devel zlib zlib-devel openssl openssl-devel 2、普通账户进行NGINX的脚本式安装 vi nginx_intall.sh #!/bin/bash TAR_NAME"$1" TAR_NAME_DIRba…...
PentestGPT 下载
PentestGPT 下载 PentestGPT 介绍 PentestGPT(Penetration Testing GPT)是一个基于大语言模型(LLM)的智能渗透测试助手。它结合了 ChatGPT(或其他 GPT 模型)与渗透测试工具,帮助安全研究人员自…...
JVM 2015/3/15
定义:Java Virtual Machine -java程序的运行环境(java二进制字节码的运行环境) 好处: 一次编写,到处运行 自动内存管理,垃圾回收 数组下标越界检测 多态 比较:jvm/jre/jdk 常见的JVM&…...
Java中接口隔离原则简介和代码举例
简介: 接口隔离原则(Interface Segregation Principle,ISP)是面向对象设计SOLID原则中的“I”,其核心思想是: 定义 客户端不应被迫依赖它不使用的方法。即,一个类对另一个类的依赖应建立在最…...
基于自定义线程池手写一个异步任务管理器
我们在后端执行某些耗时逻辑操作时往往会导致长时间的线程阻塞,在这种情况之下,我们往往会引一条异步线程去处理这些异步任务,如果每次都创建新的线程来处理这些任务,不仅会增加代码冗余,还可能造成线程管理混乱&#…...
sql靶场-时间盲注(第九、十关)保姆级教程
目录 时间盲注(第九、十关) 1.判断 2.确认时间盲注 2.手工尝试时间盲注 数据库名长度 数据库名字符 表数 表名长度 表名字符 字段数 字段名长度 字段名字符 4.脚本时间盲注注入 5.第十关 时间盲注(第九、十关) 1.判…...
Vuex 高级技巧与最佳实践
使用 map 辅助函数简化代码: javascript import { mapState, mapGetters } from vuexexport default {computed: {...mapState([num]),...mapGetters([doubleNum])} }模块化开发: javascript // modules/student.js export default {namespaced: true,st…...
51c自动驾驶~合集54
我自己的原文哦~ https://blog.51cto.com/whaosoft/13517811 #Chameleon 快慢双系统!清华&博世最新:无需训练即可解决复杂道路拓扑 在自动驾驶技术中,车道拓扑提取是实现无地图导航的核心任务之一。它要求系统不仅能检测出车道和交…...
大模型推理:LM Studio在Mac上部署Deepseek-R1模型
LM Studio LM Studio是一款支持离线大模型部署的推理服务框架,提供了易用的大模型部署web框架,支持Linux、Mac、Windows等平台,并提供了OpenAI兼容的SDK接口,主要使用LLama.cpp和MLX推理后端,在Mac上部署时选择MLX推理…...
扩散模型:AIGC领域的核心引擎,解锁图像生成新维度
一、扩散模型技术原理 扩散模型是一类生成模型,它运用了物理热力学中的扩散思想, 主要包括前向扩散和反向扩散两个过程。 1.1、生成模型 在深度学习中,生成模型的目标是根据给定的样本(训练数据) 生成新样本。首先给…...
Java多线程与高并发专题——原子类和 volatile、synchronized 有什么异同?
原子类和 volatile异同 首先,通过我们对原子类和的了解,原子类和volatile 都能保证多线程环境下的数据可见性。在多线程程序中,每个线程都有自己的工作内存,当多个线程访问共享变量时,可能会出现一个线程修改了共享变…...
//要求:将输入的字符串中的数字转换为罗马数字,长度小于9(运用方法:Switch方法)
import java.util.Scanner;public class Num2 {public static void main(String[] args){ // I II III IV V VI VII VIII IX//要求:将输入的字符串中的数字转换为罗马数字,长度小于9(运用方法:查表法)//1输入数字//2有效字符判断/…...
【数据结构】数据结构,算法 概念
0.本篇问题: 数据、数据元素、数据对象、数据项之间的基本关系?ADT是什么?数据结构的三要素?数据的逻辑结构有哪些?数据的存储结构有哪些?算法的五个特征?O(1) O(logn) O(n^n) O(n) O(n^2…...
pytest 框架学习总结
视频:pytest01-快速上手_哔哩哔哩_bilibili 资料:pytest 框架 - 白月黑羽 基于 Python 语言的自动化测试框架 最知名的 有如下 3 款unittest、pytest、robotframework 前两款框架主要(或者说很大程度上)是 聚焦 在 白盒单元测试…...
总结 HTTP 协议的基本格式, 相关知识以及抓包工具fiddler的使用
目录 1 HTTP是什么 2 HTTP协议格式 3 HTTP请求(Request) 3.1 认识URL 3.2 方法 3.3 认识请求"报头"(header) 4 HTTP响应详解 4.1 认识"状态码"(statuscode) 4.2 认识响应"报头"(header) 4.3 认识响应"正⽂"(body) 5 通过f…...
python中的max(),需要注意的点
words ["apple", "banana", "grape", "cherry"] 对每个单词,keylambda x: len(x) 会计算它的长度: "apple" 长度是 5"banana" 长度是 6"grape" 长度是 5"cherry" 长度…...
DeepSeek-R1大模型微调技术深度解析:架构、方法与应用全解析
1. DeepSeek-R1大模型架构设计与技术特性 1.1 架构设计 DeepSeek-R1作为超大规模语言模型,其核心架构设计包含以下创新: 专家混合架构(MoE) 采用6710亿参数的混合专家架构(MoE),每个推理过程仅激活370亿参数,实现计算效率与资源利用率的突破性提升。 Transformer框架…...
探索Maas平台与阿里 QWQ 技术:AI调参的魔法世界
摘要:本文介绍了蓝耘 Maas 平台在人工智能领域的表现及其核心优势,包括强大的模型支持、高效的资源调度和友好的操作界面。文章还探讨了蓝耘 Maas 平台与阿里 QWQ 技术的融合亮点及应用拓展实例,并提供了调参实战指南,最后对蓝耘 …...
Linux第三次练习
1、创建根目录结构中的所有的普通文件 首先在根目录下面新创建一个test目录,然后将查找到的普通文件新建到test目录下 2、列出所有账号的账号名 3、将/etc/passwd中内容按照冒号隔开的第三个字符从大到小排序后输出所有内容 4、列出/etc/passwd中的第20行-25行内容…...
软件测试知识总结
1、黑盒测试、白盒测试、灰盒测试 1.1 黑盒测试 黑盒测试又叫功能测试、数据驱动测试 或 基于需求规格说明书的功能测试。该类测试注重于测试软件的功能性需求。 采用这种测试方法,测试工程师把测试对象看作一个黑盒子,完全不考虑程序内部的逻辑结构和…...
JConsole 监控线程池状态
JConsole 可以用来监控 Java 线程池(ThreadPoolExecutor)的状态,包括线程数量、任务执行情况、CPU 及内存使用情况等。下面是具体的操作步骤: 一、启动 JConsole 1. 启动 JConsole Windows:在 JDK bin 目录下找到 j…...
【HTML】三、表单与布局标签
文章目录 1、input1.1 input的占位文案1.2 单选框1.3 上传文件1.4 多选框 2、 下拉菜单3、文本域:多行输入4、label标签:说明与增大点击范围5、按钮与form表单6、无语义布局标签7、有语义的布局标签8、字符实体9、练习:注册页面 1、input in…...
OpenBMC:BmcWeb添加路由1 getParameterTag
BmcWeb对于路由的设计其实是参考了Crow BMCWEB_ROUTE(app, "/upload/image/<str>").privileges({{"ConfigureComponents", "ConfigureManager"}}).methods(boost::beast::http::verb::post, boost::beast::http::verb::put)([](const cro…...
【结构设计】3D打印创想三维Ender 3 v2
【结构设计】3D打印创想三维Ender 3 v2 文章目录 前言一、Creality Slicer1.2.3打印参数设置二、配件更换1.捆扎绑扎线2.气动接头3D打印机配件插头3.3D打印机配件Ender3pro/V2喷头套件4.读卡器 TF卡5.micro sd卡 三、调平四、参考文章总结 前言 使用工具: 1.创想三…...
嵌入式web服务器实现上传下载储存研究
标题:嵌入式web服务器实现上传下载储存研究 内容:1.摘要 随着互联网与嵌入式系统的不断融合,嵌入式设备对数据上传、下载及储存功能的需求日益增长。本文旨在研究嵌入式web服务器实现上传、下载和储存功能的有效方法。通过分析常见的嵌入式web服务器架构࿰…...
UE小:UE5.5 PixelStreamingInfrastructure 使用时注意事项
1、鼠标默认显示 player.ts中的Config中添加HoveringMouse:true 然后运行typescript\package.json中的"build":npx webpack --config webpack.prod.js...
Anaconda 入门指南
Anaconda 入门指南 一、下载安装 Anaconda 1、下载地址:Anaconda 推荐下载 python3 版本, 毕竟未来 python2 是要停止维护的。 2、安装 Anaconda 按照安装程序提示一步步安装就好了, 安装完成之后会多几个应用: Anaconda Navigtor :用于管…...
