java数据结构_Map和Set(一文理解哈希表)_9.3

news2025/3/4 9:06:27

目录

5. 哈希表

5.1 概念

5.2 冲突-概念

5.3 冲突-避免

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

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

5.6 冲突-解决

5.7 冲突-解决-闭散列

5.8 冲突-解决-开散列 / 哈希桶

5.9 冲突严重时的解决办法


5. 哈希表

5.1 概念

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

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

向该结构中:

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

        搜索元素:对元素的关键码进行同样的计算,把求得的函数值当作元素的存储位置,在结构

中按此位置取元素进行比较,若关键码相同,则搜索成功

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

例如:数据集合{1,7,6,4,5,9},哈希函数可以设置为:hash(key) = key % capacity capacitty为存储元素底层空间总的大小

用该方法进行搜索不必进行多次关键码的比较,效率会大大加快,但是,如果按照上述哈希方法,向集合中插入元素44,会出现什么问题?

5.2 冲突-概念

对于两个数据元素的关键字,数据元素的关键字并不相同,但是通过哈希函数计算出来的结果是相同的,即:不同关键字,通过哈希函数计算出相同的哈希地址,这种现象称为哈希冲突或者哈希碰撞

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

5.3 冲突-避免

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

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

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

哈希函数设计原则:

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

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

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

常见的哈希函数

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

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

        其他方法不作介绍

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

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

α是散列表装满程度的标志因子。由于散列表的长度是定值,α与“填入表中的元素的个数”成正比,所以,α越大,表明填入表中元素越多,产生冲突的可能性就越大。反之,α越小,表明填入表中的元素越少,产生冲突的可能性就越小,实际上,散列表的平均查找长度载荷因子α的函数,只是不同处理冲突的方法有不同的函数。

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

当冲突率达到一个无法忍受的高度时候,需要通过降低负载因子来变相的降低冲突率。

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

5.6 冲突-解决

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

5.7 冲突-解决-闭散列

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

        1.线性探测

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

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

但是采取线性探测的缺陷是,数据会堆积到一起,更会增加了发生哈希冲突的可能性

        2.二次探测

线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,线性探测中,就是载挨着往后逐个去找。二次探测为了避免该问题,找下一个空位置的方法为:H(i) = ( H(0) + i^2 )% m, 或者:H(i) = ( H(0)- i^2 )% m,H(0)是通过散列函数Hash(x)对元素的关键码key进行计算得到的位置,m是表的大小,i = 1,2,3....

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

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

5.8 冲突-解决-开散列 / 哈希桶

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

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

代码实现:

首先还是需要定义一个内部类Node,然后创建一个类型为Node[]的数组,定义usedSize

接下来是put方法,参数为要插入的数据的key,val,然后根据val值计算出对应的下标,index = key % array.length,定义cur,Node cur = array[index],然后用cur遍历该索引下的链表,检查是否存在当前的key(key是不能重复的),如果没有key,则使用头插法/尾插法插入(此处使用头插法)

添加完成之后,可以判断一下负载因子,如果负载因子大于0.75,则对散列表进行扩容

loadFactor方法:

在resize方法中,要将散列表的容量进行扩容,在扩容后,就要对整个散列再进行 !!!重新哈希!!!例如之前 4 和 14 都在散列表索引为4的位置,扩容后,14可能就不再索引为4的位置了,要进行重新哈希!!!

resize方法如下:

接下来是get方法,和put一样,得到对应的index,然后检查key值是否相同,如果找到就返回对应的val,如果没有找到就返回-1,下面是get方法的代码:

下面是全部代码:

