数据结构 哈希表

数据结构 哈希表

文章目录

  • 数据结构 哈希表
    • 1. 概念
    • 2. 冲突-概念
    • 3. 冲突-避免
      • 3.1 哈希函数设计
      • 3.2 负载因子调节
    • 4.冲突-解决
      • 4.1 闭散列
      • 4.2 开散列(哈希桶)
      • 4.3 哈希桶实现
    • 5. 性能分析
    • 6. 和java类集的关系

1. 概念

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

理想的搜索方法:

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

当向该结构中:

  • 插入元素

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

  • 搜索元素

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

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

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

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

在这里插入图片描述

用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较块

但是,如果按照上述哈希方法向集合中插入元素44,却会出现问题,为什么?往下看

2. 冲突-概念

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

我们把具有不同关键码而具有相同哈希地址的数据元素称为"同义词"

3. 冲突-避免

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

3.1 哈希函数设计

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

哈希函数设计原则:

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

常见哈希函数

  1. 直接定制法

    取关键字的某个线性函数为散列地址:Hash(Key) = A * Key + B

    优点:简单、均匀 缺点:需要事先知道关键字的分布情况

    使用场景:适合查找比较小且连续的情况

    代码示例:

    class Solution {public int firstUniqChar(String s) {int[] count = new int[26];for (int i = 0;i < s.length();i++) {count[s.charAt(i) - 97]++;}for (int i = 0;i < s.length();i++) {if (count[s.charAt(i) - 97] == 1) {return i;}}return -1;}}
    

    在这里插入图片描述

  2. 除留余数法

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

注:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突

3.2 负载因子调节

在这里插入图片描述

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

在这里插入图片描述

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

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

4.冲突-解决

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

4.1 闭散列

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

那么如何寻找下一个空位置呢?

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

  1. 线性探测

    • 插入

      • 通过哈希函数获取待插入元素在哈希表中的位置

      • 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素

        在这里插入图片描述

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

  2. 二次探测

    线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后去找,因此二次探测为了避免该问题,找下一个空位置的方法为:Hi = (H0 + i²) % m,或者:Hi = (H0 - i²) % m。其中:i = 1,2,3…,H0是通过散列函数Hash(x)对元素的关键码key进行计算得到的位置,m是表的大小。对于2.1中如果要插入44,产生冲突,使用解决后的情况为:

    在这里插入图片描述

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

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

4.2 开散列(哈希桶)

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

在这里插入图片描述

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

开散列(哈希桶),可以认为是把一个在大集合中的搜索问题转化为在小集合中做搜索

当冲突比较严重时,小集合的搜索性能解决效果也不高,这个时候

我们需要将这个所谓的小集合搜索问题继续进行转换,例如:

  1. 每个桶的背后是另一个哈希表
  2. 每个桶的背后是一棵搜索树

4.3 哈希桶实现

代码示例:

package demo1;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 static final float DEFAULT_LOAD_FACTOR = 0.75f;public HashBuck() {array = new Node[10];}/*插入元素*/public void put(int key, int val) {int index = key % array.length;Node cur = array[index];while(cur != null) {if (cur.key == key) {cur.val = val;return;}cur = cur.next;}// 遍历完后说明没有找到key元素,利用头插法将元素加进桶中Node newNode = new Node(key, val);newNode.next = array[index];array[index] = newNode;usedSize++;//若负载因子超过0.75,扩容if (doLoadFact() > 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) {Node temp = cur.next;int newIndex = cur.key % newArray.length;//采用头插法,将节点插入新数组的newIndex下标中cur.next = newArray[newIndex];newArray[newIndex] = cur;cur = temp;}}array = newArray;}private float doLoadFact() {return usedSize*1.0f / array.length;}/*获取元素*/public int get(int key) {int Index = key % array.length;Node cur = array[Index];while(cur != null) {if (cur.key == key) {return cur.val;}cur = cur.next;}return -1;}
}

在这里插入图片描述

5. 性能分析

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

6. 和java类集的关系

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

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

STM32不使用 cubeMX实现外部中断

这篇文章将介绍如何不使用 cubeMX完成外部中断的配置和实现。 文章目录 前言一、文件加入工程二、代码解析exti.cexti.hmain.c 注意&#xff1a;总结 前言 实验开发板&#xff1a;STM32F103C8T6。所需软件&#xff1a;keil5 &#xff0c; cubeMX 。实验目的&#xff1a;如何不…

1024程序员狂欢节有好礼 | 前沿技术、人工智能、集成电路科学与芯片技术、新一代信息与通信技术、网络空间安全技术

