彻底理解哈希表(HashTable)结构哈希游戏
哈希游戏作为一种新兴的区块链应用,它巧妙地结合了加密技术与娱乐,为玩家提供了全新的体验。万达哈希平台凭借其独特的彩票玩法和创新的哈希算法,公平公正-方便快捷!万达哈希,哈希游戏平台,哈希娱乐,哈希游戏
,从而实现高效的查找、插入和删除操作。哈希表通常被用在需要快速查找的场景,例如数据库索引、缓存和符号表等
哈希表本质上就是一个数组,只不过数组存放的是单一的数据,而哈希表中存放的是键值对(key - value pair)
在哈希表中,每个键(key)与一个值(value)相关联,并且是通过键来查找值的
查找速度快:在哈希函数设计合理的情况下,哈希表可以在常数时间内完成查找、插入和删除操作
空间浪费:为了减少碰撞,哈希表的容量通常要比实际存储的元素数量多,造成了一定的空间浪费
哈希函数设计复杂:哈希函数的设计对哈希表的性能至关重要,设计不当的哈希函数会导致大量碰撞,影响性能
存储结构: 哈希表通常由一个数组和一个哈希函数组成。数组的每个元素称为桶(Bucket),它可以存储一个或多个键-值对
哈希化(Hashing):是将输入数据(通常是任意长度的)通过哈希函数转换为固定长度的输出(数组范围内下标)的过程
哈希函数(Hash Function): 哈希函数将键转换成一个数组的索引(即一个整数)
哈希冲突(Hash Collision): 当不同的键被哈希函数映射到相同的索引时,就发生了哈希冲突。常用的解决哈希冲突的方式有链地址法和开放地址法
装填因子(Load Factor):哈希表中存储的元素数量与数组大小(总槽位数量)的比率
装填因子表示哈希表空间的利用效率。较高的装填因子意味着表中存储的元素较多,空间利用率高
当装填因子增加(接近于 1 或更高)时,哈希表的查找效率可能降低,尤其是在使用开放地址法时,冲突处理需要更多的探测次数
在动态哈希表中,当装填因子超过某个阈值(如 0.75),通常会触发哈希表的扩容,以减少冲突和提高性能
开放地址法:理想的装填因子通常低于 1,推荐在0.5 到 0.75之间,以平衡空间利用和查找效率
链地址法:装填因子可以超过 1,因为多个元素可以存储在同一个槽中。通常装填因子在0.7 到 1.0是较好的选择,但也可以更高,具体取决于链表长度对性能的影响
扩容(Rehashing):当哈希表的装载因子超过设定值时,可能需要进行扩容。扩容通常包括创建一个更大的数组,并重新计算所有键的哈希值,以将它们映射到新数组中
缩容:缩容是指在数据量减少到一定程度时,减少哈希表的容量,以节省空间并提高内存利用效率
快速的计算:哈希表的优势就在于效率,所以快速获取到对应的hashCode非常重要
均匀的分布:哈希表中无论是链地址法还是开放地址法,当多个元素映射到同一个位置的时候,都会影响效率,优秀的哈希函数应该尽可能将元素映射到不同的位置,让元素在哈希表中均匀的分布
下面的三个模块知识都是为了理解计算问题,为了快速地获取对应的hashCode
归根结底哈希函数就是将键key转换为索引值,能作为键key的类型有很多数字、字符串、对象等等,我们这里主要是学习字符串作为键,因为哈希表的键通常被实现为字符串,主要是出于以下几个原因:
一致性:使用字符串作为键可以提供一致的方式来处理不同类型的键。即使是数字和其他类型,最终也会转换为字符串进行存储
哈希函数:哈希表使用哈希函数将键映射到数组的索引。字符串通常更易于处理,因为它们可以直接进行哈希运算,而其他类型(如对象)可能需要更多的处理步骤
易于比较:字符串具有明确的比较规则,这使得查找、插入和删除操作变得更简单和高效
空间效率:在许多语言中,字符串作为键的存储方式通常比对象或其他复杂类型更紧凑,减少了内存使用
灵活性:字符串可以表示各种信息,能够轻松适应不同的场景,例如数据库索引、字段名称等
理解了键的类型,我们能想到就是哈希函数要将字符串转换为数字,那这有什么方法那?
方法一:将字符的ASCII值相加,问题就是很多键最终的值可能都是一样的,比如was/tin/give/tend/moan/tick都是43
方法二:使用一个常数乘以键的每个字符的ASCII值,得到的数字可以基本保证它的唯一性(下面解释),和别的单词重复率大大降低,问题是得到的值太大了,因为这个值是要作为索引的,创建这么大的数组是没有意义的
其实我们平时使用的大于10的数字,可以用一种幂的连乘来表示它的唯一性: 比如:7654 = 7*10³ + 6*10² + 5*10 + 4
那么对于获取到的索引太大这个问题又出现了压缩算法,即把幂的连乘方案中得到的巨大整数范围压缩到可接受的数组范围中
如何压缩呢?有一种简单的方法就是使用取余操作符,它的作用是得到一个数被另外一个数整除后的余数
举个例子理解:先来看一个小点的数字 范围压缩到一个小点的空间中,假设把从0~199的数字,压缩为从0~9的数字
同理数组的索引是有限的,比如从0到n-1(其中n是可接受的数组长度),通过对进行取余操作,可以将它们限制在这个范围内
设hash是通过哈希函数得到的键的哈希值,n是数组的长度,索引可以通过index = hash % n计算得出
在某些情况下,哈希值可能是负数。为了确保得到的索引是非负的,可以使用以下公式:index = ((hash % n) + n) % n
通过变换可以得到一种快得多的算法,即解决这类求值问题的高效算法霍纳法则,在中国被称为秦九韶算法
在设计哈希表时,已经处理映射到相同下标值的情况:链地址法或者开放地址法(文章后面详细讲解)
但是无论哪种方案都是为了提高效率,最好的情况还是让数据在哈希表中均匀分布
质数和其他数相乘的结果相比于其他数字更容易产生唯一性的结果,减少哈希冲突
Java中的HashMap的N次幂的底数选择的是31,比较常用的数是31或37,是经过长期观察分布结果得出的
如果使用10作为模数(数组长度)取模,那么所有以0或5结尾的数,都会被映射到同一个槽位
但如果使用质数11,则可以有效地避免这种情况,因为11没有因数是2或5,它能使得键值分布更加均匀
比如经过哈希化后demystify和melioration得到的下标相同,那怎么放入数组中那?就是当不同的键被哈希函数映射到相同的索引时,就发生了哈希冲突。常用的解决哈希冲突的方式有链地址法和开放地址法
链地址法的核心思想是将每个数组元素作为一个桶,桶内存储多个键值对。通常使用链表或数组来实现
当查询时先根据哈希化后的下标值找到对应的位置,再取出链表,依次查询找寻找的数据
哈希表有N个数据项,有arraySize个槽位,每个槽位中的链表长度大致相同,怎么计算每个链表的平均长度
假设arraySize = 10(哈希表中有10个槽位),N = 50(哈希表中有50个数据项),平均每个链表中大约有5个数据项(通过将总的数据项数N除以槽位数arraySize来得到,公式就是装填因子loadFactor)
计算哈希值并定位到槽位:这一步是一个常数时间操作,通常是1次操作,不依赖链表的长度
遍历链表查找元素:如果多个元素在同一个槽位上,哈希表会在这个槽位的链表中查找元素。查找的平均次数是链表长度的一半,通常是loadFactor / 2
因此成功可能只需要查找链表的一半即可:1 + loadFactor/2,不成功可能需要将整个链表查询完才知道不成功:1 + loadFactor
链地址法相对来说效率是好于开放地址法的,所以在真实开发中使用链地址法的情况较多
它不会因为添加了某元素后性能急剧下降,比如在Java的HashMap中使用的就是链地址法
经过哈希化得到的index = 5,但是在插入的时候,发现该位置已经有了49
首先经过哈希化得到index = 5,看5的位置结果和查询的数值是否相同
相同那么就直接返回,不相同就线性查找,从index + 1位置开始查找和38一样的
删除操作一个数据项时,不可以将这个位置下标的内容设置为null,为什么呢?
当看到-1位置的数据项时,就知道查询时要继续查询,但是插入时这个位置可以放置数据
比如在没有任何数据的时候,插入的是22-23-24-25-26,那么意味着下标值:0-1-2-3-4的位置都有元素,这种一连串填充单元就叫做聚集
聚集会影响哈希表的性能,无论是插入/查询/删除都会影响,比如我们插入一个38,会发现连续的单元都不允许放置数据,并且在这个过程中需要探索多次
二次探测对步长做了优化,比如从下标值x开始x+1²,x+2²,x+3²,这样就可以一次性探测比较长的距离,比避免那些聚集带来的影响
不能输出为0(否则将没有步长,每次探测都是原地踏步,算法就进入了死循环)
随着数据量的增多,每一个index对应的bucket会越来越长,也就造成效率的降低,所以在合适的情况对数组进行扩容,比如扩容两倍
扩容可以简单的将容量增大两倍,但是这种情况下所有的数据项一定要同时进行修改(重新调用哈希函数,来获取到不同的位置)
缩容时通常将哈希表容量减小为当前容量的一半,并重新计算并分配所有现有元素的位置
loadFactor>
0.75的时候进行扩容,比如Java的哈希表就是在装填因子大于0.75的时候,对哈希表进行扩容
比如 0.25,低于此值时触发缩容,但也要避免过度缩容:通常会设置一个较小的、合理的最小容量,比如 7 或 8,以保证哈希表即使在元素很少时也保持一定的容量,从而避免频繁地扩容和缩容
理清楚实现的方案和结构,这样当我们实现操作方法时才能更清晰的知道要做什么
使用数组实现,因为数组是js/ts已经提供给我们的数据结构(想用链表后面自己练习吧,需要自己再实现链表结构,参考文章post/743181…)
get(name)时,只需查找数组中第一项等于name的元组,取第二项作为值