/**
 * @author : zzr
 * @description :
 * @date :
 */
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;
        }
    }
    public Node[] array;
    public int usedSize;

    public HashBuck() {
        array = new Node[10];
    }

    public void put(int key, int val) {

        int index = key % array.length;

        Node cur = array[index];
        //遍历该索引下的链表,看一下是否存在的当前key(不重复)
        while (cur != null) {
            if(key == cur.key) {
                cur.val = val;
                return;
            }
            cur = cur.next;
        }
        //没有这个key
        //头插法插入单链表中
        Node node = new Node(key, val);
        node.next = array[index];
        array[index] = node;
        usedSize++;

        //如果负载因子大于等于0.75 就进行扩容 --> 重新哈希
        if(loadFactor() >= 0.75) {
            resize();
        }
    }

    private void resize() {
        //在扩容的时候 要对整个哈希表都重新哈希
        //例如之前 4 和 14都在索引为4的位置时候,扩容后,14 可能就不在索引为4的位置了
        Node[] tmpArr = new Node[array.length * 2];
        //遍历原来的数组 将所有的元素 !!!重新哈希!!! 到新的数组当中
        for (int i = 0; i < array.length; i++) {
            Node cur = array[i];
            while(cur != null) {
                Node curNext = cur.next;//记录cur的next位置
                int newIndex = cur.key % tmpArr.length;

                //头插法:
                cur.next = tmpArr[newIndex];
                tmpArr[newIndex] = cur;

                //cur继续向后走
                cur = curNext;
            }
        }
    }

    private double loadFactor() {
        return usedSize * 1.0 / array.length;
    }

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

测试符合预期:

这样实现的方法,在散列表中对数据进行增删改查的时间复杂度就都是O(1)了,这里可能会有疑问,因为我们的实现相当于是在数组中的每一个位置添加了一个单链表,我们在操作数据的时候,还是需要在单链表中对数据进行查找,才能进行操作,其时间复杂度不可能仅仅为O(1)。这里需要解释的是,我们在put方法中是由resize方法和loadFactor方法的,因为由负载因子的出现,是不会使得单链表的长度十分长的,单链表的长度一定会保持在一个常数级的大小,所以可以将该方法的时间复杂度记为O(1)。虽然时间复杂度降低为O(1),但是空间复杂度会上升,哈希表就是一个典型的浪费空间,节省时间的例子。

但上面的方法中,我们默认key和val都是int类型的数据,才能在计算index中直接index = key % array.length,但是在实际使用中,key,val类型通常是引用类型,则无法计算index了,那该怎么办呢?

如下:如果有一个类是Person类呢,其成员变量为String类型的id

这时候就要提出hashCode方法了,hasCode方法在JDK中底层是使用C++实现的,无法显示源码,我们只需要直到hashCode可以将引用类型转换成一个int类型并且返回。

在Person类中,我们并没有实现hashCode方法,则hashCode是其默认父类Object中的方法。我们在定义Person中id则代表一个人的身份证号,如果person1和person2的id相同,就说明他们是同一个人,但我们在直接打印person1和person2的hasCode的时候,发现其结果并不相同:

但是当我们在Person类中自动重写hashCode方法之后,再进行打印就发现其值相同了

这样就可以重写刚刚的put和get方法啦!

与之前的方法对比:

put方法:

get方法:

完整代码如下:

import java.util.Objects;

/**
 * @author : zzr
 * @description :
 * @date :
 */
class Person{
    String id;

    public Person(String id) {
        this.id = id;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return Objects.equals(id, person.id);
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(id);
    }
}

public class HashBuck2<K,V> {
    static class Node<K, V> {

        public K key;
        public V val;

        public Node next;
        public Node(K key, V val) {
            this.key = key;
            this.val = val;
        }
    }
    public Node<K,V>[] array;
    public int usedSize;

    public HashBuck2() {
        array = (Node<K, V>[]) new Node[10];
    }

    public void put(K key, V val) {
        int hash = key.hashCode();
        int index = hash % array.length;

        Node cur = array[index];

        while(cur != null) {
            if(key.equals(cur.key)) { //只能用equals方法而不是==
                cur.val = val;
                return;
            }
            cur = cur.next;
        }

        Node node = new Node<>(key, val);
        node.next = array[index];
        array[index] = node;
        usedSize++;
    }
    public V get(K key) {
        int hash = key.hashCode();
        int index = hash % array.length;
        Node<K,V> cur = array[index];
        while(cur != null) {
            if(key.equals(cur.key)) {
                return cur.val;
            }
            cur = cur.next;
        }
        return null;
    }

}

5.9 冲突严重时的解决办法

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

完!!!

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

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

相关文章

