当前位置:   article > 正文

哈希表/哈希冲突及解决方法(较全)_字符串哈希冲突

字符串哈希冲突

哈希表的概念请参阅他人文章,关于哈希冲突的解决这篇文章基本都整理到了,还有几个常见的面试题。

前导(题外话):

哈希表的几个概念:

映像:由哈希函数得到的哈希表是一个映像

冲突:如果两个关键字的哈希函数值相等,这种现象称为冲突。

关于哈希函数的选取,可以参见 这篇文章

另外常见的字符串哈希函数及c++代码实现可以看这里

主要有:常用的字符串Hash函数还有ELFHash,APHash等等,都是十分简单有效的方法。这些函数使用位运算使得每一个字符都对最后的函数值产生影响。另外还有以MD5SHA1为代表的杂凑函数,这些函数几乎不可能找到碰撞。
常用字符串哈希函数有BKDRHash,APHash,DJBHash,JSHash,RSHash,SDBMHash,PJWHash,ELFHash等等。

BKDRHash无论是在实际效果还是编码实现中,效果都是最突出的。APHash也是较为优秀的算法。DJBHash,JSHash,RSHash与SDBMHash各有千秋。PJWHash与ELFHash效果最差,但得分相似,其算法本质是相似的。

好了,进入正题:
通过构造性能良好的哈希函数,可以减少冲突,但一般不可能完全避免冲突,因此解决冲突是哈希法的另一个关键问题。创建哈希表和查找哈希表都会遇到冲突,两种情况下解决冲突的方法应该一致。下面以创建哈希表为例,说明解决冲突的方法。常用的解决冲突方法有以下四种:

一.开放定址法(闭散列)

开放定址法
这种方法也称再散列法,其基本思想是:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。
查找时探查到开放的 地址则表明表中无待查的关键字,即查找失败
这种方法有一个通用的再散列函数形式:
Hi=(H(key)+di)% m i=1,2,…,n
其中H(key)为哈希函数,m 为表长,di称为增量序列。增量序列的取值方式不同,相应的再散列方式也不同。
在这里插入图片描述
这里只给出大致的思想,更详细的具体步骤和代码可以参见这里:
哈希表的原理及解决冲突的方法
另外,散列表的查找性能,一般有两种方法,具体可以看上面这篇文章介绍。

成功平均查找长度(ASLs)
ASLs: 查找表中关键词的平均查找比较次数(其冲突次数加1)
不成功平均查找长度 (ASLu)
ASLu:不在散列表中的关键词的平均查找次数(不成功)
一般方法:将不在散列表中的关键词分若干类。

解决哈希冲突主要有以下三种:

1.线性探测再散列

