【Java数据结构】哈希表——学习笔记

本文涉及的产品
函数计算FC,每月15万CU 3个月
简介: 概念、哈希冲突的概念、对于哈希冲突的理解、如何避免哈希冲突——哈希函数设计、、如何避免哈希冲突——负载因子调节、如何解决哈希冲突、解决冲突——闭散列、解决冲突——开散列/哈希桶、冲突严重时的解决办法、Hash桶实现HashMap、性能分析、哈希表和Java类集的关系

概念

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


理想的搜索方法:

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


当向该结构中:


插入元素

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

搜索元素

对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功

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


例如:数据集合{1,7,6,4,5,9};

哈希函数设置为:hash(key) = key % capacity;

capacity为存储元素底层空间总的大小。

c1.png


用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快


问题:按照上述哈希方式,向集合中插入元素44,会出现什么问题?

答:这就是哈希冲突,4%10 和44%10 得到的位置都是4,那这个位置到底存4还是44呢?往下看


哈希冲突的概念

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


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


对于哈希冲突的理解

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


如何避免哈希冲突——哈希函数设计


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


哈希函数设计原则:


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

哈希函数计算出来的地址能均匀分布在整个空间中

哈希函数应该比较简单

以下给出几个常见的哈希函数:


1.直接定制法–(常用)

取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B

优点: 简单、均匀

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

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

2.除留余数法–(常用)

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

3.平方取中法–(了解)

假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址; 再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址 平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况

4.折叠法–(了解)

折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况

5.随机数法–(了解)

选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中random为随机数函数。通常应用于关键字长度不等时采用此法

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

设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。

例如:

假设要存储某家公司员工登记表,如果用手机号作为关键字,那么极有可能前7位都是 相同的,那么我们可以选择后面的四位作为散列地址,如果这样的抽取工作还容易出现 冲突,还可以对抽取出来的数字进行反转(如1234改成4321)、右环位移(如1234改成4123)、左环移位、前两数与后两数叠加(如1234改成12+34=46)等方法。

c2.png

数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀的情况

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


如何避免哈希冲突——负载因子调节

c3.png

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

c4.png


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


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


如何解决哈希冲突

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


解决冲突——闭散列

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


线性探测

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

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


插入:


通过哈希函数获取待插入元素在哈希表中的位置

如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素


c5.png


二次探测

线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此

c6.png


二次探测为了避免该问题,找下一个空位置的方法为

c7.png


c8.png


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


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


解决冲突——开散列/哈希桶

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


c9.png


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


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


冲突严重时的解决办法

刚才我们提到了,哈希桶其实 可以看作将大集合的搜索问题转化为小集合的搜索问题 了,那如果冲突严重,就意味着小集合的搜索性能其实也时不佳的,这个时候我们就可以 将这个所谓的小集合搜索问题继续进行转化

例如:


每个桶的背后是另一个哈希表

每个桶的背后是一棵搜索树

Hash桶实现HashMap

实现代码的时候注意一个问题,扩容的时候,要将所有数据重新哈希一遍,因为key对应的哈希值(位置下标)有可能改变


c10.png


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;

       }

   }


   private Node[] array;

   public int usedSize;


   public HashBucket() {

       this.array = new Node[10];

       this.usedSize = 0;

   }



   public void put(int key, int val) {

       //1、确定下标

       int index = key % this.array.length;

       //2、遍历这个下标的链表

       Node cur = array[index];

       while (cur != null) {

           //更新val

           if (cur.key == key) {

               cur.val = val;

               return;

           }

           cur = cur.next;

       }

       //3、cur == null   当前数组下标的 链表  没要key

       Node node = new Node(key, val);

       node.next = array[index];

       array[index] = node;

       this.usedSize++;

       //4、判断  当前 有没有超过负载因子

       if (loadFactor() >= 0.75) {

           //扩容

           resize();

       }

   }


   public int get(int key) {

       //以什么方式存储的  那就以什么方式取

       int index = key % this.array.length;


       Node cur = array[index];


       while (cur != null) {

           if (cur.key == key) {

               return cur.val;

           }

           cur = cur.next;

       }


       return -1;//

   }



   public double loadFactor() {

       return this.usedSize * 1.0 / this.array.length;

   }


   public void resize() {

       //自己创建新的2倍数组

       Node[] newArray = new Node[2 * this.array.length];

       //遍历原来的哈希桶

       //最外层循环 控制数组下标

       for (int i = 0; i < this.array.length; i++) {

           Node cur = array[i];

           Node curNext = null;

           while (cur != null) {

               //记录cur.next

               curNext = cur.next;

               //在新的数组里面的下标

               int index = cur.key % newArray.length;

               //进行头插法

               cur.next = newArray[index];

               newArray[index] = cur;

               cur = curNext;

           }

       }

       this.array = newArray;

   }

}



