16.散列(哈希):
16.1:定义
16.2:构造散列函数的几种方法
16.3:哈希冲突的解决方法
16.3.1开放定址法:(缺点:容易产生堆积问题,删除元素时要特殊处理,节点规模大时浪费空间)
线性探测再散列
平方探测再散列
随机探测再散列(双探测再散列):
16.3.2 链地址法(拉链法)
16.3.3 再散列法
16.3.4 公共区域溢出法
16.4:一致性哈希
16.4.1 应用场景:
16.4.2 方法:
16.5:多阶哈希
16.5.1 概念:
16.5.2 特点:
16.5.3 使用场景:
16.6 散列的实际应用场景
16.7 总结
16.散列(哈希):
16.1:定义
散列函数:关键字与存储位置的一个确定的关系,通过查询key找到对应的value。
散列表:通过散列函数将记录存储在一块连续的存储空间中。
O(1)时间查找数据,通过key直接计算出元素在集合中的位置
16.2:构造散列函数的几种方法
直接定址法:hash(k) = ak + b
数字分析法:比如生日,身份证号,前面几位都相同,我们可以直接找规律取后几位进行计算。
折叠法:如果数字的位数很多,可以将数字分割为几个部分,取他们的叠加和作为hash地址
除留余数法: hash(k)= k % m
平方取中法:集合中每项数据都很相近,通过平方拉开差距再计算
斐波那契散列法:本质上还是一个乘法散列法,特殊的是它采用一个特殊的乘数,选择的这个乘数和黄金分割比例密切相关;
16.3:哈希冲突的解决方法
哈希冲突:不同的关键字经过散列函数的计算得到了相同的散列地址。
16.3.1开放定址法:(缺点:容易产生堆积问题,删除元素时要特殊处理,节点规模大时浪费空间)
假设一个散列函数的公式 hash(k) = (ak + d) % m
d 有三种取值:
线性探测再散列:d = c * i
平方探测再散列:d=1*1,-1*1,2*2,-2*2
随机探测再散列(双探测再散列):d是一组伪随机数列
例如将 19,01,23,14,55,68,11,86,37 存储在表长11的数组中,其中hash(k) = key % 11。
如下图线性探测法所示: 23%11 与 1%11 冲突,因此将23放在(23+1)%11的位置上。
68%11 与 23的位置冲突,(68+1)%11 与 14%11 冲突,因此将68放在 (68+2) %11的位置上
如下图平方探测法所示:23%11 与 1%11 冲突,因此将23放在 (23+1*1)%11的位置上
还有最后一种随机探测法,如下图所示:
开放定址法的缺点:容易产生堆积问题(随着冲突的增多,数据不停的向后放置,形成一条链表),删除元素时要特殊处理(一个特殊的标记位,记录数据是被删除的而不是空白的。下次查找的时候继续向后走),节点规模大时浪费空间等等
16.3.2 链地址法(拉链法)
在哈希表每一个单元中设置链表,某个数据项对的关键字还是像通常一样映射到哈希表的单元中,而数据项本身插入到单元的链表中。发生冲突时,在链表后面追加数据
16.3.3 再散列法
多准备几个哈希函数,一个冲突了,再用另外一个
16.3.4 公共区域溢出法
将冲突的放在一个特殊的公共区域
16.4:一致性哈希
16.4.1 应用场景:
一种特殊的哈希算法,在移除或者添加一个服务器时,能够尽可能小地改变已存在的服务请求与处理请求服务器之间的映射关系。
16.4.2 方法:
举例:我们用HASH(图片名称)%N 将3W张图片均匀缓存在3台缓存服务器上,如果缓存服务器挂掉/新增1台呢?那么势必会造成缓存雪崩,于是一致性哈希算法应运而生
基本概念:一致性哈希算法也是使用取模的方法,只不过将服务器数量换成了对2^32取模。
我们将2^32想象成一个圆,圆环的正上方的点代表0,0点右侧的第一个点代表1,以此类推,2、3、4、5、6……直到2^32-1,也就是说0点左侧的第一个点代表2^32-1 。我们把这个由2的32次方个点组成的圆环称为Hash环。
确定A,B,C三台服务器和图片的位置,图片将会被缓存到它顺时针碰到的第一个服务器上,也就是A。这就是一致性哈希算法
优点:当服务器数量发生变化时,只有部分缓存失效,不会产生雪崩情况
哈希环的倾斜:在实际的映射中,服务器可能会被映射成下图左模样,被缓存的对象大部分集中缓存在A上。假如此时服务器A出现故障,那么失效缓存的数量也将达到最大值,在极端情况下,仍然有可能引起系统的崩溃。这种情况则被称之为hash环的偏斜
虚拟节点:那么我们应该怎样防止hash环的偏斜呢?一致性hash算法中使用"虚拟节点"解决了这个问题。就是凭空的让服务器节点多起来,既然没有多余的真正的物理服务器节点,我们就只能将现有的物理节点通过虚拟的方法复制出来,这些由实际节点虚拟复制而来的节点被称为"虚拟节点"。加入虚拟节点以后的hash环如下:
"虚拟节点"是"实际节点"(实际的物理服务器)在hash环上的复制品,一个实际节点可以对应多个虚拟节点。从上图可以看出,A、B、C三台服务器分别虚拟出了一个虚拟节点。当然,如果你需要,也可以虚拟出更多的虚拟节点。引入虚拟节点的概念后,缓存的分布就均衡多了。
16.5:多阶哈希
16.5.1 概念:
多阶Hash通常是一个二维数组。内存构造如下图所示,逻辑上是一个阶梯状,但实际申请内存,是一块连续的内存结构,用每次层的阶数来标识出不同阶的分界。
举例:存在一个二维数组,数组内每个元素都能存储10000个元素。现在我们要插入key = 99995554 的元素
那么 99995554 在arr[0]的存储位置是99995554 / 10000 = 5554;
如果arr[0]的5554位置已经被占用了,则使用arr[1]的5554位置;
如果arr[1]的5554位置也被占用,则使用使用arr[2]的5554位置。
考虑到离散数学中同余原理,每行最大元素数目不同且数字取质数为宜,每行的数字可依次递减。譬如第一行取比10000稍小的质数9973,第二行大小取值9969,第三行取值9949。
所以若再次计算元素存储位置,则计算过程就稍微改变。计算过程如下:
在arr[0]的存储位置是99995554 % 9973;
如果arr[0]的存储位置99995554 % 9969 被占用,则使用arr[1]的99995554 % 9969位置;
如果同样也被占用,则使用使用arr[2]的99995554 % 9949位置。
16.5.2 特点:
1.二维数组的方式,每一行的个数都是素数。且数量逐行减少。
2.每个桶只存一个元素,用一维数组来具体实现二维的感觉。
3.冲突处理简单,逐行往下处理。
4.实现简单,扩展性强,当无法插入数据时,只需要继续增加阶数即可。
5.查询快速,空间利用率高。
6. 支持无锁安全高并发。(两个线程在竞争同一个位置时,谁先通过原子操作往里修改标志位并返成功即占有位置,返回失败的往下一层走,不需要锁)
7.当数据插入不了的时候,只能通过增加桶数,同时增加一阶(前面的不变)。
16.5.3 使用场景:
多级哈希用于需要快速查找、数据多的情况。 比如服务器数据缓存。
16.6 散列的实际应用场景
1 文件校验:CRC校验,奇偶校验
2 数字签名:非对称算法比较慢,所以数字签名中常使用单向散列函数
3 鉴权协议
4 md5,SHA1
6 负载均衡
7 分布式缓存
16.7 总结
哈希表基于数组,类似于k-v的存储形式。关键字通过哈希函数映射成数组下标。
哈希表是一个在空间和时间上做出权衡的经典例子,如果没有内存限制,那么可以直接将键作为数组索引。那么所查找的时间复杂度为O(1) 。如果没有时间限制,那么我们可以使用无序数组并进行顺序查找,这样只需要很少的内存。哈希表使用了适度的时间和空间在这两个极端中找到了平衡。通过调整哈希函数算法即可在时间和空间上做出取舍