哈希表超详解

news2024/12/28 22:16:37

目录

哈希表

概念

冲突-概念

冲突-避免

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

冲突-避免-负载因子的调节

冲突-解决-闭散列

冲突-解决-开散列

哈希桶的实现

 性能分析

java和类集的关系


哈希表

概念

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

因此我们就会想,有没有一种理想的方法,可以不经过任何比较,一次从表中得到要搜索的元素那么就可以构造某种函数,使该元素的存储位置与关键码之间存在映射关系,(即key->通过某种方法->一次定位到key的位置),那么这种通过函数的方式就很容易找到元素

当向该结构中:

插入元素

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

搜索元素

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

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

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

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

存储情况如下:

用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快。而此时又衍生出来了一个问题,万一两个关键码通过函数计算的存放位置相同,该怎么办?这就涉及到了冲突。

冲突-概念

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

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

冲突-避免

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

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

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

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

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

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

常见哈希函数(常用)

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

2.除留余数法:设散列表允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p

作为除数,按照哈希函数: Hash(key) = key% p(p<=m), 将关键码转换成哈希地址
注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突

冲突-避免-负载因子的调节

散列表的载荷因子定义为 : a = 填入表中元素个数 / 散列表的长度

注:由于散列表长度是定值,所以填入表中的元素个数越多,产生冲突的可能性就越大。

对于开放定址法,荷载因子是特别重要的因素,应该严格限制在0.7-0.8以下,超过0.8,查表时CPU缓存不命中按照指数直线上升。因此,一些采用开放定址法的hash库,如Java系统库限制了荷载因子为0.75,如果超过荷载因子的话将对散列表进行扩容。

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

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

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

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

冲突-解决-闭散列

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

1.线性探测

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

线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
插入
通过哈希函数获取待插入元素在哈希表中的位置
如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素

 

采用闭散列处理哈希冲突时不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其它元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能有影响。因此线性探测采用伪删除法来删除一个元素 

2.二次探测

线性探测的缺陷是产生的数据堆积在一块(导致不能均匀分布在空间中),这与其找下一个空位置有关系,因为找空位置的方式就是挨个往后逐个去找 ,因此二次探测为了避免该问题,找下一个空位置的方法为:Hi = (H0 + i ^ 2) % m.H0为应该放置的位置,m为冲突次数,m为表的大小。

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

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

冲突-解决-开散列

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

方法如图所示:

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

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

哈希桶的实现

 下面是基于key-value模型写的哈希桶部分方法的代码:

//key-value模型
public class HashBucket {
    private static class Node {
        private int key;
        private int value;
        Node next;

        public Node(int key, int value) {
            this.key = key;
            this.value = value;
        }
    }

    //用数组表示哈希表
    private Node[] array;
    //当前哈希表元素个数
    private int size;
    //定义荷载因子
    private static final double LOAD_FACTOR = 0.75;

    public int put(int key, int value) {
        //根据哈希函数确定存放的下标
        int index = key % array.length;

        //在链表中查找key所在的结点
        //如果找到了,更新
        //所有节点都不是key,插入一个新的结点
        for (Node cur = array[index]; cur != null; cur = cur.next) {
            if(key == cur.key) {
                int oldValue = cur.value;
                cur.value = value;
                //返回更新前key对应的value
                return oldValue;
            }
        }
        //链表遍历完成,没有找到这个key
        Node node = new Node(key, value);
        node.next = array[index];
        array[index] = node;
        size++;

        if(loadFactor() >= LOAD_FACTOR) {
            resize();
        }
        return -1;
    }

    private void resize() {
        //创建一个扩容数组,并将原来数组中的元素按照新的规则放入新的数组当中
        Node[] newArray = new Node[array.length * 2];
        //遍历原来的数组
       for(int i = 0; i < array.length; i++) {
           //遍历一个数组中的链表
           Node cur = array[i];
           while(cur != null) {
               //利用tmp记录cur的位置
               Node tmp = cur.next;
               //计算元素在新数组中的位置
               int newIndex = cur.key % newArray.length;
               //头插法
               cur.next = newArray[newIndex];
               newArray[newIndex] = cur;
               //回溯cur的位置
               cur = tmp;
           }
       }
        //将新数组赋值给原数组
        array = newArray;
    }

