数据结构:哈希表(二)

news2025/4/1 5:24:22

目录

一、哈希表

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值了

 


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

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

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

相关文章

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&…

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

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

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

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

JS:页面事件

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

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

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

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

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

纯手工搭建整套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…

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

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

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

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

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

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

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

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

LeetCode 热题 100_搜索二维矩阵(64_74_中等_C++)(二分查找)(暴力破解法;Z字形查找;一次二分查找)

LeetCode 热题 100_搜索二维矩阵&#xff08;64_74&#xff09; 题目描述&#xff1a;输入输出样例&#xff1a;题解&#xff1a;解题思路&#xff1a;思路一&#xff08;暴力破解法&#xff09;&#xff1a;思路二&#xff08;Z字形查找&#xff09;&#xff1a;思路三&#x…

学习量化交易的环境安装记录

1、安装anaconda 因为使用python&#xff0c;需要安装anaconda&#xff0c;具体是下面的官方地址&#xff0c;根据自己需要下载相应的版本 https://www.anaconda.com/download 运行上面下载的文件&#xff0c;安装anaconda 可以根据自己需要安装到相应的盘上面 同时环境变量…

MySQL数据库入门到大蛇尚硅谷宋红康老师笔记 高级篇 part 1

第01章_Linux下MySQL的安装与使用 首先在vmware中下载centos7&#xff0c;实际上8更好一点&#xff0c;不过centos已经是时代的眼泪了&#xff0c;我之前已经教过了&#xff0c;不过是忘了&#xff0c;所以重新说一遍&#xff0c;看文档即可 2.开机前修改mac地址 &#xff0…

基于AVue的二次封装:快速构建后台管理系统的CRUD方案

基于AVue的二次封装&#xff1a;快速构建后台管理系统的CRUD方案 在开发后台管理系统时&#xff0c;表格是常见的组件之一。然而&#xff0c;使用原生的Element Plus实现CRUD&#xff08;增删改查&#xff09;功能往往需要编写大量重复代码&#xff0c;过程繁琐。即使借助类似…

第6章:基于LangChain如何开发Agents,附带客户支持智能体示例

本文主要介绍了 LangChain4j 中的 Agent&#xff08;代理&#xff09; 概念&#xff0c;以及如何使用 LangChain4j 构建代理系统&#xff0c;重点提供了一个客户支持系统的智能体样例 代理&#xff08;Agents&#xff09;| LangChain4j 注意&#xff1a; 请注意&#xff0c;“A…

传统的自动化行业的触摸屏和上位机,PLC是否会被取代?

传统的自动化行业的触摸屏和上位机是否会被取代&#xff1f; 在工业自动化领域&#xff0c;触摸屏和上位机长期扮演着核心角色&#xff0c;尤其在污水处理、化工生产等场景中&#xff0c;它们通过实时数据采集、逻辑控制、报警联动等功能&#xff0c;保障了生产设备的稳定运行…