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

数据结构:哈希表(二)

目录

一、哈希表

1、概念

二、哈希冲突

1、概念

2、冲突避免

(1)哈希函数设计

(2)负载因子调节

3、冲突解决

(1)闭散列

1、线性探测

2、二次探测

(2)开散列

4、哈希桶实现

(1)put(int key,int value)

(2)get(int key)

5、和java类集的关系


一、哈希表

1、概念

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

顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(logN),搜索的效率取决于搜索过程中元素的比较次数。


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


而当我们向上述的结构中进行:

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

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

能够实现这样的操方式即为我们的哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(HashTable)(或者称散列表)


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

二、哈希冲突

1、概念

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

2、冲突避免

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

(1)哈希函数设计

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

哈希函数设计原则:

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

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

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

常用哈希函数:

1、直接定制法

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

优点:简单、均匀

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

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

2、除留余数法

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

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

(2)负载因子调节

负载因子 = 填入表中元素的个数 / 散列表的长度

负载因子与“填入表中的元素个数”成正比,所以负载因子越大,表明填入表中的元素越多,产生冲突的可能性就越大,反之,负载因子越小,标明填入表中的元素越少,产生冲突的可能性就越小。

正因是这样的性质,所以我们可以去增大我们散列表的长度,让我们的负载因子变小,而负载因子变小了,代表我们能填入的元素个数就增加了

3、冲突解决

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

(1)闭散列

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

那我们如何寻找下⼀个空位置呢?这就引出了两个新的方法

1、线性探测

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

通过哈希函数获取待插入元素在哈希表中的位置 , 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测 找到下一个空位置,插入新元素

 

而线性探测是有缺陷的,他会导致产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式 就是挨着往后逐个去找,而我们的接下来的二次探测就解决了这种缺陷。

2、二次探测

找下一个空位置的方法为:Hi = (H0+i^2)%m 或Hi=(H0-i^2)%m 其中 i= 1,2,3……,H0是通过对元素的关键码 key 进行哈希函数计算得到的位置,m是散列表的大小。

 

(2)开散列

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

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

4、哈希桶实现

我们的哈希表是一个key-value模型,还是一个节点数组,并且他是带有负载因子的

public class HashBucket {public static class Node{public int key;public int val;Node next;public Node(int key,int val){this.key = key;this.val = val;}}public Node[] arr;public int usesize;public static final double LOAD_FACTOR = 0.75;
}

 

(1)put(int key,int value)

1、使用哈希函数计算出key的哈希地址


2、检查我们得到的哈希地址,去数组中查找key这个值是否存在,存在更新对应的value值


 

3、如果不存在我们就利用头插法在这个哈希地址处将这个结点插入,同时让我们的有效长度usesize++


4、之后我们要判断是否超过了我们的负载因子,如果超过了我们就要进行扩容


5、扩容后我们会发现,我们有的元素可以放到新的位置处,所以我们需要进行重新哈希,在重新哈希的过程中再次利用头插法进行插入元素,但是如果在同一位置处有多个元素,我们将第一个元素放入新的位置(cur.next),我们原本的cur下一个元素就会无法找到因此,我们需要将原本cur.next记录下来。


 