    //计算当前荷载因子的大小
    private double loadFactor() {
        return size * 1.0 / array.length;
    }

    public HashBucket() {
        array = new Node[8];
        size = 0;
    }

    //get方法
    public int get(int key) {
        int index = key % array.length;

        Node head = array[index];
        Node cur = head;
        while(cur != null) {
            if(key == cur.key) {
                return cur.value;
            }
            cur = cur.next;
        }

        //未找到,则返回-1
        return -1;
    }
}

 性能分析

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

java和类集的关系

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

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

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

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

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

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

相关文章

不做技术不会管理,测试人还有这个职位可以进阶

之前我们讲过&#xff0c;测试工程师的4层技术发展路线都需要掌握哪些技能。学而优则仕&#xff0c;今天我们来说说如果想做某个行业的专家应该掌握哪些技能。 如果你对测试技术不感兴趣&#xff0c;但对某领域的业务兴趣浓厚&#xff0c;可以考虑行业专家路线。 由于测试工程…

python单元测试框架(继承、unittest参数化、断言、测试报告)

一、继承 继承能解决什么问题&#xff1f; unittest每个模块都要用到前提条件以及清理&#xff0c;如果有上百个模块&#xff0c;我们要改域名和浏览器&#xff0c;就会工作量很大特别麻烦&#xff0c;这时我们可以用继承的思想只用改一次 我们可以将前提和清理提出来单独放…

日志分析系统——ELK

目录 一、ELK概述 ELK的组成 1、ElasticSearch 2、Logstash 3、Kiabana 完整日志采集系统基本特征 ELK的工作原理 二、ELK的部署 1、环境准备 2、部署ElasticSearch软件 3、安装Elasticsearch-head插件 4、Logstash部署 5、Kibana部署 三、FilebeatELK部署 1、安…

python基础教程:递归函数教程

嗨喽&#xff0c;大家好呀~这里是爱看美女的茜茜呐 1.递归的定义&#xff1a; 在函数内部直接或者间接调用函数本身 &#x1f447; &#x1f447; &#x1f447; 更多精彩机密、教程&#xff0c;尽在下方&#xff0c;赶紧点击了解吧~ python源码、视频教程、插件安装教程、资…

数据科学中常用的应用统计知识

随着大数据算法技术发展&#xff0c;数据算法越来越倾向机器学习和深度学习相关的算法技术&#xff0c;概率论和应用统计 等传统的技术貌似用的并不是很多了&#xff0c;但实则不然&#xff0c;在数据科学工作&#xff0c;还是会经常需要应用统计概率相关知识解决一些数据问题&…

Autosar诊断实战系列25-UDS 0x27服务相关问题思考

本文框架 前言0x27服务几个相关问题1. 安全访问种子的随机数能不能是全0?2. 安全级别之间是否有联系?是怎么确定的?3. 安全访问错误计数器具体变化策略?前言 在本系列笔者将结合工作中对诊断实战部分的应用经验进一步介绍常用UDS服务的进一步探讨及开发中注意事项, Dem/D…

如何使用 AI 快速学习新技术?

大家好&#xff0c;我是木川 当今&#xff0c;技术的快速发展势不可挡。无论您是初学者还是专业人士&#xff0c;学习新技术都是不可或缺的。 在没有 AI 的时代&#xff0c;我们如何学习新技术&#xff1f;而当 AI 出现后&#xff0c;我们如何更高效地掌握新技术呢&#xff1f;…

线性代数中涉及到的matlab命令-第三章:矩阵的初等变换及线性方程组

目录 1&#xff0c;矩阵的初等变换 1.1&#xff0c;初等变换 1.2&#xff0c;增广矩阵 ​1.3&#xff0c;定义和性质 1.4&#xff0c;行阶梯型矩阵、行最简型矩阵 1.5&#xff0c;标准形矩阵 1.6&#xff0c;矩阵初等变换的性质 2&#xff0c;矩阵的秩 3&#xff…

微信小程序6

一、什么是后台交互&#xff1f; 在小程序中&#xff0c;与后台交互指的是小程序前端与后台服务器之间的数据通信和请求处理过程。通过与后台交互&#xff0c;小程序能够获取服务器端的数据、上传用户数据、发送请求等。 与后台交互可以通过以下方式实现&#xff1a; 发起网络请…

