算法通过村——Hash基础问题解析

Hash表

1、Hash概念

哈希表(hash table)也叫做散列表,这种数据结构提供了键(Key)值(Value) 的映射关系。只要给出一个Key,就可以高效查找到它所匹配的Value,时间复杂度接近于O(1)。

在这里插入图片描述

2、Hash函数

​ 基于Hash表的特性,采取了查询效率最高的一种数据结构来实现——数组。哈希表本质上也是一个数组。通过哈希函数,把Key和数组下标进行转换。

在这里插入图片描述

​ 在不同的语言中,哈希函数的实现方式是不一样的。这里以Java的常用集合HashMap为例,来看一看哈希函数在Java中的实现。

​ 在Java及大多数面向对象的语言中,每一个对象都有属于自己的hashcode,这个hashcode是区分不同对象的重要标识。无论对象自身的类型是什么,它们的hashcode都是一个整型变量。

​ 既然都是整型变量,想要转化成数组下标就不难实现了。最简单的转化方式是按照数组的长度进行取模运算。

index = HashCode (Key) % Array.length

​ 实际上,JDK中的哈希函数并没有直接采用取模运算,而是利用了位运算的方法来优化性能。不过在这里可以姑且简单理解成取模操作。

​ 通过哈希函数,我们可以把字符串或其他类型的Key,转化为数组的下标index。

如给出一个长度为8的数组,则当

​ Key = 001121时,

index = HashCode(“001121”) % Array.length = 1420036703 % 8 = 7

​ Key = this时,

index = HashCode(“this”) % Array.length = 3559070 % 8 = 6

3、哈希表的读写操作

3.1、写操作(put)

​ 写操作就是在哈希表中插入新的键值对(在JDK中叫做Entry)。

​ 如调用hashMap.put(“002931”, “王五”),意思是插入一组Key为002931、Value为王五的键值对。

​ 具体的做法如下:

  1. 通过哈希函数,把Key转化为数组下标5.

  2. 如果数组下标5对应的位置没有元素,就把这个Entry填充到数组下标5的位置。

    在这里插入图片描述

​ 但是,由于数组的长度是有限的,当插入的Entry越来越多时,不同的Key通过哈希函数获得的下标有可能是相同的。例如002936这个Key和002947这个Key对应的数组下标都是2。这时,就产生了哈希冲突,也叫碰撞哈希冲突是无法避免的

​ 常见的解决方法由:开放寻址法(Java中的Threadlocal)链地址法(Java中的ConcurrentHashMap)、再哈希法(布隆过滤器)、建立公共溢出区。后两种用的比较少,重点看前两个。

3.1.1、开放寻址法

​ 开放寻址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。

在这里插入图片描述

​ 例如上面要继续存7、8、9时,7可以直接存到索引0的位置。8本来应该存到索引1的位置,但是已经满了,所以继续向后找,索引3的位置是空的,所以8存到3位置。同理索引9存到6位置。

3.1.2、链地址法

​ HashMap数组的每个元素不仅是一个Entry对象,还是一个链表的头节点。每一个Entry对象通过next指针指向它的下一个Entry节点。当新来的Entry映射到与之冲突的数组位置时,只需要插入到对应的链表中即可。

在这里插入图片描述

3.2、读操作(get)

​ 与写操作对应,读操作就是通过给定的Key,在散列表中查找对应的Value。

​ 例如调用hashMap.get(“002936”),意思是查找Key为002936的Entry在散列表中所对应的值。

​ 以链地址法为例,步骤如下:

  1. 通过哈希函数,把Key转化为数组下标2

  2. 找到数组下标2对应的元素,如果这个元素的Key是002936,那么就找到了对应的Value;如果这个Key不是002936,由于数组的每一个元素都与一个链表对应,可以顺着链表慢慢往下找,看看能否找到与Key相匹配的节点。

    在这里插入图片描述

​ 在上图中,首先查到的节点Entry6的Key是002947,和带查找的Key002936不符。接着定位到链表的下一个节点Entry1,发现Entry1的Key002936正是我们要寻找的,所以返回Entry1的Value即可。

3.3、扩容(resize)

​ 当经过多次元素插入,散列表达到一定饱和度时,Key映射位置发生冲突的概率会逐渐提高。这样一来,大量的元素拥挤在相同的数组下标位置,形成很长的链表,对后续插入操作和查询操作的性能都有很大影响。

在这里插入图片描述

​ 这时,散列表就需要扩展它的长度,也就是进行扩容

​ 对于JDK中的散列表实现类HashMap来说,影响其扩容的因素有两个。

  • Capacity,即HashMap当前的长度

  • LoadFactor,即HashMap的负载因子,默认值为0.75f

    当==HashMap.Size >= Capacity * LoadFactor==时,HashMap需要扩容。

    扩容步骤如下:

    1. 扩容,创建一个新的Entry空数组,长度是原数组的两倍
    2. 重新Hash,遍历原Entry数组,把所有的Entry重新Hash到新数组中。为什么要重新Hash?因为长度扩大后,Hash的规则也随之改变。

​ 经过扩容,原本拥挤的散列表重新变得稀疏,原有的Entry也重新得到尽可能均匀的分配。

​ 扩容前的HashMap如下

在这里插入图片描述

​ 扩容后的HashMap如下

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Molche

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值