基于SpringBoot的“数据驱动的资产管理系统站”的设计与实现(源码+数据库+文档+PPT)

基于SpringBoot的“数据驱动的资产管理系统站”的设计与实现&#xff08;源码数据库文档PPT) 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;SpringBoot 工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 系统功能结构图 局部E-R图 系统登录界…

excel 斜向拆分单元格

右键-合并单元格 右键-设置单元格格式-边框 在设置好分割线后&#xff0c;你可以开始输入文字。 需要注意的是&#xff0c;文字并不会自动分成上下两行。 为了达到你期望的效果&#xff0c;你可以通过 同过左对齐、上对齐 空格键或使用【AltEnter】组合键来调整单元格中内容的…

深入理解推理语言模型(RLM)

大语言模型从通用走向推理&#xff0c;万字长文解析推理语言模型&#xff0c;建议收藏后食用。 本文基于苏黎世联邦理工学院的论文《Reasoning Language Models: A Blueprint》进行整理&#xff0c;你将会了解到&#xff1a; 1、RLM的演进与基础&#xff1a;RLM融合LLM的知识广…

2025年具有百度特色的软件测试面试题

百度业务场景 如何测试一个高并发的搜索系统(如百度搜索)?如何测试一个在线地图服务(如百度地图)?如何测试一个大型推荐系统(如百度推荐)的性能?百度技术栈 你对百度的 PaddlePaddle 框架有了解吗?如何测试基于 PaddlePaddle 的服务?如何测试百度云的 API 服务?你对…

Lua | 每日一练 (5)

&#x1f4a2;欢迎来到张胤尘的技术站 &#x1f4a5;技术如江河&#xff0c;汇聚众志成。代码似星辰&#xff0c;照亮行征程。开源精神长&#xff0c;传承永不忘。携手共前行&#xff0c;未来更辉煌&#x1f4a5; 文章目录 Lua | 每日一练 (5)题目参考答案浅拷贝深拷贝使用场景…

C# Unity 唐老狮 No.5 模拟面试题

本文章不作任何商业用途 仅作学习与交流 安利唐老狮与其他老师合作的网站,内有大量免费资源和优质付费资源,我入门就是看唐老师的课程 打好坚实的基础非常非常重要: 全部 - 游习堂 - 唐老狮创立的游戏开发在线学习平台 - Powered By EduSoho 如果你发现了文章内特殊的字体格式,…

云原生事件驱动架构:构建实时响应的数字化神经系统

引言&#xff1a;重塑企业实时决策能力 Uber实现事件驱动架构升级后&#xff0c;实时供需匹配延迟降至8ms&#xff0c;动态定价策略响应速度提升1200倍。Netflix通过事件流处理实现个性化推荐&#xff0c;用户点击率提高34%&#xff0c;事件处理吞吐量达2000万/秒。Confluent基…

Metasploit multi/handler 模块高级选项解析

multi/handler 是 Metasploit 框架中至关重要的模块&#xff0c;主要用于监听目标机的连接并处理来自目标的反向 shell 或会话。它可以灵活地适应不同渗透测试场景&#xff0c;提供高度的自定义选项以优化监听器的行为。 在 Metasploit msf6 框架中&#xff0c;当使用 exploit…

WPF高级 | WPF 应用程序部署与发布:确保顺利交付到用户手中

WPF高级 | WPF 应用程序部署与发布&#xff1a;确保顺利交付到用户手中 一、前言二、部署与发布基础概念2.1 部署的定义与目的2.2 发布的方式与渠道2.3 部署与发布的关键要素 三、WPF 应用程序打包3.1 使用 Visual Studio 自带的打包工具3.2 使用第三方打包工具 四、发布到不同…

Spring MVC 程序开发(1)

目录 1、什么是 SpringMVC2、返回数据2.1、返回 JSON 对象2.2、请求转发2.3、请求重定向2.4、自定义返回的内容 1、什么是 SpringMVC 1、Tomcat 和 Servlet 分别是什么&#xff1f;有什么关系&#xff1f; Servlet 是 java 官方定义的 web 开发的标准规范&#xff1b;Tomcat 是…