黑客技术(自学方法)——网络安全技术

前言 前几天发布了一篇 网络安全&#xff08;黑客&#xff09;自学 没想到收到了许多人的私信想要学习网安黑客技术&#xff01;却不知道从哪里开始学起&#xff01;怎么学&#xff1f;如何学&#xff1f; 今天给大家分享一下&#xff0c;很多人上来就说想学习黑客&#xff0c…

腾讯地图基本使用(撒点位,点位点击,弹框等...功能) 搭配Vue3

腾讯地图的基础注册账号 展示地图等基础功能在专栏的上一篇内容 大家有兴趣可以去看一看 今天说的是腾讯地图的在稍微一点的基础操作 话不多说 直接上代码 var marker ref(null) var map var center ref(null) // 地图初始化 const initMap () > {//定义地图中心点坐标…

DocArray 近期更新:全面支持 Pydantic V2

DocArray 是一个专门为多模态数据的表示、传输、存储和检索而设计的 Python 库。其设计专为多模式人工智能应用程序的开发而量身定制&#xff0c;可保证与广泛的 Python 和机器学习生态系统的无缝集成。在 2022 年 1 月&#xff0c;DocArray 在 Apache License 2.0 下公开分发&…

ssm351校园服务平台管理系统+jsp

项目名称&#xff1a;ssm351校园服务平台管理系统jsp 点击这里进入源码目录 声明&#xff1a; 适用范围&#xff1a; 本文档适用于广泛的学术和教育用途&#xff0c;包括但不限于个人学习、毕业设计和课程设计。免责声明&#xff1a; 特此声明&#xff0c;本文仅供参考学习之用…

代码随想录算法训练营第六十天 | 739. 每日温度、496.下一个更大元素 I

739. 每日温度 链接&#xff1a; 代码随想录 &#xff08;1&#xff09;代码 496.下一个更大元素 I 链接&#xff1a; 代码随想录 &#xff08;1&#xff09;代码

Allegro如何对器件进行等距对齐操作

在用Allegro进行PCB设计时,需要经常用到对齐操作。那如何在不使用skill的情况下,对器件进行对齐操作呢? 本方法同样可对Pins,文本进行对齐操作。 使用软件自带的详细操作方法如下: (1)选择菜单Setup。 选择Application Mode(应用模式)→Placement Edit(放置编辑),…

u-boot和bootloader到底有什么区别

嵌入式软件工程师都听说过 u-boot 和 bootloader&#xff0c;但很多工程师依然不知道他们到底是啥。 今天就来简单讲讲 u-boot 和 bootloader 的内容以及区别。 Bootloader Bootloader从字面上来看就是启动加载的意思。用过电脑的都知道&#xff0c;windows开机时会首先加载bi…

Unity中Shader阴影的投射

文章目录 前言一、我们急需使用之前 Offset 中的Shader作为测试二、生成阴影1、添加"LightMode" "ShadowCaster"的Pass.2、appdata中声明float4 vertex:POSITION;和half3 normal:NORMAL;这是生成阴影所需要的语义.3、v2f中添加V2F_SHADOW_CASTER;用于声明…

java 通用导出接口

每个功能导出文件都单独写接口太过繁琐&#xff0c;出于方便大致讲讲通用导出功能的实现。 导出文件配置表&#xff0c;该表保存导出dto和导出文件名的对应关系等信息&#xff1a; TableName(value "SIMPLE_COMMON_EXPORT_TAB") public class SimpleCommonExportT…

vue3+element-plus 封装列表页,分页,排序,导出

目录 背景描述&#xff1a; 开发流程&#xff1a; 详细开发&#xff1a; 总结&#xff1a; 背景描述&#xff1a; web很多时候&#xff0c;要开发一个列表页&#xff0c;展示大量数据&#xff0c;并且提供一些交互功能&#xff0c;例如排序和分页&#xff0c;导出功能&…

嵌入式硬件库的基本操作方式与分析

本次要介绍的开源软件是 c-periphery&#xff1a; https://github.com/vsergeev/c-periphery一个用 C 语言编写的硬件外设访问库。 我们可以用它来读写 Serial、SPI、I2C 等&#xff0c;非常适合在嵌入式产品上使用。 我们可以基于它优秀的代码框架&#xff0c;不断地扩展出更…