线性探测再散列
dii=1,2,3,…,m-1(m是tablesize
这种方法的特点是:冲突发生时,顺序查看表中下一单元,直到找出一个空单元或查遍全表。

2.二次探测再散列

二次探测再散列
di=12,-12,22,-22,…,k2,-k2 (2是平方) ( k<=m/2 )
这种方法的特点是:冲突发生时,在表的左右进行跳跃式探测,比较灵活。
(坏处是只能探测到哈希表长度的一半,会浪费一定空间,但若是真的探测到一半的位置了,说明哈希函数的设置有问题,应重选哈希函数)

3.伪随机探测再散列

伪随机探测再散列
di=伪随机数序列。

具体实现时,应建立一个伪随机数发生器,(如i=(i+p) % m),并给定一个随机数做起点。
例如,已知哈希表长度m=11,哈希函数为:H(key)= key % 11,则H(47)=3,H(26)=4,H(60)=5,假设下一个关键字为69,则H(69)=3,与47冲突。
如果用线性探测再散列处理冲突,下一个哈希地址为H1=(3 + 1)% 11 = 4,仍然冲突,再找下一个哈希地址为H2=(3 + 2)% 11 = 5,还是冲突,继续找下一个哈希地址为H3=(3 + 3)% 11 = 6,此时不再冲突,将69填入5号单元。
如果用二次探测再散列处理冲突,下一个哈希地址为H1=(3 + 12)% 11 = 4,仍然冲突,再找下一个哈希地址为H2=(3 - 12)% 11 = 2,此时不再冲突,将69填入2号单元。
如果用伪随机探测再散列处理冲突,且伪随机数序列为:2,5,9,………,则下一个哈希地址为H1=(3 + 2)% 11 = 5,仍然冲突,再找下一个哈希地址为H2=(3 + 5)% 11 = 8,此时不再冲突,将69填入8号单元。

二.再哈希法

再哈希法
这种方法是同时构造多个不同的哈希函数:
Hi=RH1(key) i=1,2,…,k
当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。

三.链地址法(开散列)

链地址法
这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。

将所有关键字为同义词的结点链接在同一个单链表中。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数
组T[0…m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。

介绍和代码实现

四.建立公共溢出区

建立公共溢出区
这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。

五.优缺点(重要)

1.开放散列(open hashing)/ 拉链法(针对桶链结构)

开放散列(open hashing)/ 拉链法(针对桶链结构)

1)优点: ①对于记录总数频繁可变的情况,处理的比较好(也就是避免了动态调整的开销) ②由于记录存储在结点中,而结点是动态分配,不会造成内存的浪费,所以尤其适合那种记录本身尺寸(size)很大的情况,因为此时指针的开销可以忽略不计了 ③删除记录时,比较方便,直接通过指针操作即可

2)缺点: ①存储的记录是随机分布在内存中的,这样在查询记录时,相比结构紧凑的数据类型(比如数组),哈希表的跳转访问会带来额外的时间开销如果所有的 key-value 对是可以提前预知,并之后不会发生变化时(即不允许插入和删除),可以人为创建一个不会产生冲突的完美哈希函数(perfect hash function),此时封闭散列的性能将远高于开放散列 ③由于使用指针,记录不容易进行序列化(serialize)操作

2.封闭散列(closed hashing)/ 开放定址法

优缺点

封闭散列(closed hashing)/ 开放定址法

1)优点: ①记录更容易进行序列化(serialize)操作 ②如果记录总数可以预知,可以创建完美哈希函数,此时处理数据的效率是非常高的

2)缺点: ①存储记录的数目不能超过桶数组的长度,如果超过就需要扩容,而扩容会导致某次操作的时间成本飙升,这在实时或者交互式应用中可能会是一个严重的缺陷
②使用探测序列,有可能其计算的时间成本过高,导致哈希表的处理性能降低 ③由于记录是存放在桶数组中的,而桶数组必然存在空槽,所以当记录本身尺寸(size)很大并且记录总数规模很大时,空槽占用的空间会导致明显的内存浪费 ④删除记录时,比较麻烦。比如需要删除记录a,记录b是在a之后插入桶数组的,但是和记录a有冲突,是通过探测序列再次跳转找到的地址,所以如果直接删除a,a的位置变为空槽,而空槽是查询记录失败的终止条件,这样会导致记录b在a的位置重新插入数据前不可见,所以不能直接删除a,而是设置删除标记。这就需要额外的空间和操作。
另外还有一篇:

(1)拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
(2)由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
(3)开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;
(4)在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。而对开放地址法构造的散列表,删除结点不能简单地将被删结点的空间置为空,否则将截断在它之后填人散列表的同义词结点的查找路径。这是因为各种开放地址法中,空地址单元(即开放地址)都是查找失败的条件。因此在用开放地址法处理冲突的散列表上执行删除操作,只能在被删结点上做删除标记,而不能真正删除结点。

附上常见的面试题:
首先是一个总览,从STL中的map和unordered_map

