如何解决Hash冲突

Hash冲突

产生Hash冲突的原因

例子:

你可以把hash想象成一个数组,现在你想把一个数据存到hash表中。那么问题来了:这个数据应该存到哪里?
于是,你需要一个hash函数,这个函数的作用就是把你要存的数据映射成hash表中的一个位置,这个位置就是你要存放该数据的地方。一般把hash表的每个位置都叫做“槽(slot)”,很形象,你要往槽里放数据。假如你要存的数据为k,存放在哪个槽里呢?很简单,存在hash(k)这个槽里。
这个hash函数是你自己选的。这里我以《算法导论》里面的一个题目举例:现在你选的hash函数是这样的:
hash(k) = k % 9
假设hash表的大小为9(即有9个槽),现在要把一串数据存到表里:5,28,19,15,20,33,12,17,10
简单计算一下:hash(5)=5, 所以数据5应该放在hash表的第5个槽里;hash(28)=1,所以数据28应该放在hash表的第1个槽里;hash(19)=1,也就是说,数据19也应该放在hash表的第1个槽里——于是就造成了碰撞(也称为冲突,collision)。

Hash算法:

  • Hash算法可以将一个数据转换为一个标志,这个标志和源数据的每一个字节都有十分紧密的关系。Hash算法还具有一个特点,就是很难找到逆向规律。
  • Hash算法是一个广义的算法,也可以认为是一种思想,使用Hash算法可以提高存储空间的利用率,可以提高数据的查询效率,也可以做数字签名来保障数据传递的安全性。所以Hash算法被广泛地应用在互联网应用中。
  • Hash算法也被称为散列算法,Hash算法虽然被称为算法,但实际上它更像是一种思想。Hash算法没有一个固定的公式,只要符合散列思想的算法都可以被称为是Hash算法。

基本概念:

  • 若在表结构中存在和关键字K相等的记录,则记录必定在f(K)的存储位置上。由此,不需比较便可直接取得所查记录。称这个对应关系f为散列函数(Hash function),这个事先建立的表为散列表。

  • 对不同的关键字可能得到同一散列地址,即:key1≠key2,而f(key1)=f(key2),这种现象称碰撞(冲突)。具有相同函数值的关键字对该散列函数来说称做同义词。综上所述,根据散列函数H(key)和处理冲突的方法将一组关键字映射到一个有限的连续的地址集(区间)上,并以关键字在地址集中的“象” 作为记录在表中的存储位置,这种表便称为散列表,这一映象过程称为散列造表或散列,所得的存储位置称散列地址。

  • 若对于关键字集合中的任一个关键字,经散列函数映象到地址集合中任何一个地址的概率是相等的,则称此类散列函数为均匀散列函数(Uniform Hash function),这就是使关键字经过散列函数得到一个“随机的地址”,从而减少冲突。

如何解决Hash冲突?

  1. 开放定址法

为产生冲突的地址H(key)求得一个地址序列:

H0, H1, H2, …, Hs

其中:

H0 = H(key)
Hi = (H(key) + di) % m

扫描二维码关注公众号,回复: 8765956 查看本文章

其中m为表的长度,i = 1, 2, 3, …, n,对增量di有三种取法:

  • 线性探测再散列

di = 1, 2, 3, …, m - 1

  • 二次探测再散列

di = 12, -12, 22, -22, 32, -32, …, k2, -k2 (k <= m / 2)

  • 伪随机探测再散列

di = 伪随机数序列 (具体实现时,应建立一个伪随机数发生器,(如i = (i + p) % m),并给定一个随机数做起点。)

使用示例:
问题:已知哈希表长度m = 11,哈希函数为:H(key) = key % 11,则H(47) = 3,H(26) = 4,H(60) = 5,假设下一个关键字为69,则H(69) = 3,与H(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号单元。

  1. 链地址法(拉链法)

这种方法的基本思想是将所有哈希地址为 i 的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第 i 个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。
Java 中 HashMap 解决 Hash 冲突就是利用了这个方法,具体实现这里暂时不做详解,可以参考 Jdk HashMap 源码进行理解。

  1. 再哈希法

这种方法是同时构造多个不同的哈希函数:

Hi = RHi(key)          (i = 1,2,…,k)

当哈希地址Hi = RHi(key) 发生冲突时,再计算Hi = RHi(key) ……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。

  1. 建立公共溢出区

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

优缺点分析

这里针对开放定址法及链地址法进行分析:

开放散列(open hashing)/链地址法

  1. 优点

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

  1. 缺点

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

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

  1. 优点

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

  1. 缺点

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

持续更新中…

发布了42 篇原创文章 · 获赞 10 · 访问量 7055

猜你喜欢

转载自blog.csdn.net/MCJPAO/article/details/102693182