public void put(int key,int val){//计算哈希地址int index = key% arr.length;//判断是否出现相同的key值,出现就更新value值Node cur = arr[index];while(cur != null){if(cur.key == key){cur.val = val;return;}cur = cur.next;}//如果没有key值,使用头插法添加结点Node newNode = new Node(key,val);newNode.next = arr[index];arr[index] = newNode;usesize++;//增加有效长度//判断负载因子if(loadFactor() > LOAD_FACTOR){resize();//超过负载因子的值就进行扩容}}//计算负载因子private double loadFactor(){return usesize * 1.0 / arr.length;//由于是小数要乘以1.0}//扩容private void resize(){Node[] newarr = new Node[arr.length*2];//以2倍扩容//利用头插法重新哈希for(int i = 0; i < arr.length;i++){Node cur = arr[i];while(cur != null){int newindex = cur.key % newarr.length;Node curN = cur.next;cur.next = newarr[newindex];newarr[newindex] = cur;cur = curN;}}arr = newarr;//让arr指向新的数组}

 

(2)get(int key)

这个方法就很简单了,要我们根据key得到对应的value值,首先我们还是计算处哈希地址,根据得到的哈希地址,去查找,如果存在这个key值就返回他所对应的value值。

public int get(int key){int index = key % arr.length;Node cur = arr[index];while(cur != null){if(cur.key == key){return cur.val;}cur = cur.next;}return -1;}

 

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

5、和java类集的关系

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

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

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

     数组长度大于64, 并且链表长度大于8

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


 

像是我们的第4个关系如果我们传入的key值是一个字符串,我们上述实现的哈希桶就就无法使用的,那么我们是否可以创建一个通用的哈希桶呢,这当然是可以的,我们之前学习过泛型,可以根据我们传入类型的不同进行改变

public class HashBucket1<K,V> {public static class Node<K,V>{public K key;public V val;Node<K,V> next;public Node(K key,V val){this.key = key;this.val = val;}}public Node<K,V>[] arr =(Node<K, V>[]) new Node[10];public int usesize;public static final double LOAD_FACTOR = 0.75;public void put(K key,V val){//计算哈希地址int hashcode = key.hashCode();int index = hashcode % arr.length;//判断是否出现相同的key值,出现就更新value值Node<K,V> cur = arr[index];while(cur != null){if(cur.key.equals(key)){cur.val = val;return;}cur = cur.next;}//如果没有key值,使用头插法添加结点Node<K,V> newNode = new Node(key,val);newNode.next = arr[index];arr[index] = newNode;usesize++;//增加有效长度//判断负载因子if(loadFactor() > LOAD_FACTOR){resize();//超过负载因子的值就进行扩容}}//计算负载因子private double loadFactor(){return usesize * 1.0 / arr.length;//由于是小数要乘以1.0}//扩容private void resize(){Node<K,V>[] newarr =(Node<K, V>[]) new Node[arr.length*2];//以2倍扩容//利用头插法重新哈希for(int i = 0; i < arr.length;i++){Node<K, V> cur = arr[i];while(cur != null){int hashcode = cur.key.hashCode();int newindex = hashcode % newarr.length;Node<K, V> curN = cur.next;cur.next = newarr[newindex];newarr[newindex] = cur;cur = curN;}}arr = newarr;//让arr指向新的数组}public V get(K key){int hashcode = key.hashCode();int index = hashcode % arr.length;Node<K,V> cur = arr[index];while(cur != null){if(cur.key.equals(key)){return cur.val;}cur = cur.next;}return null;}

但是在这里我们发现一件事,如果我们有两个引用他们的key值是相同的,这时我们put一个引用,但是我们用另一个引用去获得value值时我们却得不到

这时因为虽然两个引用的值时相同的但是他们的地址是不同的,然而我们要实现的哈希桶只有值是相同的就可以得到,所以我们要重写我们的hashCode()和equals()方法 

 重写之后我们就可以发现我们的person2也可以得到person1的value值了

 


好了,今天的分享就到这里了还请大家多多关注,我们下一篇见!

相关文章:

数据结构:哈希表(二)

目录 一、哈希表 1、概念 二、哈希冲突 1、概念 2、冲突避免 &#xff08;1&#xff09;哈希函数设计 &#xff08;2&#xff09;负载因子调节 3、冲突解决 &#xff08;1&#xff09;闭散列 1、线性探测 2、二次探测 &#xff08;2&#xff09;开散列 4、哈希桶实…...

blender笔记2

一、物体贴地 物体->变换->对齐物体 ->对齐弹窗(对齐模式&#xff1a;反方&#xff0c;相对于&#xff1a;场景原点&#xff0c;对齐&#xff1a;z)。 之后可以设置原点->原点--3d游标 二、面上有阴影 在编辑模式下操作过后&#xff0c;物体面有阴影。 数据-&g…...

1.21作业

1 unserialize3 当序列化字符串中属性个数大于实际属性个数时&#xff0c;不会执行反序列化 外部如果是unserialize&#xff08;&#xff09;会调用wakeup&#xff08;&#xff09;方法&#xff0c;输出“bad request”——构造url绕过wakeup 类型&#xff1a;public class&…...

深入浅出:理解闭包在JavaScript中的应用

什么是闭包 闭包&#xff08;Closure&#xff09;是 JavaScript 中的一个重要概念&#xff0c;也是函数式编程中的核心特性之一。简单来说&#xff0c;闭包是指一个函数能够访问并记住其词法作用域&#xff08;Lexical Scope&#xff09;&#xff0c;即使这个函数在其词法作用…...

【Quest开发】全身跟踪(一)

软件&#xff1a;Unity 2022.3.51f1c1、vscode、Meta XR All in One SDK V72 硬件&#xff1a;Meta Quest3 最终效果&#xff1a;能像meta的操作室沉浸场景一样根据头盔移动来推断用户姿势&#xff0c;实现走路、蹲下、手势匹配等功能 需要借助UnityMovement这个包 GitHub …...

【QT中的一些高级数据结构,持续更新中...】

QT中有一些很精妙、便捷的设计&#xff0c;在了解这些数据的同时&#xff0c;我们可以学到如何更好的设计代码。本贴持续更新中&#xff0c;欢迎关注和收藏 一 QScopedPointer主要特点&#xff1a;示例代码 二 Q_DISABLE_COPY 一 QScopedPointer QScopedPointer 是 Qt 中的一种…...

最新版本Exoplayer扩展FFmpeg音频软解码保姆级教程

ExoPlayer 是一个开源的 Android 媒体播放库&#xff0c;由 Google 开发和维护&#xff0c;用于替代 Android 系统自带的 MediaPlayer。它提供了更强大的功能、更好的性能和更高的灵活性&#xff0c;适用于各种复杂的媒体播放场景。所以被广泛用于各种播放器场景。 最近项目中…...

JS:页面事件

文章目录 一、页面加载事件二、页面滚动事件三、页面尺寸事件总结 一、页面加载事件 有时候我们会把script的内容放在body前&#xff0c;这时候代码的执行在元素的加载之前&#xff0c;会导致页面元素未加载而报错 解决办法是调用Window的load加载事件&#xff0c;将所有操作放…...

✨ 索引有哪些缺点以及具体有哪些索引类型

索引的定义与原理 索引是数据库中用于提高数据检索效率的数据结构。它就像是书籍的目录&#xff0c;通过目录可以快速定位到所需内容的页码&#xff0c;而在数据库中&#xff0c;索引可以帮助数据库系统快速找到符合查询条件的数据行&#xff0c;而不必对整个表进行扫描。 其…...

C++ ——继承

体现的是代码复用的思想 1、子类继承父类&#xff0c;子类就拥有了父类的特性&#xff08;成员方法和成员属性&#xff09; 2、已存在的类被称为“基类”或者“父类”或者“超类”&#xff1b;新创建的类被称为“派生类”或者“子类” 注意&#xff1a; &#xff08;1&#…...

vue,vue3 keepalive没有效果,无法缓存页面include无效,keep-alive

keepalive没有效果&#xff0c;无法缓存页面&#xff1f; 问题大概是组件的name值不对应&#xff0c;vue2修改组件文件的name值&#xff0c;vue3保持组件文件名称和路由页面配置的name一致就可以了&#xff0c;如果vue3不想保持一致&#xff0c;必须手动在文件后面添加export..…...

DeepSeek智能测试知识库助手PRO版:多格式支持+性能优化

前言 测试工程师在管理测试资产时,需要面对多种文档格式、大量文件分类及知识库的构建任务。为了解决这些问题,我们升级了 DeepSeek智能测试知识库助手,不仅支持更多文档格式,还加入了 多线程并发处理 和 可扩展格式支持,大幅提升处理性能和灵活性。 主要功能亮点: 多格…...

【ELK】【Elasticsearch】数据查询方式

1. 简单查询&#xff08;URI Search&#xff09; 通过 URL 参数直接进行查询&#xff0c;适合简单的搜索场景。 示例&#xff1a; bash 复制 GET /index_name/_search?qfield_name:search_value 说明&#xff1a; index_name&#xff1a;索引名称。 field_name&#xf…...

Kotlin 优雅的接口实现

1. 日常遇到的冗余的接口方法实现 日常开发中&#xff0c;经常会要实现接口&#xff0c;但是很多场景中&#xff0c;只需要用到其中一两个方法&#xff0c;例如 ActivityLifecycleCallbacks&#xff0c;它有很多个接口需要实现&#xff0c;但是很多时候我们只需要用到其中的一…...

go 通过ssh连接linux golang.org/x/crypto/ssh

ssh.Dial golang.org/x/crypto/ssh package mainimport ("bytes""log""os""strings""golang.org/x/term""golang.org/x/crypto/ssh" )// go ssh 连接ssh // 参考blog&#xff1a; // // https://www.cnblogs.c…...

纯手工搭建整套CI/CD流水线指南

目录 一、前言 二、环境准备 1、服务器开荒&#xff08;192.168.1.200&#xff09; 2、离线资源清单&#xff08;提前用U盘拷好&#xff09; 三、硬核安装&#xff1a;比拧螺丝还细的步骤 Step1&#xff1a;搭建GitLab&#xff08;注意&#xff01;这是只内存饕餮&#xf…...

智能硬件新时代,EasyRTC开启物联音视频新纪元

在万物互联的时代浪潮中&#xff0c;智能硬件正以前所未有的速度融入我们的生活&#xff0c;从智能家居的便捷控制&#xff0c;到智能穿戴设备的健康监测&#xff0c;再到工业物联网的高效管理&#xff0c;智能硬件的应用场景不断拓展。而在这个智能硬件蓬勃发展的背后&#xf…...

Rust编程语言入门教程(八)所有权 Stack vs Heap

Rust 系列 &#x1f380;Rust编程语言入门教程&#xff08;一&#xff09;安装Rust&#x1f6aa; &#x1f380;Rust编程语言入门教程&#xff08;二&#xff09;hello_world&#x1f6aa; &#x1f380;Rust编程语言入门教程&#xff08;三&#xff09; Hello Cargo&#x1f…...

spring 狂神说的详细笔记(完整版)

最近在B站找教程视频自学java框架&#xff08;SSM&#xff09;&#xff0c;最后发现自己迷上了狂神说&#xff0c;不得不说秦疆老师 讲得太好了&#xff0c;通俗易懂&#xff0c;而且在听他的课你会不由衷得到一些思想的启发和转变&#xff0c;而且教程视频 还是无偿免费的&…...

交易所开发:数字市场的核心动力

数字资产交易所作为连接用户与市场的核心枢纽&#xff0c;已成为推动数字经济发展的关键引擎。其开发不仅需要技术创新&#xff0c;还需兼顾用户体验、合规安全与生态构建&#xff0c;以下是交易所开发的核心要素与实践路径分析&#xff1a; 一、交易所的核心定位与技术架构…...

C++ 课程设计 汇总(含源码)

C 课程设计 [C课程设计 个人账务管理系统(含源码)](https://arv000.blog.csdn.net/article/details/145601695)[C课程设计 运动会分数统计&#xff08;含源码&#xff09;](https://arv000.blog.csdn.net/article/details/145601819)[C 课程设计打印万年历&#xff08;含源码&a…...

android调用ffmpeg解析rtsp协议的视频流

文章目录 一、背景二、解析rtsp数据1、C层功能代码2、jni层的定义3、app层的调用 三、源码下载 一、背景 本demo主要介绍android调用ffmpeg中的接口解析rtsp协议的视频流&#xff08;不解析音频&#xff09;&#xff0c;得到yuv数据&#xff0c;把yuv转bitmap在android设备上显…...

Android 之 AIDL for HAL

Android AIDL for HAL 的作用与实现 作用&#xff1a; Android AIDL for HAL&#xff08;Android Interface Definition Language for Hardware Abstraction Layer&#xff09;旨在统一 HAL 开发接口&#xff0c;替代 HIDL&#xff08;Hardware Interface Definition Language…...

Jmeter进阶篇(34)如何解决jmeter.save.saveservice.timestamp_format=ms报错?

问题描述 今天使用Jmeter完成压测执行,然后使用命令将jtl文件转换成html报告时,遇到了报错! 大致就是说jmeter里定义了一个jmeter.save.saveservice.timestamp_format=ms的时间格式,但是jtl文件中的时间格式不是标准的这个ms格式,导致无法正常解析。对于这个问题,有如下…...

TensorFlow v2.16 Overview

TensorFlow v2.16 Overview 一、模块 Modules二、类 Classes三、函数 Functions TensorFlow v2.16.1 Overview 一、模块 Modules 模块是TensorFlow中组织代码的一种方式&#xff0c;将相关的功能和类封装在一起&#xff0c;方便用户使用和管理。每个模块都提供了特定领域的公共…...

Navicat17详细安装教程(附最新版本安装包和补丁)2025最详细图文教程安装手册

目录 前言&#xff1a;为什么选择Navicat 17&#xff1f; 一、下载Navicat17安装包 二、安装Navicat 1.运行安装程序 2.启动安装 3.同意“协议” 4.设置安装位置 5.创建桌面图标 6.开始安装 7.安装完成 三、安装补丁 1.解押补丁包 2.在解压后的补丁包目录下找到“w…...

机器视觉检测中,2D面阵相机和线扫相机的区别

2D面阵相机和线扫相机是工业视觉系统中常用的两种相机类型&#xff0c;各有其特点和应用场景。 2D面阵相机 特点&#xff1a; 成像方式&#xff1a;通过二维传感器一次性捕捉整个场景的图像。 分辨率&#xff1a;分辨率由传感器的像素数决定&#xff0c;常见的有百万像素到几千…...

【接口封装】——13、登录窗口的标题栏内容设置

解释&#xff1a; 1、封装内容&#xff1a;图标、文本内容、宽度 2、ui.iconLabel&#xff1a;在UI文件中的自定义命名 3、引入头文件&#xff1a;#include<qpixmap.h> 函数定义&#xff1a; #pragma once#include <QWidget> #include "ui_TitleBar.h"cl…...

一文详解U盘启动Legacy/UEFI方式以及GPT/MBR关系

对于装系统的老手而说一直想研究一下装系统的原理&#xff0c;以及面对一些问题时的解决思路&#xff0c;故对以前的方法进行原理上的解释&#xff0c;主要想理解其底层原理。 引导模式 MBR分区可以同时支持UEFI和Legacy引导&#xff0c;我们可以看一下微pe制作的启动盘&#…...

CMake管理依赖实战:多仓库的无缝集成

随着软件复杂度的增加&#xff0c;单个项目可能需要依赖多个外部库或模块。这些依赖项可能是来自不同的代码仓库&#xff0c;如ATest和BTest。为了实现高效的依赖管理&#xff0c;CMake提供了多种方式来处理这种多仓库的情况。下面我们将详细介绍几种常见的方法&#xff0c;并通…...