map与unordered_map
相同:两者都是键-值对的集合,关联容器的一种。两者中的元素都是pair,同时拥有实值和键值。两者都不允许有两个相同的键值(实值可以相同)。两个的外部接口调用基本一致。
不同:内部实现机理不同,即map内部实现了一个红黑树;unordered_map内部实现了一个哈希表。(两者的比较成为红黑树与哈希表的比较)。由于内部实现机理不同(底层实现)造成以下不同。
map的有序性:红黑树(非严格平衡二叉树),该结构具有自动排序的功能,因此map内部的所有元素都是有序的。
unordered_map的无序性:哈希表不会根据key值大小进行排序,存储时是根据key的hash值判断元素是否相同,因此unordered_map内部元素是无序的。
map的运行效率:红黑树可以在O(log n)时间内做查找,插入和删除,这里的n是树中元素的数目。
unordered_map的运行效率:哈希表的查找的时间复杂度可达到O(1) unordered_map内存占用比map高。 红黑树
定义:RB-tree是一个需满足以下规则的二叉搜索树 每个结点不是红色就是黑色 根结点为黑色 每个叶结点(空结点)是黑色的
每个红色结点的两个子结点都是黑色的 从任一节点到其每个叶子结点的所有路径都包含相同数目的黑色节点。 为什么红黑树是一种好的搜索树
一棵内部有n个结点的红黑树的高度至多为2∗logn(性质4)。这保证了红黑树任意操作的复杂度都是O(logn)。
基本操作:左旋,右旋,重新着色
目的:红黑树在插入,删除过程中可能会破坏原本的平衡条件导致不满足红黑树的性质,这时候一般情况下要通过左旋、右旋和重新着色这个三个操作来使红黑树重新满足平衡化条件。
左旋 右旋 重新着色 哈希表 定义:散列表(Hash table,也叫哈希表),是根据关键码值(Key
value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
哈希函数 直接寻址法 数字分析法、 平方取中法 折叠法 随机数法 除留余数法 解决碰撞(冲突): 开放寻址法:Hi=(H(key) +
di) MOD m,i=1,2,…,k(k<=m-1),其中H(key)为散列函数,m为散列表长,di为增量序列(1)线性探测
di=1,2,3,…,m-1;(2)二次探测
di=12,-12,22,-22,⑶2,…,±(k)2,(k<=m/2);(3)伪随机探测 di=伪随机数序列,

再散列法:Hi=RHi(key),i=1,2,…,k
RHi均是不同的散列函数,即在同义词产生地址冲突时计算另一个散列函数地址,直到冲突不再发生,这种方法不易产生“聚集”,但增加了计算时间。
链地址法:如果遇到冲突,会在原地址新建一个空间,然后以链表结点的形式插入到该空间。

查询性能: 散列函数是否均匀

处理冲突的方法

散列表的装填因子 :α= 填入表中的元素个数 / 散列表的长度

1.什么是哈希冲突?

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

2.处理哈希冲突的方法有哪些?优缺点分别是什么?

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

3.哈希表的增删查改的时间复杂度是多少?
开放定址法哈希冲突很严重怎么办?
哈希桶哈希冲突很严重怎么办?

查找索引当然会很快,不过只有无冲突的hash table复杂度才是O(1),一般是O(c),c为哈希关键字冲突时查找的平均长度。
开放地址法是当发生冲突时,使用某种探查技术在散列表中形成一个探查序列,按照这个序列逐个单元查找,直到找到合适的位置。线性探测法使得大量元素在相邻地址出现“聚集”现象,降低效率。主要是解决冲突算法选择不好,如果选择平方探测法即二次探测再散列,则可以有效减少堆积问题(难以避免)!
但还有情况;
线性探测法:一次聚集
平方探测法:二次聚集
哈希桶哈希冲突很严重时就退化成了单个单链表。(具体怎么做Mark一下)

这篇文章有介绍的一些有关运算

4.海量数据处理的面试题?

位图,布隆过滤器等等
参见这里

5.unordered_map和map的区别是什么?哪个更好一些?

unordered_map和map类似,都是存储的key-value的值,可以通过key快速索引到value。不同的是unordered_map不会根据key的大小进行排序,

他们在以下的几个方面有区别:

内部实现

内存使用

时间复杂度

内部实现

std::map将元素存储在一个平衡二叉树中,所以元素是有序存储的。

std::unordered_map使用哈希表来存储元素,元素并不是有序存储。

内存使用

unordered_map比ordered_map更占用内存,因为需要额外的内存来存储哈希表。 查找时间复杂度

std::map的查找时间复杂度是O(log n)。

std::unordered_map最佳的查找时间复杂度是O(1),如果哈希函数不是很好的话,最糟糕的复杂度会是O(n)。

什么时候选择map:

当你需要低内存占用率

当你希望序列是有序的

当你需要稳定的表现

什么时候选择unordered_map

当你有一个很好的哈希函数和对内存占用率没有限制时

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/688799
推荐阅读
相关标签
  

闽ICP备14008679号