【数据结构】关于哈希表内部原理,你到底了解多少???(超详解)

news2024/9/23 3:31:27

前言:

🌟🌟本期讲解关于哈希表的内部实现原理,希望能帮到屏幕前的你。

🌈上期博客在这里:http://t.csdnimg.cn/7D225

🌈感兴趣的小伙伴看一看小编主页:GGBondlctrl-CSDN博客

目录

 📚️1.哈希表的概念

📚️2.哈希-冲突

2.1冲突-概念

 2.2冲突-避免

1.冲突-避免-哈希函数设计

2.冲突-避免-冲突因子

2.3冲突-解决

1.冲突-解决-闭散列

2.冲突-解决-开散列

 2.4性能分析

2.5与Java类集的关系

 📚️3.总结


 📚️1.哈希表的概念

       顺序结构以及平衡树中在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N);

      平衡树中为树的高度,即O(logN ),搜索的效率取决于搜索过程中元素的比较次数。例如上期的treeMap.

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

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


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

 哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。

图解 :

注意:这里和计数排序差不多 ,但是如果我们加入11,会发现11的位置将直接把1给覆盖掉,那么此时就叫作哈希-冲突。

📚️2.哈希-冲突

2.1冲突-概念

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

 2.2冲突-避免

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

1.冲突-避免-哈希函数设计

哈希函数设计原理:

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


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


• 哈希函数应该比较简单

小编这里介绍两个比较常用的两个方法: 

1. 直接定制法
       取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B 优点:简单、均匀 缺点:需要事先知道关键字的分布情况 使用场景:适合查找比较小且连续的情况。

例如字符串中第一个只出现一次字符

代码实例如下:

class Solution {
    public int firstUniqChar(String s) {
        int[] array=new int[26];
        for(int i=0;i<s.length();i++){
            char ch=s.charAt(i);
            array[ch-'a']++;
        }
        for(int j=0;j<s.length();j++){
            if(array[s.charAt(j)-'a']==1){
                return j;
            }
        }
        return -1;
    }
}

思路:就是创建一个26个空间大小的数组,在遍历字符串时,将字符对应的位置加一,最后再次遍历谁为1,就返回第一个不重复的字符。 

注意:这种方法只适合要求范围小的数值内进行操作,如果范围过大,则会造成数组空间的浪费。

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

2.冲突-避免-冲突因子

负载因子与冲突率图片演示:

 

 已知哈希表中已有的关键字个数是不可变的,那我们能调整的就只有哈希表中的数组的大小。(我们不可能在源头上限制我们的需求)

2.3冲突-解决

1.冲突-解决-闭散列

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

线性探测:

这里就是,当我们先插入了一个1后,然后想插入11,此时发现取余地址冲突了,此时我们就要往后寻空位置,发现后进行插入。

图片演示:

注意:

      不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。

      产生冲突的数据堆积在一块

二次探测

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

图片演示:

当然这是一个极端的例子~~~ 

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

2.冲突-解决-开散列

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

图解如下:

 注意:刚才我们提到了,哈希桶其实可以看作将大集合的搜索问题转化为小集合的搜索问题了,那如果冲突严重,就意味着小集合的搜索性能其实也时不佳的,这个时候我们就可以将这个所谓的小集合搜索问题继续进行转化,例如:
1. 每个桶的背后是另一个哈希表
2. 每个桶的背后是一棵搜索树

 开散列的代码实现模拟:

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 void put(int key,int value){

       /* int index=key% array.length;
        //遍历链表
        //头插法
        Node cur=array[index];
        while (cur!=null){
            if(cur.key==key){
                cur.val=value;
                return;
            }
            cur=cur.next;
        }
        Node node=new Node(key,value);
        node.next=array[index];
        array[index]=node;
        usedSize++;*/
        //尾插
        Node node=new Node(key,value);
        int index=key% array.length;
        if(array[index]==null){
            array[index]=node;
            return;
        }
        Node cur=array[index];
        Node prev=null;
        while (cur!=null){
            if(cur.key==key){
                cur.val=value;
                return;
            }
            prev=cur;
            cur=cur.next;
        }
        prev.next=node;
        usedSize++;
        //负载因子是否大于0.75
        if(loadFactor()>0.75){
            resize();
        }
    }
    public int loadFactor(){
        return usedSize/ array.length;
    }
    //进行扩容
    public void resize(){
        Node[] tmpArray=new Node[array.length*2];
        for (int i = 0; i < array.length; i++) {
            Node cur=array[i];
            while (cur!=null){
                Node newCur=cur.next;
                int newIndex=cur.key% tmpArray.length;
                //进行头插法
                cur.next=tmpArray[newIndex];
                tmpArray[newIndex]=cur;
                cur=newCur;
            }
        }
        array=tmpArray;
    }
    //进行取出
    public int get(int key){
        //判断k在哪里
        int index=key%array.length;
        Node cur=array[index];
        while (cur!=null){
            if(cur.key==key){
               return cur.val;
            }
            cur=cur.next;
        }
        return -1;
    }