性能分析

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


哈希表和Java类集的关系

HashMap 和 HashSet 即 java 中利用哈希表实现的 Map 和 Set

java 中使用的是哈希桶方式解决冲突的

java 会在冲突链表长度大于一定阈值后,将链表转变为搜索树(红黑树)

java 中计算哈希值实际上是调用的类的 hashCode 方法,进行 key 的相等性比较是调用 key 的 equals 方法。所以如果要用自定义类作为 HashMap 的 key 或者 HashSet 的值,必须覆写 hashCode 和 equals 方 法,而且要做到 equals 相等的对象,hashCode 一定是一致的。


相关实践学习
【AI破次元壁合照】少年白马醉春风,函数计算一键部署AI绘画平台
本次实验基于阿里云函数计算产品能力开发AI绘画平台,可让您实现“破次元壁”与角色合照,为角色换背景效果,用AI绘图技术绘出属于自己的少年江湖。
从 0 入门函数计算
在函数计算的架构中,开发者只需要编写业务代码,并监控业务运行情况就可以了。这将开发者从繁重的运维工作中解放出来,将精力投入到更有意义的开发任务上。
相关文章
|
5月前
|
Java API 微服务
2025 年 Java 从入门到精通学习笔记全新版
《Java学习笔记:从入门到精通(2025更新版)》是一本全面覆盖Java开发核心技能的指南,适合零基础到高级开发者。内容包括Java基础(如开发环境配置、核心语法增强)、面向对象编程(密封类、接口增强)、进阶技术(虚拟线程、结构化并发、向量API)、实用类库与框架(HTTP客户端、Spring Boot)、微服务与云原生(容器化、Kubernetes)、响应式编程(Reactor、WebFlux)、函数式编程(Stream API)、测试技术(JUnit 5、Mockito)、数据持久化(JPA、R2DBC)以及实战项目(Todo应用)。
351 6
|
2月前
|
小程序 Java 知识图谱
Java 学习笔记 —— BMI & BMR 计算器
这是一个使用 Java 编写的 BMI 与 BMR 计算器小程序,可输入年龄、性别、身高和体重,计算身体质量指数(BMI)和基础代谢率(BMR),并输出健康评估结果。通过该项目,掌握了 Java 的输入处理、数据验证、条件判断、数学运算及格式化输出等基础知识,是 Java 初学者的理想练习项目。
|
2月前
|
Java
Java 数组学习笔记
本文整理Java数组常用操作:遍历、求和、查找、最值及二维数组行求和等典型练习,涵盖静态初始化、元素翻倍、去极值求平均等实例,帮助掌握数组基础与应用。
|
7月前
|
前端开发 Java
java实现队列数据结构代码详解
本文详细解析了Java中队列数据结构的实现,包括队列的基本概念、应用场景及代码实现。队列是一种遵循“先进先出”原则的线性结构,支持在队尾插入和队头删除操作。文章介绍了顺序队列与链式队列,并重点分析了循环队列的实现方式以解决溢出问题。通过具体代码示例(如`enqueue`入队和`dequeue`出队),展示了队列的操作逻辑,帮助读者深入理解其工作机制。
246 1
|
8月前
|
存储 Java
# 【Java全栈学习笔记-U1-day02】变量+数据类型+运算符
本篇笔记主要围绕Java全栈学习的第二天内容展开,涵盖了变量、数据类型、运算符以及Scanner类的应用。首先介绍了变量的概念与命名规范,以及如何定义和使用变量;接着详细讲解了Java中的基本数据类型,包括整型、浮点型、字符型、布尔型等,并通过实例演示了数据类型的运用。随后,深入探讨了各类运算符(赋值、算术、关系、逻辑)及其优先级,帮助理解表达式的构成。最后,介绍了如何利用Scanner类实现用户输入功能,并通过多个综合示例(如计算圆面积、购物打折、变量交换及银行利息计算)巩固所学知识。完成相关作业将进一步加深对这些基础概念的理解与实践能力。
153 13
|
10月前
|
存储 算法 安全
探究‘公司禁用 U 盘’背后的哈希表算法与 Java 实现
在数字化办公时代,信息安全至关重要。许多公司采取“禁用U盘”策略,利用哈希表算法高效管理外接设备的接入权限。哈希表通过哈希函数将设备标识映射到数组索引,快速判断U盘是否授权。例如,公司预先将允许的U盘标识存入哈希表,新设备接入时迅速验证,未授权则禁止传输并报警。这有效防止恶意软件和数据泄露,保障企业信息安全。 代码示例展示了如何用Java实现简单的哈希表,模拟公司U盘管控场景。哈希表不仅用于设备管理,还在文件索引、用户权限等多方面助力信息安全防线的构建,为企业数字化进程保驾护航。
|
5月前
|
存储 安全 Java
Java 集合面试题从数据结构到 HashMap 源码剖析详解及长尾考点梳理
本文深入解析Java集合框架,涵盖基础概念、常见集合类型及HashMap的底层数据结构与源码实现。从Collection、Map到Iterator接口,逐一剖析其特性与应用场景。重点解读HashMap在JDK1.7与1.8中的数据结构演变,包括数组+链表+红黑树优化,以及put方法和扩容机制的实现细节。结合订单管理与用户权限管理等实际案例,展示集合框架的应用价值,助你全面掌握相关知识,轻松应对面试与开发需求。
295 3
|
7月前
|
存储 Java 编译器
Java 中 .length 的使用方法:深入理解 Java 数据结构中的长度获取机制
本文深入解析了 Java 中 `.length` 的使用方法及其在不同数据结构中的应用。对于数组,通过 `.length` 属性获取元素数量;字符串则使用 `.length()` 方法计算字符数;集合类如 `ArrayList` 采用 `.size()` 方法统计元素个数。此外,基本数据类型和包装类不支持长度属性。掌握这些区别,有助于开发者避免常见错误,提升代码质量。
758 1
|
8月前
|
开发框架 Java 开发工具
【Java全栈学习笔记-U1-day01】Java介绍
本笔记整理了Java学习的基础内容,涵盖程序理解、Java语言特性、JDK安装与配置、Java程序开发工具及编写步骤。重点介绍了Java程序的基本结构、编译和运行过程,以及输出语句的使用。通过实例演示了IDEA创建Java程序的方法,并强调了编码规范和注意事项。适合初学者复习和交流学习。 主要内容: 1. 理解程序:计算机组成、程序定义。 2. 简介:Java语言特点、技术平台、JDK作用。 3. 编写Java程序:编写、编译、运行步骤,基本结构。 4. 输出语句 5. DEA使用:新建工程、保存位置、文件介绍、新建类。 6. 扩展:注释、代码规范、大小写敏感、缩进等。
|
11月前
|
存储 缓存 安全
Java 集合江湖:底层数据结构的大揭秘!
小米是一位热爱技术分享的程序员,本文详细解析了Java面试中常见的List、Set、Map的区别。不仅介绍了它们的基本特性和实现类,还深入探讨了各自的使用场景和面试技巧,帮助读者更好地理解和应对相关问题。
179 5

热门文章

最新文章

下一篇
oss云网关配置