【数据结构】哈希/散列表

目录

  • 一、哈希表的概念
  • 二、哈希冲突
    • 2.1 冲突概念
    • 2.2 冲突避免
      • 2.2.1 方式一哈希函数设计
      • 2.2.2 方式二负载因子调节
    • 2.3 冲突解决
      • 2.3.1 闭散列
      • 2.3.2 开散列(哈希桶)
    • 2.4 性能分析
  • 三、实现简单hash桶
    • 3.1 内部类与成员变量
    • 3.2 插入
    • 3.3 获取value值
    • 3.4 总代码
    • 四、与Java集合类的关系
    • 五、练习

一、哈希表的概念

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

哈希方法:

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

哈希函数:哈希方法中使用的转换函数。

哈希表(HashTable):构造出来的结构。

举个栗子:
存储数据关键字为{1,4,5,6,7,9}的数据使用哈希表,我们设置哈希函数为:关键字%容量。因此在得到每个关键字时对容量取余,得到存储下标,放入哈希表得到以下结构。
这样下次搜素时也只需要对关键字取余找到对应下标就搜到了。

二、哈希冲突

2.1 冲突概念

不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。

例如:在上面的例子中如果在插入关键字为44的值就与4在一个下标,就发生了哈希冲突。

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

2.2 冲突避免

我们哈希表底层数组的容量往往是小于实际要存储的关键字的数量的,这就导致冲突的发生是必然的,但我们能做到尽量的降低冲突发生率。

2.2.1 方式一哈希函数设计

这种方法一般用不到,因为哈希函数一般不由小卡拉米来定制。

满足以下三个条件来设计:

  • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1
    之间;
  • 哈希函数计算出来的地址能均匀分布在整个空间中;
  • 哈希函数应该比较简单;

常见哈希函数:

  1. 直接定制法:
    取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
    优点:简单、均匀;
    缺点:需要事先知道关键字的分布情况 ;
    使用场景:适合查找比较小且连续的情况。
  2. 除留余数法:
    设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,
    按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址。
  3. 平方取中法:
    假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址。
    使用场景:不知道关键字的分布,而位数又不是很大的情况。
  4. 折叠法:
    折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。
    使用场景:事先不需要知道关键字的分布,适合关键字位数比较多的情况。
  5. 随机数法:
    选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中random为随机数函数。
    使用场景:应用于关键字长度不等时采用此法。
  6. 数学分析法:
    设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定相同,可能在某
    些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据
    散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。
    使用场景:处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均
    匀的情况。

2.2.2 方式二负载因子调节

负载因子:就是 哈希表中的元素个数 与 哈希表的长度 的比值。

负载因子与冲突发生率的关系图:

由图可知,负载因子大小与与冲突率呈正相关。
所以我们可以通过降低负载因子来降低冲突率。Java中的负载因子设置的是0.75。

2.3 冲突解决

2.3.1 闭散列

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

找下一个位置的两种方法:

  1. 线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。使用这种方式缺点就是产生冲突的数据容易堆积在一块。
    而且采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。线性探测采用标记的伪删除法来删除一个元素。
  2. 二次探测:在设计一个函数来表示下一个位置,找下一个空位置的方法为:Hi = (H0 + i^2 )% m, 或者:Hi = ( H0 - i^2 )% m。其中:i = 1,2,3…, 是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。

2.3.2 开散列(哈希桶)

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

冲突严重时的解决方式:

  1. 每个桶的背后是另一个哈希表;
  2. 每个桶的背后是一棵搜索树(Java中的HashMap集合类就是每个桶后面是一颗红黑树)。

2.4 性能分析

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

三、实现简单hash桶

我们自己实现一个简单的hash桶即HashBuck。
先写key与value都是int 类型,再类比写出泛型参数类型。

3.1 内部类与成员变量

hash桶的结构是数组加链表的形式,

  • 所以我们内部类要构造一个链表,
  • 成员要用实现链表数组,我们在这初始化长度为10。
  • 在使用一个usedSize变量记录一下节点的个数,以便调控负载因子。
  • 使用一个常量来表示负载因子最大值
	//内部类,链表
    static class Node {
        public int key;
        public int value;
        public Node next;

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

    }
    //初始数组
    public Node[] array = new Node[10];
    //使用长度
    public int usedSize;
    //负载因子最大值
    private static final double DEFAULT_LOAD_FACTOR = 0.75f;

