当前位置:首页 » 《资源分享》 » 正文

哈希表(HashTable),哈希冲突的避免、解决_有裂痕的石头的博客

14 人参与  2021年10月15日 08:03  分类 : 《资源分享》  评论

点击全文阅读


文章目录

  • 什么是哈希表
    • 哈希表概念
  • 哈希冲突
    • 哈希冲突概念
    • 解决冲突
      • 闭散列
        • 闭散列平均查找次数的问题
      • 开散列/哈希桶
      • 冲突严重时的解决办法
    • 避免冲突
      • 哈希函数设计
        • 常见的哈希函数
      • 负载因子调节

什么是哈希表

先举一个很常见的例子:
我们有一个衣柜
在这里插入图片描述
这是一个杂乱无章的衣柜,里面放了四季的衣服,每当要去找一件合适的衣服去穿的时候,要翻箱倒柜麻烦半天,为了解决这个问题,我们买了四个柜子,规定它们分别存放春夏秋冬四季的衣服:
在这里插入图片描述
从此之后找衣服就方便了很多,什么季节去哪个衣柜找就能找到合适的衣服,这个例子背后就是哈希表的原理。

哈希表概念

在顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找的时间复杂度为o(N),平衡树中为树的高度,即O(log2N),搜索的效率取决于搜索过程中元素的比较次数。
我们理想的搜索方法是可以不经过任何比较,一次直接从表中得到要搜索的元素。
如果构造一种存储结构,通过某种函数使得元素的存储位置与它的关键码之间能够建立一一对应的关系,那么在查找时通过该函数可以很快找到该元素。
当向该结构中:

  • 插入元素
    根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行处存放
  • 搜索元素
    对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功

上述方法就是哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为韩系表(Hash Table)或者称为散列表。
举个例子:
数据集合:{ 1, 7, 6, 5, 9 }
哈希函数设置为:hash(key) = key % capacity;capacity为存储元素底层空间总大小
在这里插入图片描述

用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快。
可此时出现了这样一个问题,继续向上面这个集合插入元素44,会出现什么问题?
很明显[4]号为已经有4了,此时遇到的这个问题叫做哈希冲突。

哈希冲突

哈希冲突概念

不同的关键字,通过相同的哈希函数计算出相同的哈希地址,这种现象称为哈希冲突或者哈希碰撞。
遇到冲突当然就是要解决冲突了。

解决冲突

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

闭散列

闭散列:也叫开放定址法,当发生哈希冲突是,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以吧Key存放到冲突位置中的“下一个”空位置中去,那么如何寻找下一个空位置呢?常见的有以下两种解决方法:

  1. 线性探测
    比如上面的场景,现在需要插入元素44时,先通过哈希函数计算哈希地址,下标为4,因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。线性探测法就是从发生冲突的位置开始,依次向后探测,直到找到下一个空位置为止,像上面例子44最后就被插到下标为8的位置:
    在这里插入图片描述

  2. 二次探测
    线性探测法的缺陷是产生冲突的数据堆积在一块,当要插入数字8时,它不能插入到本来通过计算得到的下标8处,要一直寻找空位到下标0,当数据堆积的多的时候无疑增加了操作的时间,二次探测法为了缓解这个问题,找下一个空位的方法是:第一次往后找找1²个,第二次找2²个,第三次找3²个。。。。。。

当然还有三次探测法,四次探测法等等。

闭散列平均查找次数的问题

例题:
在这里插入图片描述
关键字集合放入到哈希表中:
在这里插入图片描述

  1. 平均成功查找次数:
    成功查找次数就是对于一个关键字要比较几次才能找到他,比如要找19,哈希地址为8,去下标8找19,第一次就找到了,1这就是19的成功查找次数,对于11,哈希地址为0,去下标0找没有,1找没有,2找没有直到5才找到,那么一共找了6次才找到,6就是11的成功查找次数, 那么平均成功查找次数 = 成功查找总次数(每个关键字的平均查找次数加起来) / 关键字个数。
  2. 平均失败查找次数:
    从下标0开始找一直没找到就需要找10次到全数组找完还没找到,10就是下标0的失败查找次数,从下标1开始找需要查找9次。。。。。平均失败查找次数 = 所有下标失败查找次数和 / 数组长度。

开散列/哈希桶

开散列又叫链地址发,首先对关键码集合用哈希函数计算地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个同种的元素通过一个单链表连接起来,各个链表的头结点存储在哈希表中:
在这里插入图片描述
从上图中可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。
开散列,可以认为是把一个在大集合中的搜索问题转化为在小集合中搜索了。

冲突严重时的解决办法

刚才我们提到了,哈希桶其实可以看做将大集合的搜索问题转化为小集合的搜索问题了,那如果冲突严重,就意味着小集合的搜索性能也不佳时,这个时候我们就可以将小集合搜索问题继续转化:

  1. 每个桶的背后是另外一张哈希表、
  2. 每个桶的背后是一颗搜索树。

虽然说遇到哈希冲突我们可以用各种办法去尽量解决冲突,但是在冲突发生前我们能不能尽量避免冲突的发生呢?
答案是可以的:

避免冲突

首先,我们需要明确一点,由于我们哈希表底层数组(顺序结构)的容量往往是小于实际要存储的关键字的范围的,这就意味着冲突的发生是必然的,但我们能做的应该是尽量的降低冲突。有相面两个大的思路去降低冲突的发发生:

哈希函数设计

引起哈希冲突的一个原因可能是:哈希函数设计不够合理。
哈希函数设计原理:

  • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m - 1之间。
  • 哈希函数计算出来的地址能均匀分布在整个空间中。
  • 哈希函数应该比较简单

常见的哈希函数

  1. 直接定制法
    取关键字的某个线性函数为散列地址: Hash( Key ) = A * Key + B
    优点:简单、均匀
    缺点:需要事先直到关键码的分布情况
    使用场景:适合查找比较小且连续的情况
  2. 除留余数法
    设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = Key % p (p <= m),将关键码转换成哈希地址。

哈希函数很多很多,这里就不做过渡到介绍了。

负载因子调节

哈希表的负载因子 = 填入表中的元素个数 / 散列表长度

哈希表的负载因子是哈希表装满程度的标志因子。由于表长是定值,负载因子和“填入表中的元素个数”成正比,所以负载因子越大,表明填入表中的元素越多,产生冲突的可能性就越大;反之,负载因子越小,表明填入表中的元素越少,长生冲突的可能性就越小。

对于开放定址法,负载因子是特别重要的因素,应严格限制在0.7 - 0.8以下。Java中将负载因子限制在0.75.超过此值将resize哈希表,即对底层顺序结构扩容然后将旧表中的数据重新一一放入到新表中。

这是负载因子和冲突率的粗略关系:
在这里插入图片描述

所以当冲突率达到一个无法忍受的程度时,我们需要通过降低负载因子来变相的降低冲突率。


点击全文阅读


本文链接:http://zhangshiyu.com/post/29888.html

冲突  函数  元素  
<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1