&#x1f339;欢迎来到爱书不爱输的程序猿的博客, 本博客致力于知识分享&#xff0c;与更多的人进行学习交流 1024程序员狂欢节有好礼 &#x1f6a9;&#x1f6a9;&#x1f6a9;点击直达福利前言一、IT技术 IT Technology《速学Linux&#xff1a;系统应用从入门到精通》《Pytho…

基于springboot+vue网上图书商城54

大家好✌&#xff01;我是CZ淡陌。一名专注以理论为基础实战为主的技术博主&#xff0c;将再这里为大家分享优质的实战项目&#xff0c;本人在Java毕业设计领域有多年的经验&#xff0c;陆续会更新更多优质的Java实战项目&#xff0c;希望你能有所收获&#xff0c;少走一些弯路…

顶级玩家:一招搞定 App 自动化老大难问题

很多人在学习 App 自动化或者在项目中落地实践 App 自动化时&#xff0c;会发现编写的自动化脚本无缘无故的执行失败、不稳定。 而导致其问题很大原因是因为应用的各种弹窗&#xff08;升级弹窗、使用过程提示弹窗、评价弹窗等等&#xff09;&#xff0c;比如这样的&#xff1a…

深入篇【Linux】学习必备:进程环境变量/进程切换

深入篇【Linux】学习必备&#xff1a;进程环境变量/进程切换 Ⅰ.环境变量Ⅱ.深层意义Ⅲ.全局属性Ⅳ.进程切换 Ⅰ.环境变量 1.环境变量是什么&#xff1f;&#xff1a;环境变量是系统提供的一组name/value形式的变量&#xff0c;不同的环境变量有不同的用户。 一般是用来指定操作…

YOLOV8目标检测——最全最完整模型训练过程记录

文章目录 前言1 下载yolov8&#xff08;[网址](https://github.com/ultralytics/ultralytics)&#xff09;2 配置conda环境3 用pycharm打开文件3 训练自己的YOLOV8数据集4 run下运行完了之后没有best.pt文件5 导出为onnx文件6 yolov8应用完整案例&#xff08;免费且包含源代码、…

springboot 程序设计优雅退出

一 springboot优雅退出 1.1 概述 在springboot2.3版本后&#xff0c;实现了优雅退出功能。当server.shutdowngraceful启用时&#xff0c;在 web 容器关闭时&#xff0c;web 服务器将不再接收新请求&#xff0c;并将剩余活动执行完成给设置一个缓冲期。缓冲期 timeout-per-shu…

基础MySQL的语法练习

基础MySQL的语法练习 create table DEPT(DEPTNO int(2) not null,DNAME VARCHAR(14),LOC VARCHAR(13) );alter table DEPTadd constraint PK_DEPT primary key (DEPTNO);create table EMP (EMPNO int(4) primary key,ENAME VARCHAR(10),JOB VARCHAR(9),MGR …

100 # mongoose 的使用

mongoose elegant mongodb object modeling for node.js https://mongoosejs.com/ 安装 mongoose npm i mongoose基本示例 const mongoose require("mongoose");// 1、连接 mongodb let conn mongoose.createConnection("mongodb://kaimo313:kaimo313loc…

城市正视图(Urban Elevations, ACM/ICPC World Finals 1992, UVa221)rust解法

如图5-4所示&#xff0c;有n&#xff08;n≤100&#xff09;个建筑物。左侧是俯视图&#xff08;左上角为建筑物编号&#xff0c;右下角为高度&#xff09;&#xff0c;右侧是从南向北看的正视图。 输入每个建筑物左下角坐标&#xff08;即x、y坐标的最小值&#xff09;、宽度…

Linux基础命令1——Linux的命令格式与命令分类

目录 Linux命令格式 Linux命令分类 如何判断命令的类型——Type命令 内置命令 外部命令 alias命令 命令的执行效率与过程 Linux命令格式 命令格式 完整的命令格式分为三部分&#xff1a;命令、参数、对象 其中命令与参数、参数与参数、参数与对象之间最少要有一个空格做…

有哪些比较好用的协同办公软件

在疫情期间&#xff0c;协同办公大放异彩&#xff0c;解决了很多公司线上办公的问题&#xff0c;所以在后疫情时代&#xff0c;协同办公软件成为了提高工作效率和团队协作的重要工具。 随着科技的不断进步&#xff0c;越来越多的协同办公软件涌现出来。在本文中&#xff0c;我们…