泛型参数版本:

	static class Node <K,V>{
        public K key;
        public V value;
        Node<K,V> next;

        public Node(K key, V value) {
            this.key = key;
            this.value = value;
        }
    }
    //初始数组
    public Node<K,V>[] array =  (Node<K,V>[])new Node[10];
    //使用长度
    public int usedSize;
    //负载因子最大值
    private static final double DEFAULT_LOAD_FACTOR = 0.75f;

3.2 插入

实现put方法,我们插入节点的时候,

  • 先使用hash函数(key % array.length)找到当前节点该插入的下标。
  • 然后再遍历这里面的节点,如果其中关键字key已经存在,我们就更新一下value即可;
  • 没有key关键字的节点,我们就尾插进当前节点,
  • 要判断当前是否是空,所以我们使用prev,来指向cur的前一个节点,如果prev为空,就说明当前的下标中没有节点。
  • 然后节点数量usedSize加1。
  • 我们节点个数加一之后,我们就要考虑负载因子是否超了,如果负载因子超了,就扩容。
    //插入元素
    public void put(int key, int value) {
        int index = key % array.length;
        Node cur = array[index];
        Node prev = cur;
        while(cur != null) {
            //关键字key已经存在
            if(cur.key == key) {
                cur.value = value;
                return;
            }
            prev = cur;
            cur = cur.next;
        }
        //关键字key不存在
        if(prev == null) {
            array[index] = new Node(key,value);
        }else {
            prev.next = new Node(key,value);
        }
        this.usedSize++;
        //判断负载因子,是否扩容
        if( loadFactor() >= DEFAULT_LOAD_FACTOR) {
            resize();
        }
    }
    //计算负载因子
    private double loadFactor() {
        return 1.0*usedSize / array.length;
    }

泛型参数版本:

//插入元素
    public void put(K key, V value) {
        int index = key.hashCode() % array.length;
        Node<K,V> cur = array[index];
        Node<K,V> pre = cur;
        while(cur != null) {
            //关键字key已经存在
            if(cur.key.equals(key)) {
                cur.value = value;
            }
            pre = cur;
            cur = cur.next;
        }
        //关键字key不存在
        if(pre == null) {
            array[index] = new Node<>(key,value);
        } else {
            pre.next = new Node<>(key,value);
        }
        this.usedSize++;
        //判断负载因子,是否扩容
        if(loadFactor() >= DEFAULT_LOAD_FACTOR) {
            resize();
        }
    }
    //计算负载因子
    private double loadFactor() {
        return 1.0*usedSize / array.length;
    }

扩容:

  • 在扩容中由于hash函数的存在,我们不能直接使用copyOf直接扩容。
  • 由于数组长度变了,所以每个节点该插入的位置也要变化。例如:在数组长度为10时,关键字14和4的节点都要在4下标,而当数组长度变为20的时候,关键字14和4的节点分别在14和4下标。
  • 所以我们要遍历原数组中的每一个节点,将其插入新数组中。
	//扩容
    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) {
                int index = cur.key % newArray.length;
                //保存下一个节点
                Node newCur = cur.next;
                //插入新数组
                cur.next = newArray[index];
                newArray[index] = cur;
                //下一个节点
                cur = newCur;
            }
        }
        this.array = newArray;
    }

泛型参数版本:

//扩容
    private void resize() {
        Node<K,V>[] newArray = (Node<K, V>[]) new Node[array.length * 2];
        //遍历每一个节点
        for(int i = 0; i < array.length; i++) {
            Node<K,V> cur = array[i];
            //插入
            while (cur != null) {
                int index = cur.key.hashCode() % newArray.length;
                //保存下一个节点
                Node<K,V> newCur = cur.next;
                //插入新数组
                cur.next = newArray[index];
                newArray[index] = cur;
                //下一个节点
                cur = newCur;
            }
        }
        this.array = newArray;
    }

3.3 获取value值

获取关键字key的value值:

  • 直接使用hash函数,拿到key所在的下标位置,
  • 直接遍历下标中的所有节点,找到返回该节点value值,
  • 没有返回-1。
    //获取关键字key的val值
    public int getVal(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;
    }

