Java:关于哈希表

Java:关于哈希表

目录

哈希表

概念

冲突

负载因子调节

冲突的解决

哈希桶的实现

完整代码


哈希表

概念

哈希表是一种理想的从顺序表以及平衡树中查找元素的方式,它可以不经过任何比较,一次直接从表中得到想要搜索的元素。如果构造一种存储结构,通过某种函数使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

  • 插入元素:根据待插入元素的关键码,根据函数计算出存储位置并进行存放
  • 搜索元素:对元素的关键码进行计算,把求得的数据当作元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功

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

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

上面这种存放元素的方式,不用多次进行关键码的比较,搜索速度比较快,但是上面所取的集合只是一个普通情况,如果集合里再添加一个 14 那么,14应该放在哪里那?

这里便是要提到冲突。


冲突

对于两个数据元素的关键字 和 (i != j),有 != ,但有:Hash( ) == Hash( ),即:不同关键字通过相同哈 希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。

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

对于冲突,我们要认识到:

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

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

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

常见的哈希函数有:

  • 直接定制法:取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B 。优点是简单、均匀。缺点是需要事先知道关 键字的分布情况 使用场景:适合查找比较小且连续的情况
  • 除留余数法:设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数: Hash(key) = key% p(p<=m),将关键码转换成哈希地址
  • 负载因子调节,这个下面会重点讲解

其他的方法还有:平方取中法、折叠法、随机数法、数学分析法等,感兴趣的话可以了解一下。


负载因子调节

 产生冲突的概率叫做冲突率,已知哈希表中已有的关键字个数是不变的,那么我们能调整的就只有哈希表中的数组的大小。

Java中负载因子的值为0.75,即当 填入表中的元素个数 / 散列表的长度 > 0.75时。产生冲突的概率会很大,这时候我们就要来解决冲突。


冲突的解决

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

闭散列:当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以 把key存放到冲突位置中的“下一个” 空位置中去。

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

这里主要用的是通过开散列(哈希桶)来解决冲突

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

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

哈希桶的实现

从上图哈希桶图所示,我们可以把它看成是数组+链表的形式,这样我们就可以定义相关变量了。

//定义相关变量 static class Node{ public int val; public int key; public Node next; public Node(int val, int key){ this.val = val; this.key = key; } public Node[] elem = new Node[10]; public int useSize; }

插入数据

插入数据的第一步是要在数组中找到它所在的位置,然后进行链表的插入,头插法 