这里小编模拟了哈希表开散列的放入数据的两种插入方式,即链表的头插法以及链表的尾插法。

注意扩容:

1.进行原来数值的项管部位置的插入,因为扩容后的数组容量大小进行变化,必须进行重新取余。

2.在进行在新的扩容数组后,使用的cur要进行保留,否则进行新的数组插入后,cur无法回到原来数组进行遍历剩余的数值遍历。 

 2.4性能分析

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

1.每个桶中的链表的长度是一个常数,并且可以进行调整。

2.负载因子的存在,使得在遍历时可以进数值过多的扩容。

2.5与Java类集的关系

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


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


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


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

 📚️3.总结

💬💬本期小编讲解了关于哈希表的内部原理,以及它的重点内部原理冲突的避免以及冲突的解决。

本期主要是解释性语言较多,注重理解,唯一的难点是开散列的模拟代码实现。

🌅🌅🌅~~~~最后希望与诸君共勉,共同进步!!!


                               💪💪💪以上就是本期内容了, 感兴趣的话,就关注小编吧。

                                                         😊😊  期待你的关注~~~

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

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

相关文章

安嘉空间:智慧科技守护空间健康

在当今社会&#xff0c;随着人们对生活质量要求的不断提升&#xff0c;室内环境的健康与安全问题日益受到重视。安嘉空间&#xff0c;作为一家致力于人居健康空间技术研发的高科技企业&#xff0c;以其独创的技术和卓越的产品&#xff0c;为广大用户提供了一套全面的空间健康解…

进销存自动统计单据表格——未来之窗行业应用跨平台架构