泛型参数版本:

	//获取关键字key的val值
    public V getVal(K key) {
        int index = key.hashCode() % array.length;
        Node<K,V> cur = array[index];
        //遍历
        while(cur != null) {
            if(cur.key == key) {
                return cur.value;
            }
            cur = cur.next;
        }
        return null;
    }

3.4 总代码

int参数版本:

import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

public class HashBuck {

    //内部类,链表
    static class Node {
        public int key;
        public int value;
        public Node next;

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

    }
    //初始数组
    public Node[] array = new Node[10];
    //使用长度
    public int usedSize;
    //负载因子最大值
    private static final double DEFAULT_LOAD_FACTOR = 0.75f;
    //插入元素
    public void put(int key, int value) {
        int index = key % array.length;
        Node cur = array[index];
        Node prev = cur;
        while(cur != null) {
            //关键字key已经存在
            if(cur.key == key) {
                cur.value = value;
                return;
            }
            prev = cur;
            cur = cur.next;
        }
        //关键字key不存在
        if(prev == null) {
            array[index] = new Node(key,value);
        }else {
            prev.next = new Node(key,value);
        }
        this.usedSize++;
        //判断负载因子,是否扩容
        if( loadFactor() >= DEFAULT_LOAD_FACTOR) {
            resize();
        }
    }

    //扩容
    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) {
                int index = cur.key % newArray.length;
                //保存下一个节点
                Node newCur = cur.next;
                //插入新数组
                cur.next = newArray[index];
                newArray[index] = cur;
                //下一个节点
                cur = newCur;
            }
        }
        this.array = newArray;
    }
    //计算负载因子
    private double loadFactor() {
        return 1.0*usedSize / array.length;
    }


    //获取关键字key的val值
    public int getVal(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;
    }
}

泛型参数版本:

public class HashBuck <K,V>{

    static class Node <K,V>{
        public K key;
        public V value;
        Node<K,V> next;

        public Node(K key, V value) {
            this.key = key;
            this.value = value;
        }
    }
    //初始数组
    public Node<K,V>[] array =  (Node<K,V>[])new Node[10];
    //使用长度
    public int usedSize;
    //负载因子最大值
    private static final double DEFAULT_LOAD_FACTOR = 0.75f;

    //插入元素
    public void put(K key, V value) {
        int index = key.hashCode() % array.length;
        Node<K,V> cur = array[index];
        Node<K,V> pre = cur;
        while(cur != null) {
            //关键字key已经存在
            if(cur.key.equals(key)) {
                cur.value = value;
            }
            pre = cur;
            cur = cur.next;
        }
        //关键字key不存在
        if(pre == null) {
            array[index] = new Node<>(key,value);
        } else {
            pre.next = new Node<>(key,value);
        }
        this.usedSize++;
        //判断负载因子,是否扩容
        if(loadFactor() >= DEFAULT_LOAD_FACTOR) {
            resize();
        }
    }
    //计算负载因子
    private double loadFactor() {
        return 1.0*usedSize / array.length;
    }

    //扩容
    private void resize() {
        Node<K,V>[] newArray = (Node<K, V>[]) new Node[array.length * 2];
        //遍历每一个节点
        for(int i = 0; i < array.length; i++) {
            Node<K,V> cur = array[i];
            //插入
            while (cur != null) {
                int index = cur.key.hashCode() % newArray.length;
                //保存下一个节点
                Node<K,V> newCur = cur.next;
                //插入新数组
                cur.next = newArray[index];
                newArray[index] = cur;
                //下一个节点
                cur = newCur;
            }
        }
        this.array = newArray;
    }
    //获取关键字key的val值
    public V getVal(K key) {
        int index = key.hashCode() % array.length;
        Node<K,V> cur = array[index];
        //遍历
        while(cur != null) {
            if(cur.key == key) {
                return cur.value;
            }
            cur = cur.next;
        }
        return null;
    }
}

四、与Java集合类的关系

  1. HashMap 和 HashSet 即 java 中利用哈希表实现的 Map 和 Set;
  2. java 中使用的是哈希桶方式解决冲突的;
  3. java 会在冲突链表长度大于一定阈值后,将链表转变为搜索树(红黑树);
  4. java 中计算哈希值实际上是调用的类的 hashCode 方法,进行 key 的相等性比较是调用 key 的 equals 方法。所以如果要用自定义类作为 HashMap 的 key 或者 HashSet 的值,必须覆写 hashCode 和 equals 方法,而且要做到 equals 相等的对象,hashCode 一定是一致的。