C# Unity 唐老狮 No.4 模拟面试题

本文章不作任何商业用途 仅作学习与交流 安利唐老狮与其他老师合作的网站,内有大量免费资源和优质付费资源,我入门就是看唐老师的课程 打好坚实的基础非常非常重要: 全部 - 游习堂 - 唐老狮创立的游戏开发在线学习平台 - Powered By EduSoho 如果你发现了文章内特殊的字体格式,…

Protocol Buffers在MCU上的nanopb介绍及使用详解

在嵌入式系统和资源受限的环境中&#xff0c;传统的Protocol Buffers 可能显得过于庞大。因此&#xff0c;nanopb 应运而生&#xff0c;它是一个轻量级的 Protocol Buffers 生成器&#xff0c;专为嵌入式系统设计c语言设计。本文将介绍如何安装和使用 nanopb&#xff0c;以及通…

(十 二)趣学设计模式 之 享元模式!

目录 一、 啥是享元模式&#xff1f;二、 为什么要用享元模式&#xff1f;三、 享元模式的实现方式四、 享元模式的优缺点五、 享元模式的应用场景六、 总结 &#x1f31f;我的其他文章也讲解的比较有趣&#x1f601;&#xff0c;如果喜欢博主的讲解方式&#xff0c;可以多多支…

Trae:国内首款AI原生IDE,编程效率大提升

今年一月&#xff0c;在新闻上看到字节跳动面向海外市场推出了一款名为Trae的AI集成开发环境&#xff08;IDE&#xff09;。起初&#xff0c;我并未给予过多关注&#xff0c;因为市面上已有不少IDE集成了AI插件&#xff0c;功能也非常全面&#xff0c;而字节跳动自家的MarsCode…

RocketMQ定时/延时消息实现机制

RocketMQ 的延迟消息是其核心特性之一&#xff0c;允许消息在指定延迟时间后才被消费者消费。 定时消息生命周期 一、延迟消息的核心机制 RocketMQ&#xff08;5.0之前&#xff09; 不支持任意时间精度的延迟&#xff0c;而是通过预定义的 延迟级别&#xff08;Delay Level&a…

基于SpringBoot的校园二手交易平台(源码+论文+部署教程)

运行环境 校园二手交易平台运行环境如下&#xff1a; • 前端&#xff1a;Vue • 后端&#xff1a;Java • IDE工具&#xff1a;IntelliJ IDEA&#xff08;可自行更换&#xff09; • 技术栈&#xff1a;SpringBoot Vue MySQL 主要功能 校园二手交易平台主要包含前台和…

利用 LangChain 和一个大语言模型(LLM)构建一个链条,自动从用户输入的问题中提取相关的 SQL 表信息,再生成对应的 SQL 查询

示例代码&#xff1a; from langchain_core.runnables import RunnablePassthrough from langchain.chains import create_sql_query_chain from operator import itemgetter from langchain.chains.openai_tools import create_extraction_chain_pydantic# 系统消息&#xff…

力扣hot 100之矩阵四题解法总结

本期总结hot100 中二维矩阵的题&#xff0c;时空复杂度就不分析了 1.矩阵置零 原地标记&#xff0c;用第一行和第一列作为当前行列是否为0的标记&#xff0c;同时用两个标签分别记录0行、0列的标记空间中原本是否有0 class Solution:def setZeroes(self, matrix: List[List[…

在Linux上使用APT安装Sniffnet的详细步骤

一、引言 Sniffnet 是一款开源的网络流量监控工具&#xff0c;适用于多种Linux发行版。如果你的Linux系统使用APT&#xff08;Advanced Package Tool&#xff09;作为包管理器&#xff0c;以下是如何通过APT安装Sniffnet的详细步骤。 二、系统要求 在开始安装之前&#xff0…

zookeeper-docker版

Zookeeper-docker版 1 zookeeper概述 1.1 什么是zookeeper Zookeeper是一个分布式的、高性能的、开源的分布式系统的协调&#xff08;Coordination&#xff09;服务&#xff0c;它是一个为分布式应用提供一致性服务的软件。 1.2 zookeeper应用场景 zookeeper是一个经典的分…