哈希表的原理

news2024/11/19 20:33:16

哈希概念

线性表、树结构的查找方式都是以关键字的比较为基础,查找效率比较低,顺序表的时间复杂度是O(n),平衡树中为树的高度,即O(logn),搜素的效率取决于搜索过程的元素比较次数。

理想的搜素方法:可以用不经过比较,一次直接从表中得到要搜素的元素。如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与他的关键字之间能够建立一一映射的关系,那么在查找元素的时候,就可以很快寻找到该元素,这就是哈希的思想.

哈希函数

插入元素

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

搜素元素

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

此方法即为哈希(散列)方法,哈希(散列)方法中使用的转换函数称之为哈希(散列)函数,构造出来的结构就是哈希表(hashTable)(或者散列表)。

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

哈希函数设置为:hash(key)= key%capacity;(capacity代表的是底层空间总大小可以理解成元素个数)

 用该方法进行搜素的时候不必多次进行比较可以直接找到想找的关键字,但是我们就会出现一个问题:如果再次插入一个44怎么办呢?这就会引出下面的部分,哈希冲突。

哈希冲突:

概念:

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

冲突的避免

首先我们要明确疑点,由于我们哈希表底层数组容量往往小于实际要存储的数量,这就导致了一个问题,冲突是必然的,但是我们可以降低冲突率。

哈希函数的设计

1、直接定制法

取关键字的某个线性函数为散列地址:Hash(key)=A*key+B (即是一个y=ax+b的函数,如计数排序数字91存储在(91-90)的下标上)优点:简单、均匀 缺点需要事先知道关键字的分布情况 使用场景:适合查找比较小且连续的情况。

2、除留余数法

这个就是我们上面所介绍的方法,标准的官方说法是设散列表中允许的地址数为m(如果10个数据m的范围就是0-9),取一个不大于m的数,但是最接近或者等于m的质数p作为除数,按照哈希函数Hash(key)= key%p(p<=m),将关键字码转换为哈希地址。

还有很多的方法,但是使用的很少,所以这里不过多介绍。

负载因子调节

散列表的载荷因子定义为:\alpha=填入表格的元素/散列表的长度

在java系统库中限制载荷因子为0.75如果大于这个数字就需要,降低载荷因子。

哈希冲突的解决

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

闭散列:也叫开放定址法,当哈希表没有被填满,说明哈希表还有空位置,那么可以把key存放到冲突位置的下一个空位置去。那么如何寻找下一个空位置呢?

1、线性探测

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

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

 采用闭散列处理哈希冲突的时候,不能随便物理删除哈希表已经有的元素,若直接删除元素会影响其他元素的搜索。比如删除4,如果直接删除4,那么在查找44的时候就会受到影响。因此线性探测采用标记的伪元素来删除一个元素。

2、二次探测

线性探测的缺陷是产生冲突的元素堆积到一块,这与其找下一个元素位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免这样的问题,找下一个空位置的方法为:Hi=(H0+i²)%m或者Hi=(H0-i²)%m其中:i=1,2,3....H0是通过哈希函数计算出元素所在位置,m是表的大小。

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

开散列/哈希桶(重点)

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

 

开散列,可以认为把一个大集合中的搜素问题转化到小集合中进行。

代码实现:

public class Hashbucket {
    static class Node{
        private int key;
        private int value;
        private Node next;

        public Node(int key, int value) {
            this.key = key;
            this.value = value;
        }
    }
    public Node[] array;
    public int useSize;
    private static final float DEFAULT_LOAD_FACTOR = 0.75f;
    public Hashbucket() {
        array=new Node[10];
    }
    public void put(int key,int value){
        Node node=new Node(key,value);
        int index= node.key%array.length;
        Node cur=array[index];
        //java1.8之后采用的是尾插法 这里采用头插法
        while(cur!=null){
            if(cur.key==key){
                cur.value=value;
                return;
            }
            cur=cur.next;
        }
        node.next=array[index];
        array[index]=node;
        useSize++;
        if(loadFactor()>DEFAULT_LOAD_FACTOR){
            resize();
        }
    }
    public void resize(){
        //二倍扩容
        Node[] tmpArray=new Node[array.length*2];
        for (Node node : array) {
            Node cur = node;
            while (cur != null) {
                Node curNext = cur.next;
                int index = cur.key % tmpArray.length;
                //头插法
                cur.next = tmpArray[index];
                tmpArray[index] = cur;
                cur = curNext;
            }
        }
        array=tmpArray;
    }
    public float loadFactor(){
        return array.length*1.0f/useSize;
    }
    public int get(int key){
        int index=key%array.length;
        Node cur=array[index];
        while(cur!=null){
            if(cur.key==key){
                return cur.value;
            }
            cur=cur.next;
        }
        return -1;
    }
}

这个代码中这个扩容部分我们要仔细的说一说,但负载因子大于0.75的时候,我们需要降低哈希冲突,因为我们的元素个数是一定的,所以我们就需要增大散列表的大小,但是当我们扩容的时候,我们会打乱原来的散列表,比如原来4和14都是在节点4的位置,当我们二倍扩容的时候我们就需要将14放到节点14的位置。

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

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

相关文章

归并排序的递归和非递归

基本思想 归并排序&#xff08;MERGE-SORT&#xff09;是建立在归并操作上的一种有效的排序算法,该算法是采用分治法&#xff08;Divide andConquer&#xff09;的一个非常典型的应用。将已有序的子序列合并&#xff0c;得到完全有序的序列&#xff1b;即先使每个子序列有序&a…

OpenCV项目开发实战--详细介绍如何进行边缘轮廓检测 (Python/C++)-附源码

使用轮廓检测​​,我们可以检测对象的边界,并轻松在图像中定位它们。它通常是许多有趣应用的第一步,例如图像前景提取、简单图像分割、检测和识别。 因此,让我们使用 OpenCV 来了解轮廓和轮廓检测,并亲眼看看如何使用它们来构建各种应用程序。 轮廓在计算机视觉中的应用

latex2【图片、公式、矩阵】

图片 语法&#xff1a; \includegraphics{排队论模型.png} 看起来很别扭是吧&#xff0c;需要进行“修饰”&#xff1a; 当然&#xff0c;这样也很丑&#xff0c;一般写论文可以用以下的格式&#xff1a; \begin{figure}[H] \caption{问题一模型示意图} \label{paiduimx} …

【企业架构工具】2023 年 18 大企业架构工具

这些流行和新兴的 EA 工具为企业提供了支持企业架构和数字化转型所需的一切。 企业架构系统并不总是必不可少的。据推测&#xff0c;在 1940 年代&#xff0c;国际商业机器公司的一位领导人小托马斯沃森 (Thomas Watson Jr.) 曾说过&#xff1a;“我认为大约有 5 台计算机的全球…

基于SpringBoot+vue的校园疫情防控系统设计与实现

博主介绍&#xff1a; 大家好&#xff0c;我是一名在Java圈混迹十余年的程序员&#xff0c;精通Java编程语言&#xff0c;同时也熟练掌握微信小程序、Python和Android等技术&#xff0c;能够为大家提供全方位的技术支持和交流。 我擅长在JavaWeb、SSH、SSM、SpringBoot等框架…

Linux内核网络-拥塞控制系列(一)

谈起网络拥塞控制&#xff0c;大家可能很熟悉八股文中的"加法增大“、”乘法减小“、”慢开始“、“拥塞避免”、“快重传”、“快恢复”等概念。没错&#xff0c;这是一种经典网络拥塞控制算法的基础理论&#xff0c;但在实际的实现时不同的拥塞控制算法&#xff0c;有很…

【Android Camera开发】深入理解相机ISP(图像信号处理)必看文章

​原文&#xff1a;https://blog.51cto.com/u_16081664/6224003 作者&#xff1a;mb64411cc0e9333 凡是和图像领域工作的人&#xff0c;都会经常听到ISP&#xff08;Image Signal Process&#xff0c;图像信号处理&#xff09;&#xff0c;知道ISP对图像质量非常重要。比如华为…

电能管理系统在路店上的应用 安科瑞 许敏

摘要&#xff1a;随着企业改革的不断深入&#xff0c;对现代化用电管理的水平要求越来越高&#xff0c;准确、快速、经济的获得用电回路的各类数据进行用电分析、负荷管理、表计运行状况监测、电费自动结算的基础。同时也是提高企业经济效益的有效手段。近年来技术人员对监控系…

嵌入式程序开发者的数量剧增

随着物联网、智能设备和嵌入式系统的快速发展&#xff0c;嵌入式程序开发领域的需求不断增长&#xff0c;因此嵌入式程序开发者的数量也在剧增。这种趋势在过去几年中已经变得非常明显。 以下是导致嵌入式程序开发者数量剧增的一些主要原因&#xff1a; 我这里刚好有嵌入式、单…

Mac平台下如何制作pkg安装包以及rpath设置

打包工具介绍 Mac平台规范包可以使用Packages工具。下载地址 打包前准备工作 创建一个目录 macProject macProject目录中是以下目录结构 myProject.app└── Contents├── Info.plist├── MacOS├── res├── libmymath.dylib├── Frameworks└── Resources├…

Hive多行转多列,多列转多行

hive中的行列转换包含单行、多行、单列、多列&#xff0c;所以一共有四种组和转换结果。 一、多行转多列 原始数据表 目标结果表 分析&#xff1a;目标表中的a和b是用分组形成&#xff0c;所以groupby字段选用原始表中col1&#xff0c;c、d、e是原始表中的行值&#xff0c;…

数据结构(王道)——线性表的存储结构之循环表

一、循环单链表 定义&#xff1a; 循环单链表代码实现 创建并初始化、判断循环单链表是否为空、判断结点p是否为循环单链表的表尾结点的代码操作。 二、循环双链表 定义&#xff1a; 循环双链表代码实现 创建并初始化、判断循环双链表是否为空、判断结点p是否为循环双链表的…

橙河网络:怎么搭建海外问卷网站呢?

大家好&#xff0c;我是橙河&#xff0c;如果你想要搭建海外问卷网站赚钱&#xff0c;看我这篇文章就行了。 搭建网站&#xff0c;本身并不复杂&#xff0c;自己会敲代码就自己搞&#xff0c;不会就花点钱外包给别人。 搭建好问卷网站以后&#xff0c;重点来了&#xff0c;你需…

Learning Spatial and Spatio-Temporal Pixel

Learning Deformable Kernels for Image and Video Denoising 作者&#xff1a; Xiangyu Xu 商汤科技SenseTime Research 论文思想&#xff1a;一是将传统的双边滤波算法与CNN结合起来&#xff0c;二是用变形卷积来做多帧对齐的问题&#xff0c;三还是在raw上进行处理的。 …

WSL2 忘记用户密码

步骤一&#xff1a;将默认用户切换为root 在Windows里启动命令提示符&#xff0c;输入&#xff1a; ubuntu2004 config --default-user root这就已经将我的ubuntu20.04的默认用户切换为了root&#xff0c; 不同的WSL版本可能命令的第一个符号不一样&#xff0c;区别如下图&am…

Acrel-3000电能管理系统某公司项目中的应用 安科瑞 许敏

摘要&#xff1a;用户对自身用能的管理意识提升&#xff0c;促使用户侧电力配电系统在商业、工业以及民用区域的普及。系统针对用户侧主要的用能节点&#xff0c;设计安装智能仪表&#xff0c;再通过后台系统来实时监控各用能回路的工作状态、用电量、用水量、用气量数数据的采…

自动驾驶商用驶入“快车道”,汽车软件厂商如何“抢市”?

L3级及以上自动驾驶的商业化进程正在驶入“快车道”。 一方面&#xff0c;高阶自动驾驶的相关法规及标准不断出台&#xff0c;为自动驾驶行业的发展注入了“强心剂”。 比如工业和信息化部副部长辛国斌就曾表示&#xff0c;将启动智能网联汽车准入和上路通行试点&#xff0c;…

【嵌入式开发 Linux 常用命令系列 5 -- history 与 “!“ 巧妙配合】

文章目录 history 命令介绍history 命令与 “&#xff01;”运行先前执行的命令先前命令的参数传递给新命令两个或多个参数的处理设置 history 命令显示行数以及时间 上篇文章&#xff1a;嵌入式开发 Linux 常用命令系列 4 – git 常用配置及常用命令 history 命令介绍 histo…

虚拟内存、内存分页、分段、段页式内存管理

虚拟内存 为什么有虚拟内存&#xff1f; CPU是直接操作内存的物理地址。在这种情况下&#xff0c;如果两个程序占用的内存有重叠&#xff0c;要想同时运行两个程序是不可能的。 为啥它会内存有重叠啊&#xff1f;我不理解。难道不是这块内存被这个程序使用之后另外的程序就无…

Java内部类(InnerClass)

文章目录 概述1 什么是内部类2 为什么要声明内部类呢3 内部类的分类 成员内部类1 概述2 创建成员内部类对象 局部内部类1 非匿名局部内部类 匿名内部类 概述 1 什么是内部类 将一个类A定义在另一个类B里面&#xff0c;里面的那个类A就称为内部类&#xff08;InnerClass&#…