文章目录
起因
昨天注册了leetcode账号,简简单单的题目,以执行时间和内存消耗作为衡量算法优劣的指标,大神们就各种炫技,用哈希表的人居然很多,我看不懂。。。So。昨天晚上看了半个小时就是看一个别人貌似牛逼闪闪的一个函数,两项指标均打败100%的参与者(因此被推荐到了靠前的位置供大家欣赏),最后被我找出了问题,测试出了程序是错的,leetcode平台测试用例少,没有测出错。哼,嘚瑟得不行,所以,哈希表,我来啦~
为了解决什么问题
在一个表中查找有没有存在某个元素,一般都是循环遍历,直到表中的元素值等于要查找的数,如果数组时顺序排列可以用二分法查找。但是如果数组元素没有排序,但又希望能快速查找,怎么办?比如一个元素乱序排列的数组有10000个元素,我想查找的元素处在地832个,怎么查找更快速?
当我们在编程过程中,往往需要对线性表进行查找操作。在顺序表中查找时,需要从表头开始,依次遍历比较a[i]与key的值是否相等,直到相等才返回索引i;在有序表中查找时,我们经常使用的是二分查找,通过比较key与a[i]的大小来折半查找,直到相等时才返回索引i。最终通过索引找到我们要找的元素。但是,这两种方法的效率都依赖于查找中比较的次数。我们有一种想法,能不能不经过比较,而是直接通过关键字key一次得到所要的结果呢?这时,就有了散列表查找(哈希表)。
解决的思路
要说哈希表,我们必须先了解一种新的存储方式—散列技术。
散列技术是指在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使每一个关键字都对应一个存储位置。即:存储位置=f(关键字)。这样,在查找的过程中,只需要通过这个对应关系f 找到给定值key的映射f(key)。只要集合中存在关键字和key相等的记录,则必在存储位置f(key)处。我们把这种对应关系f 称为散列函数或哈希函数。
按照这个思想,采用散列技术将记录存储在一块连续的存储空间中,这块连续的存储空间称为哈希表。所得的存储地址称为哈希地址或散列地址。
比如最简单的,数组元素值和数组的索引号能一一对应,这种一一对应的关系,可能简单,可能麻烦,对应关系就是哈希函数关系。
哈希函数构造法
通常哈希函数不会像上面学号那么简单,可能需要其他的对应关系。那是什么对应关系呢?如何构造?
构造原则
- 计算简单;
- 散列地址分布均匀。
参考因素
①计算哈希地址所需的时间;
②关键字的长度;
③哈希表的大小;
④关键字的分布情况;
⑤查找的频率。
直接定制法(不常用)
取关键字或关键字的某个线性函数值为哈希地址:
即:H(key) = key 或 H(key) = a*key+b
优点:简单,均匀,不会产生冲突;
缺点:需要实现直到关键字的分布情况,适合查找表比较小且连续的情况。 产生的哈希表会很长。
数字分析法
假设关键字集合中的每个关键字key都是由s位数字组成(k1,k2,……,knk1,k2,……,kn k_1,k_2,……,k_nk1,k2,……,kn),分析key中的全体数据,并从中提取分布均匀的若干位或他们的组合构成全体
使用举例:
我们知道身份证号是有规律的,现在我们要存储一个班级学生的身份证号码,假设这个班级的学生都出生在同一个地区,同一年,那么他们的身份证的前面数位都是相同的,那么我们可以截取后面不同的几位存储,假设有5位不同,那么就用这五位代表地址。
H(key)=key%100000
此种方法通常用于数字位数较长的情况,必须数字存在一定规律,其必须知道数字的分布情况,比如上面的例子,我们事先知道这个班级的学生出生在同一年,同一个地区。
平方取中法
如果关键字的每一位都有某些数字重复出现频率很高的现象,可以先求关键字的平方值,通过平方扩大差异,而后取中间数位作为最终存储地址。
使用举例
比如key=1234 1234^2=1522756 取227作hash地址
比如key=4321 4321^2=18671041 取671作hash地址
这种方法适合事先不知道数据并且数据长度较小的情况
折叠法
如果数字的位数很多,可以将数字分割为几个部分,取他们的叠加和作为hash地址
使用举例
比如key=123 456 789
我们可以存储在61524,取末三位,存在524的位置
该方法适用于数字位数较多且事先不知道数据分布的情况
除留余数法(常用)
H(key)=key MOD p (p<=m m为表长)
很明显,如何选取p是个关键问题。
使用举例
比如我们存储3 6 9,那么p就不能取3
因为 3 MOD 3 == 6 MOD 3 == 9 MOD 3
p应为不大于m的质数或是不含20以下的质因子的合数,这样可以减少地址的重复(冲突)
随机数法
选择一个随机数,取关键字的随机函数值作为他的哈希地址。
即:f(key) = random (key)
当关键字的长度不等时,采用这个方法构造哈希函数较为合适。当遇到特殊字符的关键字时,需要将其转换为某种数字。这里的random应该用seed,否则查找的时候对应关系变了。
这个没理解。。。
解决冲突的办法
不同key值产生相同的地址,H(key1)=H(key2)
比如我们上面说的存储3 6 9,p取3是
3 MOD 3 == 6 MOD 3 == 9 MOD 3
此时3 6 9都发生了hash冲突
哈希冲突不能避免,所以我们需要找到方法来解决它。
哈希冲突的解决方案主要有四种:开放地址法;再哈希;链地址法;公共溢出区法。
进行再探测(开放地址法)
开放地址法就是指:一旦发生了冲突就去寻找下一个空的哈希地址,只要哈希表足够大,空的散列地址总能找到,并将记录存入。
公式:Hi=(H(*key) + Di) mod m (i = 1,2,3,….,k k<=m-1)
其中:H(key)为哈希函数;m为哈希表表长;Di为增量序列,有以下3中取法来随机探测:
①Di = 1,2,3,…,m-1, 称为线性探测再散列;
②Di = 1²,-1²,2²,-2²,。。。,±k²,(k<= m/2)称为二次探测再散列
③Di = 伪随机数序列,称为伪随机数探测再散列。
例如:在长度为12的哈希表中插入关键字为38的记录:
从上述线性探测再散列的过程中可以看出一个现象:当表中i、i+1位置上有记录时,下一个哈希地址为i、i+1、i+2的记录都将填入i+3的位置,这种本不是同义词却要争夺同一个地址的现象叫“堆积“。即在处理同义词的冲突过程中又添加了非同义词的冲突;但是,用线探测再散列处理冲突可以保证:只要哈希表未填满,总能找到一个不发生冲突的地方。
建立一个缓冲区
把凡是拼音重复的人放到缓冲区中。当我通过名字查找人时,发现找的不对,就在缓冲区里找。
再哈希法
公式:Hi = RHi(key) i = 1,2,…,k
Hi均是不同的哈希函数,意思为:当繁盛冲突时,使用不同的哈希函数计算地址,直到不冲突为止。这种方法不易产生堆积,但是耗费时间。
就是当冲突时,采用另外一种映射方式来查找。
应用举例:d-left hashing中的d是多个的意思,我们先简化这个问题,看一看2-left hashing。2-left hashing指的是将一个哈希表分成长度相等的两半,分别叫做T1和T2,给T1和T2分别配备一个哈希函数,h1和h2。在存储一个新的key时,同 时用两个哈希函数进行计算,得出两个地址h1[key]和h2[key]。这时需要检查T1中的h1[key]位置和T2中的h2[key]位置,哪一个 位置已经存储的(有碰撞的)key比较多,然后将新key存储在负载少的位置。如果两边一样多,比如两个位置都为空或者都存储了一个key,就把新key
存储在左边的T1子表中,2-left也由此而来。在查找一个key时,必须进行两次hash,同时查找两个位置。
链地址法
将所有关键字为同义字的记录存储在一个单链表中,我们称这种单链表为同义词子表,散列表中存储同义词子表的头指针。
如关键字集合为{19,14,23,01,68,20,84,27,55,11,10,79},按哈希函数H(key) = key mod 13;
链地址法解决了冲突,提供了永远都能找到地址的保证。但是,也带来了查找时需要遍历单链表的性能损耗。
公共溢出区法
即设立两个表:基础表和溢出表。将所有关键字通过哈希函数计算出相应的地址。然后将未发生冲突的关键字放入相应的基础表中,一旦发生冲突,就将其依次放入溢出表中即可。
查找时,先用给定值通过哈希函数计算出相应的散列地址后,首先 首先与基本表的相应位置进行比较,如果不相等,再到溢出表中顺序查找。
哈希表
在设定的哈希函数和设定的冲突处理方法,将查找表中的各元素存储在一段有限长的连续空间中,这个连续空间就是哈希表。
哈希表原来这么简单,有点儿失望,之前看一个视频中提到了一两句哈希表和链表,讲课老师说比较复杂比较麻烦,一直觉得很复杂呢,没想到这么简单,甚至有点儿low。。。。失望哦。。。
查找效率比较
哈希表是一个在空间和时间上做出权衡的经典例子。如果没有内存限制,那么可以直接将键作为数组的索引。那么所查找的时间复杂度为O(1);如果没有时间限制,那么我们可以使用无序数组并进行顺序查找,这样只需要很少的内存。哈希表使用了适度的时间和空间来在这两个极端之间找到了平衡。只需要调整哈希函数算法即可在时间和空间上做出取舍。
决定hash表查找的ASL因素:
1)选用的hash函数
2)选用的处理冲突的方法
3)hash表的饱和度,装载因子 α=n/m(n表示实际装载数据长度 m为表长)
一般情况,假设hash函数是均匀的,则在讨论ASL时可以不考虑它的因素
hash表的ASL是处理冲突方法和装载因子的函数
前人已经证明,查找成功时如下结果:
可以看到无论哪个函数,装载因子越大,平均查找长度越大,那么装载因子α越小越好?也不是,就像100的表长只存一个数据,α是小了,但是空间利用率不高啊,这里就是时间空间的取舍问题了。通常情况下,认为α=0.75是时间空间综合利用效率最高的情况。
上面的这个表可是特别有用的。假设我现在有10个数据,想使用链地址法解决冲突,并要求平均查找长度<2
那么有1+α/2 <2
α<2
即 n/m<2 (n=10)
m>10/2
m>5 即采用链地址法,使得平均查找长度< 2 那么m>5
之前我的博客讨论过各种树的平均查找长度,他们都是基于存储数据n的函数,而hash表不同,他是基于装载因子的函数,也就是说,当数据n增加时,我可以通过增加表长m,以维持装载因子不变,确保ASL不变。
那么hash表的构造应该是这样的:
hash表的删除
首先链地址法是可以直接删除元素的,但是开放定址法是不行的,拿前面的双探测再散列来说,假如我们删除了元素1,将其位置置空,那 23就永远找不到了。正确做法应该是删除之后置入一个原来不存在的数据,比如-1
参考来源链接
https://blog.csdn.net/u011109881/article/details/80379505
https://www.bilibili.com/video/BV11W41187eR
https://blog.csdn.net/weixin_38820375/article/details/100582693