public void push(int key, int val){ int index = key % array.length; Node cur = array[index]; while(cur != null){ if(cur.key == key){ cur.val = val; return; } cur = cur.next; } //没有找到当前链表中有这个key的节点 //头插法 Node node = new Node(val, key); node.next = array[index]; array[index] = node; useSize++; }

不过,这里有个重点,要注意负载因子,计算负载因子。

以代码的数据为例,如果数组中的所放数据个数大于7,那么就会有很大概率产生冲突,这时我们要解决冲突,就要对哈希表进行扩容,这里并不是简单地把数组扩大两倍,在扩大后还要把前面整个数组的数据遍历一遍,然后再次进行对应位置的存储。

像是没扩容之前,array[4] 中可能放着 4和14两个数据,现在数组长度从10扩容到20,那么4还应该放在array[4]里面,而14应该放在array[14]里面。

故要包括扩容以及再次哈希来进行

插入完整代码如下

public void push(int key, int val){ int index = key % array.length; Node cur = array[index]; while(cur != null){ if(cur.key == key){ cur.val = val; return; } cur = cur.next; } //没有找到当前链表中有这个key的节点 //头插法 Node node = new Node(val, key); node.next = array[index]; array[index] = node; useSize++; if(doLoadFactor() >= DEFAULT_LOAD_FACTOR){ //扩容 resize(); } } public void resize(){ //array = Arrays.copyOf(array, 2*array.length); Node[] newArray = new Node[2*array.length]; for (int i = 0; i < array.length; i++) { Node cur = array[i]; while(cur != null){ int newIndex = cur.key % newArray.length; Node curN = cur.next; cur.next = newArray[newIndex]; newArray[newIndex] = cur; cur = curN; } } array = newArray; } private double doLoadFactor() { return useSize*1.0 / array.length; }

注意:这里的 DEFAULT_LOAD_FACTOR 是在定义在相关变量里的,其完整代码为:

//定义相关变量 static class Node{ public int val; public int key; public Node next; public Node(int val, int key){ this.val = val; this.key = key; } public Node[] elem = new Node[10]; public int useSize; public static final double DEFAULT_LOAD_FACTOR = 0.75f; }

这里我们可以简单进行调试,

Test类代码为:

public class Test { public static void main(String[] args) { HashBusk hashBusk = new HashBusk(); hashBusk.push(1, 9); hashBusk.push(11, 9); hashBusk.push(14, 9); hashBusk.push(4, 9); hashBusk.push(2, 9); hashBusk.push(15, 9); hashBusk.push(6, 9); hashBusk.push(5, 9); } }

调试的断点放在了第7个数的位置,因为再往下走需要进行扩容了,可以看到代码是按照上面的数组+链表的方式进行存储的。

然后是扩容的部分

可以看到,扩容后数组的长度来到15,证明扩容部分也是可以正常进行的。

getVal方法

通过key的值,来得到val值,这部分代码,其实和插入里部分代码有些相同的部分:

通过key值来找到数据的位置,如果相同返回val值,没找到返回-1。

public int getVal(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; }
完整代码
public class HashBusk { //定义相关变量 static class Node { public int val; public int key; public Node next; public Node(int val, int key) { this.val = val; this.key = key; } } public Node[] array = new Node[10]; public int useSize; public static final double DEFAULT_LOAD_FACTOR = 0.75f; public void push(int key, int val){ int index = key % array.length; Node cur = array[index]; while(cur != null){ if(cur.key == key){ cur.val = val; return; } cur = cur.next; } //没有找到当前链表中有这个key的节点 //头插法 Node node = new Node(val, key); node.next = array[index]; array[index] = node; useSize++; if(doLoadFactor() >= DEFAULT_LOAD_FACTOR){ //扩容 resize(); } } public void resize(){ Node[] newArray = new Node[2*array.length]; for (int i = 0; i < array.length; i++) { Node cur = array[i]; while(cur != null){ int newIndex = cur.key % newArray.length; Node curN = cur.next; cur.next = newArray[newIndex]; newArray[newIndex] = cur; cur = curN; } } array = newArray; } private double doLoadFactor() { return useSize*1.0 / array.length; } public int getVal(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; } }

Read more

Flutter 三方库 jwt_io 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、严谨、全能的 JSON Web Token (JWT) 加解密与身份安全验证引擎

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 jwt_io 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、严谨、全能的 JSON Web Token (JWT) 加解密与身份安全验证引擎 在鸿蒙(OpenHarmony)系统的端云一体化登录、政企应用的安全审计或复杂的跨端权限校验场景中,如何确保来自云端授信中心的 JWT Token 既能被正确解析(Decode),又能被严密地校验其合法性与过期时间?jwt_io 为开发者提供了一套工业级的、基于 RFC 7519 标准的 JSON Web Token 深度处理方案。本文将深入实战其在鸿蒙应用安全底座中的应用。 前言 什么是 JWT IO?它不仅是一个简单的 Base64 解码器,而是一个具备深厚 RFC

By Ne0inhk

Ubuntu 22.04 中禁用 `unattended-upgrades` 完全指南

Ubuntu 22.04 中禁用 unattended-upgrades 完全指南 📌 什么是 unattended-upgrades? unattended-upgrades 是 Ubuntu 系统默认预装的自动更新工具,主要用于自动下载并安装安全更新(如系统漏洞修复、关键组件补丁),无需用户手动干预。其设计目的是提升系统安全性,但在部分场景下(如服务器稳定运行、测试环境控制、带宽受限等),用户可能需要禁用该功能。 ⚠️ 禁用前的重要提醒 * 禁用自动更新后,系统将不再自动获取安全补丁,需手动定期执行更新(推荐 sudo apt update && sudo apt upgrade -y),否则可能面临安全风险。 * 以下方法适用于 Ubuntu 22.04(基于 Debian 11 架构),其他版本可能略有差异。 * 操作前建议备份关键配置文件(如 /etc/apt/

By Ne0inhk
【Linux 网络】理解并应用应用层协议:HTTP(附简单HTTP服务器C++代码)

【Linux 网络】理解并应用应用层协议:HTTP(附简单HTTP服务器C++代码)

前言:         上文我们学习到了什么是守护进程以及其原理【Linux网络】深入理解守护进程(Daemon)及其实现原理-ZEEKLOG博客         本文我们来认识应用层的协议:HTTP! HTTP协议         虽然应用层协议通常可由程序员自定义,但在实际开发中,我们通常直接使用业界专家已经定义好且非常成熟的现成协议。HTTP(超文本传输协议)就是其中最重要、最好用的应用层协议之一。         HTTP是互联网世界的基石,它定义了客户端(如浏览器)与服务器之间进行通信的标准方式,主要用于交换或传输超文本数据(例如 HTML 文档)。         HTTP协议遵循标准的请求-响应(Request-Response)模型:         请求:客户端通过HTTP协议主动向服务器发送请求。         响应:服务器收到客户端发来的请求后进行处理,并将结果作为响应返回给客户端。         HTTP协议具有两个显著特点:         无连接:每一次请求都想要建立一个新的连接,处理完既断开         无状态:服务器不会保存客

By Ne0inhk
Flutter 三方库 http_core_client 的鸿蒙化适配指南 - 打造极简、健壮的 OpenHarmony 网络请求核心组件

Flutter 三方库 http_core_client 的鸿蒙化适配指南 - 打造极简、健壮的 OpenHarmony 网络请求核心组件

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 http_core_client 的鸿蒙化适配指南 - 打造极简、健壮的 OpenHarmony 网络请求核心组件 在 Flutter 应用开发中,网络请求层(Networking Layer)是应用的生命线。虽然 dio 功能强大,但对于追求轻量化、高性能的鸿蒙应用来说,一个精简且核心功能完备的客户端库往往更具优势。http_core_client 为开发者提供了一套基于 BaseClient 封装的极简模型。在 OpenHarmony(鸿蒙)环境下,如何结合其底层网络栈、处理系统的代理配置以及优化连接复用,是构建高标准鸿蒙应用的必修课。本文将带大家深入探讨其适配要点。 前言 随着鸿蒙系统(HarmonyOS)进入原生应用开发的新阶段,网络栈的稳定性与安全性(如 TLS

By Ne0inhk