哈希
哈希 其实是随机存储的一种优化,先进行分类,然后查找时按照这个对象的分类去找。
哈希通过一次计算大幅度缩小查找范围,自然比从全部数据里查找速度要快。
哈希函数
哈希函数是一种映射关系,根据数据的关键词 key ,通过一定的函数关系,计算出该元素存储位置的函数。
常见的哈希函数:
为什么大部分 hashcode 方法使用 31
《Effective Java》第 42 页就有对 hashCode 为什么采用 31 做了说明
之所以使用 31, 是因为他是一个奇素数。如果乘数是偶数,并且乘法溢出的话,信息就会丢失,因为与2相乘等价于移位运算(低位补0)。使用素数的好处并不很明显,但是习惯上使用素数来计算散列结果。 31 有个很好的性能,即用移位和减法来代替乘法,可以得到更好的性能: 31 * i == (i « 5) - i, 现代的 VM 可以自动完成这种优化。这个公式可以很简单的推导出来。
HashMap 的 hash 算法的实现原理(为什么右移 16 位,为什么要使用 ^ 位异或)
为了性能,进一步减少hash冲突
HashMap 为什么使用 & 与运算代替模运算?
tab[(n - 1) & hash]
其中 n 是数组的长度。其实该算法的结果和模运算的结果是相同的。但是,对于现代的处理器来说,除法和求余数(模运算)是最慢的动作。
a % b == (b-1) & a ,当b是2的指数时,等式成立。
HashMap 的容量为什么建议是 2的幂次方?
为了保证按位与操作时,位数全为1,最大限度的利用 hash 值,并更好的散列,只有全是1 ,才能有更多的散列结果。
我们自定义 HashMap 容量最好是多少?
如果我们预计我们的散列表中有2个数据,那么我就初始化容量为2嘛?
绝对不行,如果大家看过源码就会发现,如果Map中已有数据的容量达到了初始容量的 75%,那么散列表就会扩容,而扩容将会重新将所有的数据重新散列,性能损失严重,所以,我们可以必须要大于我们预计数据量的 1.34 倍,如果是2个数据的话,就需要初始化 2.68 个容量。当然这是开玩笑的,2.68 不可以,3 可不可以呢?肯定也是不可以的,我前面说了,如果不是2的幂次方,散列结果将会大大下降。导致出现大量链表。那么我可以将初始化容量设置为4。 当然了,如果你预计大概会插入 12 条数据的话,那么初始容量为16简直是完美,一点不浪费,而且也不会扩容。
哈希冲突的解决
1、拉链法
将所有关键字为同义词的结点链接在同一个单链表中。
若选定的散列表长度为 m,则可将散列表定义为一个由 m 个头指针组成的指针数组 T[0..m-1] 。
凡是散列地址为 i 的结点,均插入到以 T[i] 为头指针的单链表中。
T 中各分量的初值均应为空指针。
在拉链法中,装填因子 α 可以大于 1,但一般均取 α ≤ 1。
2、开放定址法
当冲突发生时,使用某种探测技术在散列表中形成一个探测序列。沿此序列逐个单元地查找,直到找到给定的关键字,或者碰到一个开放的地址(即该地址单元为空)为止(若要插入,在探查到开放的地址,则可将待插入的新结点存人该地址单元)。查找时探测到开放的地址则表明表中无待查的关键字,即查找失败。
简单的说:当冲突发生时,使用某种探查(亦称探测)技术在散列表中寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到。
按照形成探查序列的方法不同,可将开放定址法区分为线性探查法、二次探查法、双重散列法等。
冲突是不是可以避免的?
哈希表
一般情况下,在哈希表上的插入、查找、删除等操作的时间复杂度是 O(1)。
查找过程中,关键字的比较次数,取决于产生冲突的多少,产生的冲突少,查找效率就高,产生的冲突多,查找效率就低。因此,影响产生冲突多少的因素,也就是影响查找效率的因素。
影响产生冲突多少有以下三个因素:
- 哈希函数是否均匀;
- 处理冲突的方法;
- 哈希表的加载因子。
哈希表的加载因子和容量决定了在什么时候桶数(存储位置)不够,需要重新哈希。
加载因子太大的话桶太多,遍历时效率变低;太小的话频繁 rehash,导致性能降低。所以加载因子的大小需要结合时间和空间效率考虑。
在 HashMap 中的加载因子为 0.75,即四分之三。
一致性哈希算法在分布式缓存中的应用
一致性哈希将哈希值取值空间组织成一个虚拟的环,各个服务器与数据关键字K使用相同的哈希函数映射到这个环上,数据会存储在它顺时针“游走”遇到的第一个服务器。可以使每个服务器节点的负载相对均衡,很大程度上避免资源的浪费。
使用带虚拟节点的一致性哈希算法,可以有效地降低服务硬件环境变化带来的数据迁移代价和风险,从而使分布式缓存系统更加高效稳定。
ringbuffer
参考: