认识哈希表

news2024/9/21 17:56:24

作者~小明学编程 

文章专栏Java数据结构

格言目之所及皆为回忆,心之所想皆为过往

目录

为什么我们需要哈希表?

哈希表的原理

什么是哈希值

冲突

负载因子

解决冲突

闭散列

开散列/哈希桶

代码实现

不考虑泛型

put操作

get操作

整体代码

考虑泛型

hashCode和equals为何要同时重写

代码:


为什么我们需要哈希表?

         前面我们已经讲了很多的数据结构了,例如顺序表,链表,二叉树等等,这些数据结构的增删查改的时间复杂度有的是增为O(1),但删就为O(n),有的虽然增为O(n),但是删为O(1),但是我们的哈希表很牛,增删的时间复杂度都是O(1),就问你牛杯不?下面我们就来详细的说说这是怎么实现的。

哈希表的原理

        之所以我们的时间复杂度能达到O(1)是因为我们没有比较,直接将我们的元素插入到哈希表中,我们想插入的元素都会有一个哈希值,这个哈希值我们可以看作是一个地址,这样我们就相当于通过这个地址直接插入元素,当我们想要取出这个元素的时候也可以通过我们的哈希值直接找到我们的元素。

什么是哈希值

我们先来看一下这幅图

 上面是我们的数组,下面是我们想要放进去的数字,可以看到我们数组的长度是7,这时候我们的哈希值就可以是我们的key%capacity,也就是拿我们的当前的值余上我们的数组长度,这样我们放的时候就不会出现数组越界的情况了,有点像我们的桶排序。

 我们可以看到前面放元素的过程就是这样,现在我们想要把8也放进去然后就会发现问题,我们8所计算出来的哈希值也是2。

冲突

这时我们就发现我们想要放进去的8与我们哈希表里面的1冲突了,这种冲突不可避免,首先我们想到的就是去降低我们冲突发生的频率,想要降低冲突频率当然就是扩大我们的哈希表,也就是扩大我们的数组。

负载因子

所谓的负载因子就是我们哈希表底层的数组,已经放进去的元素/数组的长度,负载因子与我们冲突的关系如下:

 一般情况下我们的负载因子设定为0.75也就是当我们的底层数组被放了七八成的时候我们就要开始对数组进行扩容了。

这个时候也许会有疑问,我们的负载因子为什么不设置为1或者0.5呢?

        这是因为当我们的负载因子设置为1的时候我们肯定已经产生大量的冲突了,这是我们所不想看到的,我们想要的是较低的冲突,那么既然我们想要较低的冲突我们为什么不将其设置为0.5呢?通过上面的图我们可以看到,但负载因子是0.5的时候我们的冲突率是很低的,确实冲突率是降低了,但是我们的数组才用了一半就要进行数组的扩容这样太浪费空间了,所以我们取中设0.75为我们的负载因子。

解决冲突

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

闭散列

闭散列:也叫开放定址法,当我们发生冲突的时候我们的数组我们可以将发生冲突的元素放到数组中的其它空闲的位置。

对于如何去找下一个空闲的位置我们常见的有两种方法:

线性探测法:就是向当前冲突的位置一直向后找直到找到空闲的位置,但是这样做的缺点就是会导致我们冲突的元素都聚集到一起了。

二次探测:二次探测的出现就是解决我们线性探测法的问题的,其目的就是避免聚集,所以我们找空位置的方式就不再是挨着去找了,而是用一个公式找出我们的位置H(i) =( H0+i^2)%m,其中:i = 1,2,3…, 是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。

开散列/哈希桶

开散列法又叫链地址法(开链法):我们将哈希表的底层数组中的每个元素都设置为一个链表,当我们产生冲突的时候就向我们的链表中插入一个新的节点,如下图:

 思路很简单,接下来我们就用代码来实现一下。

代码实现

不考虑泛型

首先我们就是要设置我们的链表节点:

    static class Node {
        public int key;
        public int val;
        public Node next;
        public Node(int key, int val) {
            this.key = key;
            this.val = val;
        }
    }

我们将其定义为一个静态的内部类,里面包含我们的key值,数据域val,还有我们的下一个节点的地址next。

    Node[] array;//哈希表
    public int userSize = 0;//我们哈希表里面的元素个数
    public static final double DEFAULT_LOAD_FACTOR = 0.75;//负载因子
    public HashBuck() {
        this.array = new Node[10];
    }

接下来就是设定我们的哈希表,设置为我们的节点类型的数组,元素个数和负载因子。

put操作

我们的put操作的功能就是向我们的哈希表中放元素,这里我们的key是整型的所以:

1.算出我们的hash值,new一个我们当前的节点。

2.通过hsah值找到我们元素在数组中的位置。

3.遍历当前链表的所有节点看看是否该key已存在,如果存在那就直接结束不用再插入了。

4.判断一下我们的负载是否超过负载因子,超过的话就需要扩容。

5.采用链表的头插法将当前节点插入哈希表中。

代码:

    //放入元素的操作
    public void put(int key,int val) {
        int index = key% array.length;//找到我们对应的数组下标
        Node node = new Node(key, val);
        Node cur = array[index];//找到数组所对应链表的首地址
        //首先遍历一遍链表如果key相同的话,我们要更新key的值
        while (cur!=null) {
            if (cur.key == key) {
                cur.val = val;
                return;
            }
            cur = cur.next;
        }
        //考虑扩容问题,如果我们当前的负载因子达到了阈值,就开始扩容
        if (getDefaultLoadFactor()>DEFAULT_LOAD_FACTOR) {
            resize();
        }
        //将当前的节点连接到链表上去,采用头插法
        node.next = array[index];
        array[index] = node;
        this.userSize++;
    }
    //返回我们当前的负载因子
    private double getDefaultLoadFactor() {
        return 1.0*userSize/this.array.length;
    }
    //扩容操作
    private void resize() {
        //首先new一个新的数组
        Node[] newArray = new Node[2 * array.length];
        for (int i = 0; i < array.length; i++) {
            Node cur = array[i];
            while (cur!=null) {
                Node curNext = cur.next;//记录一下我们的下一个节点防止后面插入后丢失
                int index = cur.key % newArray.length;//记录当前的下标
                //一样还是采用头插法
                cur.next = newArray[index];
                newArray[index] = cur;
                cur = curNext;
            }
        }
        this.array = newArray;
    }

get操作

get操作就比较简单了,获取我们的hash值,然后找到数组下标,接着遍历链表找到我们的key.

代码:

    //获取操作
    public int get(int key) {
        int index = key % array.length;
        Node cur = array[index];
        while (cur!=null) {
            if (cur.key==key) {
                return cur.val;
            }
        }
        return -1;
    }

整体代码

public class HashBuck {
    //哈希表中的节点
    static class Node {
        public int key;
        public int val;
        public Node next;
        public Node(int key, int val) {
            this.key = key;
            this.val = val;
        }
    }
    Node[] array;//哈希表
    public int userSize = 0;//我们哈希表里面的元素个数
    public static final double DEFAULT_LOAD_FACTOR = 0.75;//负载因子
    public HashBuck() {
        this.array = new Node[10];
    }
    //放入元素的操作
    public void put(int key,int val) {
        int index = key% array.length;//找到我们对应的数组下标
        Node node = new Node(key, val);
        Node cur = array[index];//找到数组所对应链表的首地址
        //首先遍历一遍链表如果key相同的话,我们要更新key的值
        while (cur!=null) {
            if (cur.key == key) {
                cur.val = val;
                return;
            }
            cur = cur.next;
        }
        //考虑扩容问题,如果我们当前的负载因子达到了阈值,就开始扩容
        if (getDefaultLoadFactor()>DEFAULT_LOAD_FACTOR) {
            resize();
        }
        //将当前的节点连接到链表上去,采用头插法
        node.next = array[index];
        array[index] = node;
        this.userSize++;
    }
    //返回我们当前的负载因子
    private double getDefaultLoadFactor() {
        return 1.0*userSize/this.array.length;
    }
    //扩容操作
    private void resize() {
        //首先new一个新的数组
        Node[] newArray = new Node[2 * array.length];
        for (int i = 0; i < array.length; i++) {
            Node cur = array[i];
            while (cur!=null) {
                Node curNext = cur.next;//记录一下我们的下一个节点防止后面插入后丢失
                int index = cur.key % newArray.length;//记录当前的下标
                //一样还是采用头插法
                cur.next = newArray[index];
                newArray[index] = cur;
                cur = curNext;
            }
        }
        this.array = newArray;
    }
    //获取操作
    public int get(int key) {
        int index = key % array.length;
        Node cur = array[index];
        while (cur!=null) {
            if (cur.key==key) {
                return cur.val;
            }
        }
        return -1;
    }

}

考虑泛型

前面我们因为我们的key值是整型所以我们在计算哈希值的时候直接用key%数组长度,那么当我们的key不是整型而是其它的类型,例如是一个String类型或者,其它应用类型时候该怎么办呢?

这时候就要用到我们的hashCode方法,该方法就是根据我们的引用来生成一个hash值,例如我们有一个Person的类。

class Person {
    public int ID;
    public Person(int ID) {
        this.ID = ID;
    }
}
    public static void main(String[] args) {
        Person person1 = new Person(340);
        Person person2 = new Person(340);
        Person person3 = person1;
        System.out.println(person1.hashCode());//460141958
        System.out.println(person2.hashCode());//1163157884
        System.out.println(person3.hashCode());//460141958
    }

这里我们调用hashCode的方法可以看到我们生成一个hash值,这样就解决了我们hsah值的问题了,那么问题又来了,我们new的这两个对象本质上是一样的,都是ID=340,但是其hash值却不一样,当我们想要其hash值一样的时候就需要重写其hashCode,但是在我们重写hashCode的过程中往往也要重写equals,这是为什么呢?

hashCode和equals为何要同时重写

这是因为我们的hashCode所生成的hash值在是根据对象的地址来生成就如同上述的代码,我们的person1和person2不相等(没有重写equals,此时判断是否相等比较的是地址)而person1和person3是相等的,所以我们person1和person3所生成的hash值都是一样的。

现在我们不需要根据地址来生成哈希值,因为这样没有太大的意义,我们要根据我们对象里面的成员来生成我们的hash值,比如我们的Person类,我们要根据我们的ID来生成我们想要的hash值,此时我们就要去重写hashCode了,重写完之后:

class Person {
    public int ID;
    public Person(int ID) {
        this.ID = ID;
    }

    @Override
    public int hashCode() {
        return Objects.hash(ID);
    }
}
    public static void main(String[] args) {
        Person person1 = new Person(340);
        Person person2 = new Person(340);
        Person person3 = person1;
        System.out.println(person1.hashCode());//371
        System.out.println(person2.hashCode());//371
        System.out.println(person3.hashCode());//371

    }

此时我们得到了我们想要的东西,hash都一样了,但是当我们向里面put元素和get元素的时候,我们就会发现问题:

 我们会发现无论是put还是get我们都会有一个equals,比较当前对象是否一致才行,如果不重写还是默认比较地址,那么既然是比较地址就行了何必重写我们的hashCode呢,我们的hash值能让我们找到我们想要的元素在数组的哪一个下标,但是每个下标下面又是一个链表(或者红黑树)所以想要找到具体的元素还需要去比较,所以就得重写equals。

代码:

public class HashBuck2<K,V> {
    static class Node<K,V>{
        public K key;
        public V val;
        public Node<K,V> next;
        public Node(K key,V val){
            this.key = key;
            this.val = val;
        }
    }
    public Node<K,V>[] array = (Node<K,V>[])(new Node[10]);//哈希表
    public int userSize = 0;//我们哈希表里面的元素个数
    public static final double DEFAULT_LOAD_FACTOR = 0.75;//负载因子
    //放入元素的操作
    public void put(K key,V val) {
        int hash = key.hashCode();
        int index = hash% array.length;//找到我们对应的数组下标
        Node<K,V> node = new Node(key, val);
        Node<K,V> cur = array[index];//找到数组所对应链表的首地址
        //首先遍历一遍链表如果key相同的话,我们要更新key的值
        while (cur!=null) {
            if (cur.key.equals(key)) {
                cur.val = val;
                return;
            }
            cur = cur.next;
        }
        //考虑扩容问题,如果我们当前的负载因子达到了阈值,就开始扩容
        if (getDefaultLoadFactor()>DEFAULT_LOAD_FACTOR) {
            resize();
        }

        //将当前的节点连接到链表上去,采用头插法
        node.next = array[index];
        array[index] = node;
        this.userSize++;
    }
    //返回我们当前的负载因子
    private double getDefaultLoadFactor() {
        return 1.0*userSize/this.array.length;
    }
    //扩容操作
    private void resize() {
        //首先new一个新的数组
        Node<K,V>[] newArray = new Node[2 * array.length];
        for (int i = 0; i < array.length; i++) {
            Node<K,V> cur = array[i];
            while (cur!=null) {
                Node<K,V> curNext = cur.next;//记录一下我们的下一个节点防止后面插入后丢失
                int hash = cur.key.hashCode();
                int index = hash % newArray.length;//记录当前的下标
                //一样还是采用头插法
                cur.next = newArray[index];
                newArray[index] = cur;
                cur = curNext;
            }
        }
        this.array = newArray;
    }
    //获取操作
    public V get(K key) {
        int hash = key.hashCode();
        int index = hash % array.length;
        Node<K,V> cur = array[index];
        while (cur!=null) {
            if (cur.key.equals(key)) {
                return cur.val;
            }
        }
        return null;
    }

   
}

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

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

相关文章

我们为什么需要调用InitCommonControls?

很多第一次使用外壳通用控件 (Shell common controls) 的新手碰到的问题之一是&#xff1a;他们经常忘记调用 InitCommonControls 。 但是如果你有机会查看这个函数的反汇编代码&#xff0c;则你会发现&#xff1a;这个函数实际上不做任何事情&#xff0c;就像另外一个函数 Flu…

python--pip常用命令、国内PyPI镜像、使用pip安装第三方库

让我们来看看具体内容&#xff1a; 一. pip常用命令 列出已安装的包&#xff1a; pip freeze or pip list 导出requirements.txt&#xff1a; pip freeze ><目录>/requirements.txt 在线安装包&#xff08;模块库&#xff09;&#xff1a; pip install <包名>…

论文解读:为了数据中心的未来,存算这对CP竟然又离了

古语有云&#xff1a;天下大势分久必合、合久必分。 同样&#xff0c;在数据中心多年的发展历程中&#xff0c;计算与存储也经历了多次分分合合。从大型机的计算与存储紧耦合&#xff0c;到小型机经典的IOE存算分离架构&#xff0c;再到随云兴起的超融合让存算再次融合&#x…

达梦数据库的名词解释

达梦数据库的名词解释 C/S、客户端、服务器、物理结构、逻辑结构、文件系统、文件、数据库、数据库实例、表空间、表、段、簇、页、用户、模式、角色、 一、数据库的组成 客户端连接服务器&#xff0c;通过数据库实例&#xff08;共享内存后台进程及线程&#xff09;将磁盘内…

关于修复预制体上UnityEngine.UI引用丢失的一种思路

在开发项目过程中&#xff0c;如果出现了Unity版本变化&#xff0c;有可能会导致一些预制体上的UI组件丢失&#xff0c;特别是大量UI脚本&#xff0c;明明一看就知道这个是Text组件&#xff0c;但是一个大大的missing出现在预制体上&#xff0c;让人产生了莫名的恐慌。 一、根…

头歌计算机组成原理MIPS寄存器文件设计

全部答案点击底部 <?xml version"1.0" encoding"UTF-8" standalone"no"?> <project source"2.15.0.2.exe" version"1.0"> This file is intended to be loaded by Logisim http://logisim.altervista.org &…

Linux | 二级页表的虚拟地址是怎么转换的?

文章目录页的概念可执行文件的虚拟地址二级页表的转换二级页表的优点页的概念 在聊文件系统时&#xff0c;我提到操作系统是以块为基本单位进行IO的&#xff0c;一个块的大小为4KB&#xff0c;在文件系统中它的名字叫做块&#xff0c;在内存系统中它的名字叫做页&#xff0c;p…

并发编程十 定时任务定时线程池

一 ScheduledThreadPoolExecutor 定时线程池类的类结构图 它接收SchduledFutureTask类型的任务&#xff0c;是线程池调度任务的最小单位&#xff0c;有三种提交任务的方式&#xff1a; schedulescheduledAtFixedRatescheduledWithFixedDelay 它采用DelayQueue存储等待的任务…

