【数据结构】 哈希表 详解
目录
- 1. 概念 引入
- 2. 冲突
- 2.1 概念
- 2.2 避免
- 2.3 冲突-避免-哈希函数设计
- 2.4 冲突-避免-负载因子调节(重点)
- 2.5 冲突-解决
- 2.5.1 闭散列
- 2.6 冲突-解决-开散列/哈希桶 (数组+链表)
- 2.7 冲突严重时的解决办法
- 3. key-val值假设都为int型的代码实现
- 4. 性能分析
- 5.与Java类集的关系(代码举列)
1. 概念 引入
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O( logN),搜索的效率取决于搜索过程中
元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
当向该结构中
- 插入元素 :根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
- 搜索元素:对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(HashTable)(或者称散列表)
例如:数据集合{1,7,6,4,5,9};
哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。
用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快
2. 冲突
2.1 概念
对于两个数据元素的关键字 和 (i != j),有 != ,但有:Hash( ) == Hash( ),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。
2.2 避免
首先,我们需要明确一点,由于我们哈希表底层数组的容量往往是小于实际要存储的关键字的数量的,这就导致一个问题,冲突的发生是必然的,但我们能做的应该是尽量的降低冲突率。
2.3 冲突-避免-哈希函数设计
引起哈希冲突的一个原因可能是:哈希函数设计不够合理。 哈希函数设计原则:
- 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
- 哈希函数计算出来的地址能均匀分布在整个空间中
- 哈希函数应该比较简单
常见哈希函数
- 直接定制法–(常用)
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B 优点:简单、均匀 缺点:需要事先知道关键字的分布情况 使用场景:适合查找比较小且连续的情况 面试题:字符串中第一个只出现一次字符 - 除留余数法–(常用)
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:
Hash(key) = key% p(p<=m),将关键码转换成哈希地址 - 平方取中法–(了解)
假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址; 再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址 平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况 - 折叠法–(了解)
折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。
折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况 - 随机数法–(了解)
选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中random为随机数函数。
通常应用于关键字长度不等时采用此法 - 数学分析法–(了解)
设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。
数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀的情况
哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突
2.4 冲突-避免-负载因子调节(重点)
所以当冲突率达到一个无法忍受的程度时,我们需要通过降低负载因子来变相的降低冲突率。
已知哈希表中已有的关键字个数是不可变的,那我们能调整的就只有哈希表中的数组的大小。
2.5 冲突-解决
解决哈希冲突两种常见的方法是:闭散列和开散列
2.5.1 闭散列
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。
寻找下一个空位置的方法 :
- 线性探测
比如上面的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,下标为4,因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
- 插入
- 通过哈希函数获取待插入元素在哈希表中的位置
- 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素
- 采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。
- 二次探测(采用特定公式避免数据紧挨在一起放置)
线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为:Hi = ( H0+i^2 )% m, 或者:Hi
= (H0 -i^2 )% m。其中:i = 1,2,3…, H0是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。
研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容。
因此:比散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。
2.6 冲突-解决-开散列/哈希桶 (数组+链表)
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
2.7 冲突严重时的解决办法
哈希桶其实可以看作将大集合的搜索问题转化为小集合的搜索问题了,那如果冲突严重,就意味着小集合的搜索性能其实也时不佳的,这个时候我们就可以将这个所谓的小集合搜索问题继续进行转化,例如:
- 每个桶的背后是另一个哈希表
- 每个桶的背后是一棵搜索树
3. key-val值假设都为int型的代码实现
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 double DEFAULT_LOAD_FACTOR=0.75;public HashBuck(){this.array=new Node[10];}/*** put函数* @param key* @param val*/public void put(int key,int val){//1.找到key所在的位置int index=key%this.array.length;//2.遍历这个下标的链表,看是不是有相同的key 有 更新val值Node cur=array[index];while (cur!=null){if (cur.key==key){cur.val=val;//更新val的值return;}cur=cur.next;}//3.没有这个key的话,采用头插法插入Node node=new Node(key,val);node.next=array[index];array[index]=node;this.usedSize++;//4.插入元素成功后,检查当前散列表的负载因子if (loadFactor()>=DEFAULT_LOAD_FACTOR){}}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;//获取新的下标//重新哈希:就是把cur这个节点,以头插/尾插的形式 插入到新的数组对应下标的链表当中Node curNext=cur.next;cur.next=newArray[index];//先绑定后面newArray[index]=cur;//再绑定前面cur=curNext;}}array=newArray;}private double loadFactor(){return 1.0*usedSize/array.length;}/*** get函数* 根据key获取val的值* @param key* @return*/public int get(int key){//1.找到key所在的位置int index=key%this.array.length;//2.获取valNode cur=array[index];while (cur!=null){if (cur.key==key){return cur.val;}cur=cur.next;}return -1;}
}
扩容前:
扩容后:(重新哈希后再放置元素 )
4. 性能分析
虽然哈希表一直在和冲突做斗争,但在实际使用过程中,我们认为哈希表的冲突率是不高的,冲突个数是可控的,也就是每个桶中的链表的长度是一个常数,所以,通常意义下,我们认为哈希表的插入/删除/查找时间复杂度是
O(1) 。
5.与Java类集的关系(代码举列)
- HashMap 和 HashSet 即 java 中利用哈希表实现的 Map 和 Set
- java 中使用的是哈希桶方式解决冲突的
- java 会在冲突链表长度大于一定阈值后,将链表转变为搜索树(红黑树)
- java 中计算哈希值实际上是调用的类的 hashCode 方法,进行 key 的相等性比较是调用 key 的 equals 方法。所以如果要用自定义类作为 HashMap 的 key 或者 HashSet 的值,必须重写 hashCode 和 equals 方 法,而且要做到 equals 相等的对象,hashCode 一定是一致的;
- hashcode一样,equals不一定一样!
- equals一样,hashcode一定一样!
代码举列如下:
import java.util.HashMap;
import java.util.Objects;/*** Created with IntelliJ IDEA.* User: 12629* Date: 2022/2/22* Time: 21:32* Description:*/
class Person { //自定义person类public String ID;public Person(String ID) {this.ID = ID;}@Override // 重写equals方法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 //重写hashcode方法public int hashCode() {return Objects.hash(ID);}@Overridepublic String toString() {return "Person{" +"ID='" + ID + '\'' +'}';}
}
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.val = val;this.key = key;}}public Node<K,V>[] array = (Node<K,V>[])new Node[10];public int usedSize;public void put(K key,V val) {int hash = key.hashCode();//转换为一个整数 int index = hash % array.length;Node<K,V> cur = array[index];while (cur != null) {if(cur.key.equals(key)) {cur.val = val;//更新val值return;}cur = cur.next;}Node<K,V> node = new Node<>(key, val);node.next = array[index];array[index] = node;this.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(cur.key.equals(key)) {//更新val值return cur.val;}cur = cur.next;}return null;}public static void main(String[] args) {//我们认为 身份证ID一样的两个人是同一个人 //通过对hashcode和equals方法的重写 可以实现这一逻辑//重写hashcode之后,字符串类型的ID相同的话生成的整数就是相同的//实现了ID一样的两个人是同一人这一逻辑Person person1 = new Person("123");Person person2 = new Person("123");HashBuck2<Person,String> hashBuck2 = new HashBuck2<>();hashBuck2.put(person1,"love");System.out.println(hashBuck2.get(person2));}}
因为person1和person2是同一个人
所以get person2的val其实就是放入person1的love
- over
【数据结构】 哈希表 详解相关推荐
- 判断数组中某个元素除自身外是否和其他数据不同_算法工程师要懂的3种算法数据结构:线性表详解...
算法思想有很多,业界公认的常用算法思想有8种,分别是枚举.递推.递归.分治.贪心.试探法.动态迭代和模拟.当然8种只是一个大概的划分,是一个"仁者见仁.智者见智"的问题. 其实这些 ...
- 【数据结构】哈希表详解以及代码实现
目录 1.来源: 2.哈希函数 1.哈希函数的设计规则 2.哈希函数的设计思路 3.哈希碰撞 4.解决哈希碰撞的方案 5.负载因子 3.基于开散列方案的HashMap实现 1.HashMap类中的属性 ...
- 【数据结构和算法】哈希表详解
哈希函数构造方法: (1)直接定址法: (2)除留余数法: 解决哈希冲突的办法: 如果出现哈希冲突,循环向右找空的内存单元,作为新的哈希地址 线性探查法的缺点: 堆积现象(非哈希冲突):同义词争 ...
- 【数据结构】顺序表详解 | 从零开始步步解读 | 画图理解并调试分析
前言: 本章节将对顺序表的概念进行介绍,着重讲解动态顺序表.对常用的接口函数进行一个个讲解,并进行解析.顺序表讲解部分将从零实现顺序表接口函数,遇到问题我会进行一步步地调试说明,通过对本章的学习不仅能 ...
- 数据结构——哈希表的详解与实现
数据结构--哈希表(HashTable) 1.前言 当我们频繁的查找数据中的某个元素时,我们通常会选择数组来存放数据,因为数组的的内存是连续的,可以直接通过下标访问数据,但是它添加和删除数据比较麻 ...
- c++数据结构中 顺序队列的队首队尾_数据结构与算法—队列详解
前言 栈和队列是一对好兄弟,前面我们介绍过数据结构与算法-栈详解,那么栈的机制相对简单,后入先出,就像进入一个狭小的山洞,山洞只有一个出口,只能后进先出(在外面的先出去).而队列就好比是一个隧道,后面 ...
- mysql数据库表分区_MySQL数据库之MySQL的分区和分表详解
本文主要向大家介绍了MySQL数据库之MySQL的分区和分表详解 ,通过具体的内容向大家展现,希望对大家学习MySQL数据库有所帮助. 1. 分区 MySQL中的分区是指将一个数据表按照某种规则(如时 ...
- 二叉树,平衡二叉树,B-Tree,B+Tree,跳表详解
二叉树,平衡二叉树,B-Tree,B+Tree,跳表详解 1.二叉查找树(BST) 1.1 二叉查找树概念 1.2 二叉查找树特点 2. 平衡二叉树(AVL) 2.1 平衡二叉树概念 2.2 平衡二叉 ...
- HBase数据结构与基本语法详解
HBase数据结构与基本语法详解.背景: 阅读新闻 [日期:2019-01-06] 来源:Linux社区 作者:Linux [字体:大 中 小] HBase中的表一般有这样的特点: 1 大:一个表可以 ...
最新文章
- hdu - 1087 - Super Jumping! Jumping! Jumping!
- Web生产:外部JS文件中的绝对URL?
- 如何在SAP社区上查找其他SAP从业者遇到的问题或者提新问题
- Android学习第十三天----ScrollView
- uva 10891 - Game of Sum
- 每日算法系列【LeetCode 1250】检查「好数组」
- MacOS 软件 Adobe Photoshop 2022 安装使用详细教程
- 【JZOJ】1183:【入门】判断是否构成三角形
- 在java中class是什么意思_java 中Class? 中的?代表什么意思?
- SPARC架构下的反汇编(三)——SPARC汇编语言
- 吃我一记咸鱼突刺——使用板载RTC定时开机
- ARM920T内存管理单元MMU
- Java中对象转换为字符串的几种方式
- python做网站开发_如何用Python做网站开发
- 微信特殊字符php,php 获取微信昵称时 过滤特殊字符
- TCP协议用socket连接时客户端请求服务器read()一直阻塞解决
- 免费代理池(proxy_pool)搭建教程
- python组合键触发_Python3 tkinter基础 Frame bind 捕捉多键同时按
- 该如何学习三维点云配准的相关知识?
- ucore lab5
热门文章
- Python上下文语法with小述
- re.sub()用法详解
- python进程执行带有参数的任务(args、kwargs)
- 2006年上半年 网络工程师 上下午试卷【附带答案】
- not syncing : corrupted stack end detected inside scheduler解决办法 以及高版本的激活码!
- Python学习笔记.OS学习笔记 OS操作系统(operating system)(二)程序和进程 输出进程号,工作目录最有用!
- 人人必须要知道的语义分割模型:DeepLabv3+
- [Math]理解卡尔曼滤波器 (Understanding Kalman Filter)
- day12-mysql 增删改查
- MySQL主从的一致性校验及修复