五、练习

1. 只出现一次的数字
2.复制带随机指针的链表
3.宝石与石头
4.坏键盘打字
5.前K个高频单词

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Go语言基础语法

一、创建工程 说明&#xff1a; &#xff08;1&#xff09;go.mod文件是go项目依赖管理文件&#xff0c;相当于前端的package.json&#xff0c;也就是Java项目中的Maven的pom.xml。 二、打印数据到控制台 &#xff08;1&#xff09;引入fmt &#xff08;2&#xff09;使用fmt…

class com.alibaba.fastjson2.JSONObject cannot be cast to class com.ruoyi.sys

class com.alibaba.fastjson2.JSONObject cannot be cast to class com.ruoyi.sys ry-cloud报错原因解决 ry-cloud 报错 系统监控→在线用户打开后报错 报错信息如下 class com.alibaba.fastjson2.JSONObject cannot be cast to class com.ruoyi.sys原因 type导致&#xff…

用 Python 从零开始创建神经网络(一)

用 Python 从零开始创建神经网络&#xff08;一&#xff09; 引言1. A Single Neuron&#xff1a;Example 1代码部分&#xff1a; Example 2代码部分&#xff1a; 2. A Layer of Neurons&#xff1a;Example 1代码部分&#xff1a; 引言 本教程专为那些对神经网络已有基础了解…

双指针算法习题解答

1.移动零 题目链接&#xff1a;283. 移动零 - 力扣&#xff08;LeetCode&#xff09; 题目解析&#xff1a;该题要求将数组中为0的元素全部转移到数组的末尾&#xff0c;同时不能改变非零元素的相对位置。 解题思路&#xff1a;我们可以用变量dest和cur将该数组分为三个区域。…

思源笔记轻松连接本地Ollama大语言模型,开启AI写作新体验!

文章目录 前言1. 下载运行Ollama框架2. Ollama下载大语言模型3. 思源笔记设置连接Ollama4. 测试笔记智能辅助写作5. 安装Cpolar工具6. 配置Ollama公网地址7. 笔记设置远程连接Ollama8. 固定Ollama公网地址 前言 今天我们要聊聊如何通过cpolar内网穿透技术&#xff0c;把国产笔…

SAP ABAP开发学习——WDA 五 使用表格控件实例

目录 实现 先建一个Web Dynpro Component 将两个view关联 input_view中添加按钮 output_view创建按钮 创建一个服务 input_view中使用向导创建两个输入框 output部分创建输出表单 output inbound 创建APPLICATION 效果 实现 先建一个Web Dynpro Component 将两个vi…

qt QCompleter详解

1、概述 QCompleter是Qt框架中的一个类&#xff0c;用于为文本输入提供自动完成功能。它可以与Qt的输入控件&#xff08;如QLineEdit、QTextEdit等&#xff09;结合使用&#xff0c;根据用户的输入实时过滤数据源&#xff0c;并在输入控件下方或内部显示补全建议列表。用户可以…

数据采集-Kepware连接倍福(Beckhoff)PLC(OPCUA协议)

KepserverEX 连接倍福(beckhoff)-ADS协议 系列文章目录 数据采集-Kepware 安装证书异常处理 数据采集-Kepware OPCUA 服务器实现 数据采集-Kepware连接倍福(Beckhoff)PLC(ADS协议) 目录 KepserverEX 连接倍福(beckhoff)-ADS协议系列文章目录前言一、OPC UA&#xff08;OPC统一…

vue中html如何转成pdf下载,pdf转base64,忽略某个元素渲染在pdf中,方法封装

