Hash寻址详解
Posted 叶长风
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Hash寻址详解相关的知识,希望对你有一定的参考价值。
Hash寻址详解
最近在使用ThreadLocal保存上下文信息,原本准备写ThreadLocal原理以及源码详解,然后在偶尔中看到关于ThreadLocal中的hash寻址方式,与HashMap中寻址方式不同,于是决定先写一篇讲述Hash寻址方式的文章,再回头讲述ThreadLocal源码。
Hash
还是得从Hash的定义开始说起,hash的基本思想就是从一条记录中取出一条一个字段称之为key,通过一些固定的过程将key转换为数值,该数值称为散列键,散列键表示存储或者查找项中的位置,其中该数值将在0到n-1的范围,其中n是表中最大的槽数。
将key转换成散列键的固定过程称之为散列函数,如果需要访问hash表,则需要使用该函数。
我们可以用以下散列函数来确定散列键,公式为:
上图中,如果一般key为数值类型,则整除槽数作为散列值一般是一个比较合理的选择,但是并不是所有的key都是数值类型,也有字符串类型之类的,当然这种也是也办法的,根据key获取ASCII值,或者使用key做一个变换运算等等,都可以解决这个问题。
但在上图中仍然有个显而易见的问题就是并不是每个元素都是能算到一个独一无二的散列值,有可能多个key的散列值均是一样,这时候该如何解决这个问题,这种情况就叫做碰撞。
在发生这种碰撞的情况下,一般来说就是两个方法。
- 链表
- 线性探针
一般而言均是采用上述两种简单办法,当然还有稍微复杂一点,就是二次探针与双重哈希,这个在上述方案讲完后再来讲述。
链表处理
当哈希发生冲突时,比较简单的办法就是将具有相同hash值的元素放在同一个链表中,同时hash表槽中将不再存储对应hash值元素,而是存储链表地址,如下图:
在计算出hash值后,如果hash表槽中对应槽已经是链表地址,那么则遍历链表寻找与key相同的元素,如果key相同则返回,否则返回null,如果是插入操作,则进行插入即可,一般是将当前元素放在链表的表头,这样操作时间复杂度为o(1),不需要再遍历到队尾才进行插入,HashMap在进行插入操作时就是用的该方法。
线性探测处理
上述是使用数组加链表的形式来处理hash冲突,也可以不使用链表的方法来处理hash冲突,全部元素都在同一个数组里面的处理方法,这就是线性探测,当数组中数据并没有满的时候,此时发生hash碰撞,则寻找当前碰撞位置的下一个可用位置,如果下一个可用位置处没有数据则占用该位置处用于存放元素,如果不可用则继续向下寻找下一个可用位置,如果到了数组的末端,仍然没有空闲节点,则将指针重置到数组队首重新寻找空闲节点,如果到到碰撞点仍未找到空闲节点,则数组已满,需要进行扩容处理,线性探测如下:
当然,如果在发生hash冲突,解决冲突后,会形成一个连续的数据块,在这块区域内多个元素的hash值均相同,那么在此时如果有新插入的元素计算出的hash值也是与数据块中的相同,那么就有可能要进行多次寻找空闲节点来解决冲突。
二次线性探测
使用线性探针处理hash碰撞会带来一个问题,就是多个相同hash值的元素会聚集到一起,这样就会导致后续如果有相邻元素被计算出来时,需要寻找空闲节点的时间就比之前大大增长,那么此时就需要考虑将聚集在一起的元素进行打散,这就是二次线性探测的由来。
使用二次探测时,并不是每次都是移动一个点,而是从碰撞点向下寻找,直至不碰撞为止,此时不碰撞时移动步数记作为i,如果使用二次线性探测的方式避免hash碰撞,则移动步数为i^2,如下图:
二次线性探测有个限制就是,如果当表中有一半的位置已经被填满以后,就很难找到一个新的空位用来填充,需要对数组进行扩容。
双重hash
解决冲突的还有一个办法就是在当hash后的结果发生碰撞时,对第一次的hash结果进行第二次hash,第二次hash后的结果即为该元素将要插入的点。
一般来说,第二次hash方法许多都是采用使用一值减去第一次哈希结果,即为第二次点位置,即为:Hash2(key) = R - ( key % R ) ,其中R一般为小于hash表大小的一个素数,具体如下图:
以上是关于Hash寻址详解的主要内容,如果未能解决你的问题,请参考以下文章
7.redis 集群模式的工作原理能说一下么?在集群模式下,redis 的 key 是如何寻址的?分布式寻址都有哪些算法?了解一致性 hash 算法吗?