[数据结构与算法-理论] Hash表

1. 概念

1) Hash表

Hash表也叫哈希表、散列表,(我的理解)它是一种特殊的数组。数组是支持随机访问的一种数据结构,我们可以根据下表直接访问内容,时间复杂度是O(1)。但是在进行查找操作的时候,我们不知道目标元素在什么位置,下标是多少,所以只能借助二分查找、顺序查找等方法,时间效率大打折扣。

2) Hash函数

通过Hash函数将元素的内容转化成数组的下标,我们就可以通过下标进行随机访问了。这里的部分内容可能是用户的id、学生的姓名等等,我们称作Key。通过Hash函数计算得到的值,我们称为Hash值。其实Hash值不一定就是数组的下标,因为其大小可能大于数组大小,我们通常再将Hash值取模,得到数组下标。

2. Hash冲突

1) 必然发生Hash冲突

明白了Hash表和Hash函数的基本概念,那么如何设计Hash函数,得到Hash值呢?

  1. 我们要保证Hash值是一个非负整数。很好理解,这是由于数组下标的特性决定的。

  2. 当两个key相同的时候,我们要保证他们的Hash值是相同的。否则两次相同的查找,查找的结果不同,就乱套了。

  3. 我们要尽量保证当两个key不同的时候,他们的Hash值也不同。

前两点很好理解,最后一点看起来很必要(否则很多元素都会对应数组中同一个下标,就会出现Hash冲突的现象),但是实现起来几乎不可能。

大学离散数学中有一个原理叫鸽巢原理。就是说,如果有 10 个鸽巢,有 11 只鸽子,那肯定有 1 个鸽巢中的鸽子数量多于 1 个,换句话说就是,肯定有 2 只鸽子在 1 个鸽巢内。

所以,我们一方面要尽可能的设计一个完美的Hash函数来尽量避免Hash冲突,另一方面,我们要在Hash冲突时选择合适的方法来解决。

2) Hash函数的要求

一个好的Hash函数首先不能太过于复杂,否则消耗太多的计算时间会间接影响Hash表的性能。其次Hash函数要尽可能不出现Hash冲突的现象,即使出现Hash冲突,也要尽量平均分配到数组每一个单元空间。

3) 解决Hash冲突

(1) 开放寻址法

开放寻址法核心思想是,出现冲突后,寻找数组中下一个空位置存放。寻找下一个空位置的方法有多种。

1. 线性探测

线性探测很直白,出现冲突就取 **(下标 + 1)% 数组长度 ** 作为下一个空位置,如果还冲突就继续+1。

在删除操作的时候有一点需要注意。在查找操作时如果发现当前下标对应元素不是目标元素,则会寻找 **(下标 + 1)% 数组长度 ** 下标,如果还不是目标元素,则再寻找+1个位置,直到+1位置为空,则判定没有目标元素。如果删除的时候我们真的将元素删除了,在插入时由于冲突保存在被删除元素后面的元素就找不到了。我们采取的措施就是使用逻辑删除,而不是物理删除,直白的说就是将元素标记为删除,保证线性探测的连续性。

2. 二次探测

二次探测在出现冲突时,取 **(下标 + 1^2)% 数组长度 ** 作为下一个空位置,如果还冲突就取 **(下标 + 2^2)% 数组长度 ** 作为下一个空位置,以此类推。

3. 双重散列(哈希)

双重散列则是准备一组散列函数 hash1(key)、hash2(key)、hash3(key)等等,先用第一个散列函数,如果计算得到的存储位置已经被占用,再用第二个散列函数,依次类推,直到找到空闲的存储位置。

不管使用哪种开放寻址法,散列表中存储的元素个数最多就是数组的大小,如果数组存储空间满了,就只能通过扩容来解决。换个思路,如果数组几近满了,这时操作Hash表的效率就会大大降低,为了保证Hash表的操作效率,我们要在合适的时候进行扩容。这里我们引入一个概念,装载因子,用来表示Hash表数组的使用情况。

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

装载因子越大,说明空闲位置越少,冲突越多,散列表的性能就会下降。

(2) 链表法

链表法的核心思想是,使用Hash表的数组保存指针,将每个数组单元看作一个桶或者槽,发生冲突的元素全部丢到桶中。对于桶中的元素,可以使用链表来组织,也可以使用树来组织。

3. Hash表扩容

随着Hash表中元素越来越多,也就是装载因子越来越大,哈希冲突发生的概率也越来越大。当装载因子超过某个阈值时,就需要进行扩容。装载因子阈值如果过大,会影响Hash表效率,如果过小,会浪费内存空间。

扩容的大小通常为原大小的二倍,这样可以使用位运算提升效率if ((e.hash & oldCap) == 0),当if成立说明当前节点应该放在原桶中,否则,说明当前节点应该放在 原桶下标+oldCap 对应的桶中。在Java的HashMap类的resize方法中有实现。传送门:[集合类] 源码解析10(HashMap类)

如果在某次添加数据触发了扩容,而数组扩容和数据搬移的时间都分配给这次添加操作,显然会影响效率。通过均摊的思想,redis就是将数据搬移工作均摊到每个添加、删除、查找、更新操作上,从而避免了集中数据搬移带来的庞大工作对某一次操作的影响。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值