一、下载 html2Canvas jspdf npm install jspdf html2canvas二、封装转换下载方法 htmlToPdf.js import html2Canvas from html2canvas import JsPDF from jspdf/*** param {*} reportName 下载时候的标题* param {*} isDownload 是否下载默认为下载&#xff0c;传false不…

接口测试面试题及答案(后续)

一、你们什么时候测试接口 一般有需求就会做&#xff0c;后台的接口开发好&#xff0c;就可以开始测。例外&#xff0c;如果增加了新需求&#xff0c;也要做接口测试&#xff0c;还有就是开发对后台的接口做了修改&#xff0c;交互逻辑发生变化&#xff0c;我们也要重新对接口…

萤石设备视频接入平台EasyCVR多品牌摄像机视频平台海康ehome平台(ISUP)接入EasyCVR不在线如何排查?

随着智慧城市和数字化转型的推进&#xff0c;视频监控系统已成为保障公共安全、提升管理效率的重要工具。特别是在大中型项目中&#xff0c;跨区域的网络化视频监控需求日益增长&#xff0c;这要求视频监控管理平台不仅要具备强大的视频资源管理能力&#xff0c;还要能够适应多…

使用Qt制作一个流程变更申请流程进度以及未读消息提醒

1.1加载界面&#xff1a; 界面要素&#xff1a; 成员信息 变更位置申请 接受消息列表 根据角色加载对应界面。 1.2发起变更申请&#xff1a; 用户点击“发起变更申请”按钮。变更申请对话框可编辑&#xff0c;用户填写申请信息&#xff1a; 申请方&#xff08;自动填充&…

域名邮箱推荐:安全与稳定的邮件域名邮箱!

域名邮箱推荐及绑定攻略&#xff1f;最好用的域名邮箱服务推荐&#xff1f; 域名邮箱&#xff0c;作为一种个性化且专业的电子邮件服务&#xff0c;越来越受到企业和个人的青睐。烽火将详细介绍域名邮箱登录的全过程&#xff0c;从注册到登录&#xff0c;帮助您轻松掌握这一重…

IDEA:设置类标签栏多行显示

使用场景&#xff1a; 当我们打开的类超出一行&#xff0c;多出来的类会隐藏或者关掉&#xff0c;不利于我们开发。 解决方案&#xff1a; 1.设置多行显示 2.效果

高级图像处理工具

图像处理-高级 1、功能概览 随着社交媒体的普及和个人创作需求的增长&#xff0c;图像处理成为了日常生活中不可或缺的一部分。无论是专业的设计师还是爱好者&#xff0c;都需要一款强大的工具来帮助他们完成各种任务。今天&#xff0c;我们将介绍一款基于Python开发的高级图…

江协科技STM32学习- P38 软件SPI读写W25Q64

&#x1f680;write in front&#x1f680; &#x1f50e;大家好&#xff0c;我是黄桃罐头&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流 &#x1f381;欢迎各位→点赞&#x1f44d; 收藏⭐️ 留言&#x1f4dd;​…

P5665 [CSP-S2019] 划分

P5665 [CSP-S2019] 划分 难度&#xff1a;省选/NOI-。 考点&#xff1a;单调队列、贪心、前缀和。 题意&#xff1a; 没有题目大意&#xff0c;本题题目描述较长&#xff0c;认真阅读每一个信息。 ​ 这个题的样例有 n n n 组数据&#xff0c;数据从 1 ∼ n 1 \sim n 1∼n…

ThreadX在STM32上的移植:F1,F4通用启动文件tx_initialize_low_level.s

在嵌入式系统开发中&#xff0c;实时操作系统&#xff08;RTOS&#xff09;的选择对于系统性能和稳定性至关重要。ThreadX是一种广泛使用的RTOS&#xff0c;它以其小巧、快速和可靠而闻名。在本文中&#xff0c;我们将探讨如何将ThreadX移植到STM32微控制器上&#xff0c;特别是…

RTT 内核基础学习

RT-Thread 内核介绍 内核是操作系统的核心&#xff0c;负责管理系统的线程、线程间通信、系统时钟、中断以及内存等。 内核位于硬件层之上&#xff0c;内核部分包括内核库、实时内核实现。 内核库是为了保证内核能够独立运行的一套小型的类似C库的函数实现子集。 这部分根据编…

qt QPixmapCache详解

1、概述 QPixmapCache是Qt框架中提供的一个功能强大的图像缓存管理工具类。它允许开发者在全局范围内缓存QPixmap对象&#xff0c;从而有效减少图像的重复加载&#xff0c;提高图像加载和显示的效率。这对于需要频繁加载和显示图像的用户界面应用来说尤为重要&#xff0c;能够…