带你玩转序列模型之Bleu得分注意力模型语音识别

目录 一.Bleu得分 二.注意力模型直观理解 三.注意力模型 四.语音识别 五.触发字检测 一.Bleu得分 先跳过&#xff0c;等回头用得到了再来补。 二.注意力模型直观理解 在本周大部分时间中&#xff0c;你都在使用这个编码解码的构架&#xff08;a Encoder-Decoder archit…

MATLAB算法实战应用案例精讲-【图像处理】目标检测

前言 目标检测,也叫目标提取,是一种基于目标几何和统计特征的图像分割。它将目标的分割和识别合二为一,其准确性和实时性是整个系统的一项重要能力。尤其是在复杂场景中,需要对多个目标进行实时处理时,目标自动提取和识别就显得特别重要。 随着计算机技术的发展和计算机视…

AutoSAR基础:Port与Dio

AutoSAR基础:Port与Dio配置一.配置port1.Configurator1-1.进入Basic Editor->Port1-2.配置P00.0脚1-2-1.Port口1-2-2.Pin脚配置1-3.配置P00.11-3-1.Pin脚配置2.config导出生成代码3.代码部分3-1.config生成代码3-2.Write与Read3-2-1.Read函数3-2-2.Write函数二.配置Dio1.Con…

[附源码]计算机毕业设计基于SpringBoot的校园报修平台

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

qt使用http get和post

qt使用http get和post 本文目录qt使用http get和post准备get请求widge.hwidget.cpppost请求widge.hwidget.cpppost请求的数据格式参数写在url后面使用application/x-www-form-urlencoded使用application/json使用multipart/form-data传送文件准备 使用到网络编程&#xff0c;需…

Nginx配置实例-反向代理

1、实现效果 打开浏览器&#xff0c;在浏览器地址栏输入地址www.123.com&#xff0c;访问tomcat主页面。 2、准备工作 &#xff08;1&#xff09;在 liunx系统安装tomcat&#xff0c;使用默认端口8080 上传安装文件&#xff0c;并解压缩到/opt/tomcat。 进入tomcat的bin目录中…

[附源码]Python计算机毕业设计Django人员信息管理

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

ArrayList为什么线程不安全以及三种解决办法【详细】

目录不安全原因解决办法VectorCollectionsCopyOnWriteArrayList三种解决方式总结不安全原因 我们可以看一下ArrayList源码&#xff0c;找到add方法&#xff0c; public boolean add(E e) {ensureCapacityInternal(size 1); // Increments modCount!!elementData[size] e;r…

【java】Lambda表达式

文章目录体验Lambda表达式Lambda表达式的标准格式Lambda表达式的练习抽象方法无参无返回值抽象方法带参无返回值抽象方法带参带返回值Lambda表达式的省略模式Lambda表达式的注意事项Lambda表达式和匿名内部类的区别体验Lambda表达式 package heima.Lambda;import heima.多线程.…

HTML5期末大作业:旅游网页设计与实现——旅游风景区网站HTML+CSS+JavaScript 景点静态网页设计 学生DW静态网页设计

&#x1f468;‍&#x1f393;静态网站的编写主要是用 HTML DⅣV CSSJS等来完成页面的排版设计&#x1f469;‍&#x1f393;&#xff0c;一般的网页作业需要融入以下知识点&#xff1a;div布局、浮动定位、高级css、表格、表单及验证、js轮播图、音频视频Fash的应用、uli、下拉…

JMeter入门教程(9) --参数化

文章目录1.任务背景2.任务目标3.任务实操3.1 CSV数据文件1.任务背景 参数化是测试过程中很常用的一种技巧&#xff0c;可以将脚本中的某些输入用参数来代替&#xff0c;比如登陆时传递参数&#xff0c;在脚本运行时指定参数的取值范围和规则 2.任务目标 掌握基于JMeter性能测…

【浅学Java】SpringBoot创建和使用

SpringBoot创建和使用1. SpringBoot是什么2. SpringBoot的优点3. SpringBoot的创建3.1 使用idea创建3.2 测试项目_输出hello3.3 网页版创建SpringBoot4. 约定大于配置1. SpringBoot是什么 Spring的诞生是为了简化Java开发而创建的&#xff0c;而SpringBoot的诞生就是为了简化S…