一、代码 function 未来之窗_人工智能_计算单据(objbyclass,obj目标显示,obj目标值){console.log("未来之窗_人工智能_计算单据"objbyclass);var 计算结果0;$("."objbyclass).each(function(){var 输入框值 $(this).val();console.log("输入框值"…

去中心化(Decentralization)

去中心化&#xff08;Decentralization) 并不是一个新概念&#xff0c;它已在战略、管理和政府中使用了很长时间。去中心化的基本思想是将控制权和权限分配给组织的外围&#xff0c;而不是由一个中心机构完全控制组织。这种配置为组织带来了许多好处&#xff0c;例如提高了效率…

欧洲用户对中国应用程序的感知:一个复杂的挂毯

在数字时代&#xff0c;中国应用程序迅速在全球范围内占有一席之地&#xff0c;吸引了全球用户的注意力和好奇心。在欧洲&#xff0c;这些应用程序引发了人们的兴趣、阴谋和担忧&#xff0c;不同国家和人口统计数据的看法差异很大。 对中国应用程序感兴趣的主要驱动力之一是它…

git命令使用详情

目录 一. 安装教程 二. git配置 1. 查看git配置参数 2. 设置邮箱和用户名 3. SSH配置 4. 配置git远程库公钥 5. 编码设置 三. git 提交流程 1. 整体操作流程图 2. Git仓库包含5个区域 3. 下载、提交、更新命令 3.1. 下载 3.2. 提交 3.3. 更新&#xff08;两种方式…

无人机 PX4 飞控 | ROS应用层开发:基础代码框架构建

无人机 PX4 飞控 | ROS应用层开发&#xff1a;基础代码框架构建 基础代码框架构建文件建立代码基本构建测试 基础代码框架构建 本篇博客拟在构建一个 无人机 PX4 飞控 ROS应用层开发 的 基础代码框架。 其中包含了基础类文件、类头文件、main主函数文件&#xff0c;及其编译所…

数据结构-c/c++实现栈(详解,栈容量可以动态增长)

一.栈的基本介绍 栈是一种只能够在一端进行插入和删除的顺序表。如下图 空栈&#xff1a;表示不含任何元素的栈 栈顶&#xff1a;表示允许进行插入和删除元素的一端 栈底&#xff1a;表示不允许进行插入和删除元素的一端 即栈是一种后进先出的线性表数据结构 二.栈的常见操…

Redis高级---面试总结之内存过期策略及其淘汰策略

目前已更新系列&#xff1a; 当前&#xff1a;Redis高级---面试总结之内存过期策略及其淘汰策略 并发编程之----线程池ThreadPoolExecutor,Excutors的使用及其工作原理 Redis高级----主从、哨兵、分片、脑裂原理-CSDN博客 计算机网络--面试知识总结一 计算机网络-----面试知…

【深度学习】yolov8的微调

yolov8的集成度太高了&#xff0c;除了config的哪些参数以外&#xff0c;需要更精细的微调。 比如这里&#xff1a; https://github.com/ultralytics/ultralytics/blob/main/ultralytics/utils/tuner.py 应用场景&#xff0c;交通标志的向左转&#xff0c;向右转之类的&#x…

Golang | Leetcode Golang题解之第384题打乱数组

题目&#xff1a; 题解&#xff1a; type Solution struct {nums, original []int }func Constructor(nums []int) Solution {return Solution{nums, append([]int(nil), nums...)} }func (s *Solution) Reset() []int {copy(s.nums, s.original)return s.nums }func (s *Solu…

STM32G474之HAL_Delay()会导致死机的原因

使用HAL库需要HAL_Delay()函数支持&#xff0c;离开这个函数&#xff0c;和这个函数有关的函数就无法调用了。遇到这个问题&#xff0c;我们肯定会否定HAL库&#xff0c;也确实它是有点差&#xff0c;但也有好的一面。 为什么HAL库初始化了SysTick定时器&#xff0c;也使能了相…

书生大模型实战营(1)——InterStudio基础知识+Vscode SSH连接远程服务器+Linux基础指令

参加书生.浦江大模型实战训练营&#xff0c;学习大模型知识和微调技术&#xff0c;所有课程免费&#xff0c;通过闯关的形式学习&#xff0c;也比较有趣。一起来了解LLM的世界。邀请链接 产品简介 InternStudio 是大模型时代下的云端算力平台。基于 InternLM 组织下的诸多算法…

Android Handler消息机制完全解析(二)

欢迎转载&#xff0c;转载请注明出处&#xff1a;https://blog.csdn.net/dmk877/article/details/141690289 在阅读本篇博客之前建议先阅读此篇 Android Handler消息机制完全解析(一) 在看Looper源码之前必须要理解ThreadLocal&#xff0c;ThreadLocal在Handler消息机制中起到…

信息安全--(五)物理与环境安全技术(二)机房安全分析与防护

在使用本博客提供的学习笔记及相关内容时&#xff0c;请注意以下免责声明&#xff1a;信息准确性&#xff1a;本博客的内容是基于作者的个人理解和经验&#xff0c;尽力确保信息的准确性和时效性&#xff0c;但不保证所有信息都完全正确或最新。非专业建议&#xff1a;博客中的…

FlowUs 小程序:开启高效之旅,订阅内容超精彩

一、丰富多样的订阅选择 FlowUs 小程序提供了极为丰富的订阅内容。无论你是对知识管理、项目管理感兴趣&#xff0c;还是专注于创意写作、时间规划&#xff0c;都能在这里找到适合自己的订阅选项。从专业的行业资讯到实用的技巧指南&#xff0c;从激发灵感的创意案例到深入的学…

业务解耦-Spring事件监听的三种实现方式

实现ApplicationListener 步骤如下&#xff1a; 1.写Event类&#xff0c;需要继承Spring的ApplicationEvent类 2.写监听类&#xff0c;需要实现Spring的ApplicationListener接口&#xff0c;加上Component注解 3.监听类实现onApplicationEvent方法 4.通过ApplicationContext.p…

开放大世界的全局寻路

开放大世界的寻路一直是很困扰我的一个点&#xff0c;地图大、还是动态可变的&#xff0c;所以寻路会有很多要求。就我们项目来讲&#xff0c;有这几个要求&#xff1a; 能满足极大范围的地图&#xff1b; 地图寻路数据能实时构建&#xff0c;且重建代价很小&#xff1b; 寻路的…

地质灾害监测预警系统的作用

在地球的广阔舞台上&#xff0c;自然灾害如同不可预测的演员&#xff0c;时常上演着惊心动魄的剧目。地震的震撼、滑坡的肆虐、泥石流的咆哮&#xff0c;这些地质灾害不仅给人类生命财产带来巨大威胁&#xff0c;也考验着社会的防灾减灾能力。为了应对这一挑战&#xff0c;地质…

【Linux】在 bash shell 环境下,当一命令正在执行时,按下 control-Z 会?

目录 题目分析答案 题目 分析 ctrl-c&#xff1a; 发送 SIGINT 信号给前台进程组中的所有进程。常用于终止正在运行的程序&#xff1b;ctrl-z&#xff1a; 发送 SIGTSTP信号给前台进程组中的所有进程&#xff0c;常用于挂起一个进程&#xff1b;ctrl-d&#xff1a; 不是发送信…

乐城堡 JoyCastle Unity岗位笔试题

1)实现 move(GameObjct gameObject, Vector3 begin, Vector3 end, float time, bool pingpong){ } 使 gameObject 在 time 秒内&#xff0c;从 begin 移动到 end&#xff0c;若 pingpong 为 true&#xff0c;则在结束时 使 gameObject 在 time 秒内从 end 移动到 begin&#xf…