散列表与哈希算法学习笔记
散列表与哈希算法
一,散列表原理(Hash Table)
散列表来源于数组具有下标随机访问特性,理解这点非常重要。可以说散列表是由数组进化来的。将输入的键通过哈希函数映射得出的value作为index去table中查询,这便是散列的思想。
我们了解到为什么散列表的查询复杂度是O(1),因为key->value为计算过程,O(1),数组支持随机访问,查询也为O(1),所以散列表的查询效率为O(1)。
我们可以很明确的看出,散列函数(即hash function)是至关重要的。散列函数具有的特点有:
- hash(key)为非负的
- 当key1 == key2 ,hash(key1) == hash(key2)
- 当key1 != key2 ,hash(key1) != hash(key2)
第三点要求很难实现,这是由于散列冲突是几乎不可避免的,我们来聊聊散列冲突。
1,散列冲突
像知名的hash算法:MD5,SHA也无法完全避免散列冲突,常见的解决方法为两类:开放寻址和链表法。
Ⅰ ,开放寻址
开放寻址的思想是:如果出现了散列冲突,我们就重新探测一个位置,将其插入。如何探测一个新的位置呢?我们有几种方法:
①,线性探测
如果出现冲突,则从当前位置往后挪,直到找到空闲位置。
优点 | 缺点 |
---|---|
简单 | 若元素过多,冲突概率会很大,查询/插入/插入的效率急速降低 |
删除元素不能直接删除,要标记一下deleted |
②,二次探测
它与第一种方法的区别是,线性探测挪动的步伐为1,而二次探测挪动的步伐为0,1,4,9,。。。。
③,双重散列
我们不只使用一个散列函数,我们使用一组散列函数,hash1,hash2,hash3,....
以上三种方法,不管用哪种,当装载因子过大时,冲突的概率都会大大提高。
我们用装载因子表示空位的剩余。计算公式为:
装载因子 = 填入的元素个数/table的长度
Ⅱ,链表法
链表法相对开放寻址法,更加常用,更加简明。在链表法中,每个元素存储的时一条链表,所有散列值相同的元素,我们都放到对应的链表中。
开放寻址与链表法的优劣
①,开放寻址优劣
优点
开放寻址的数据都在散列表中,有效利用CPU缓存,加快查询,并且序列化简单
缺点
删除数据比较麻烦,可能需要标记deleted,相比链表法,冲突概率高,不能将装载因子设计的太大,所以相比链表法,更耗内存。4
②,链表法优劣
优点
内存利用率相对高,对大的装载因子容忍,
缺点
CPU缓存不友好(由于有链表)。
实际上我们可以在每个槽里不存指向链表的指针,而是存红黑树,跳表这种高效查询的数据结构,有效避免散列碰撞攻击(即使数据都挤在一个槽,查询效率也为O(logn))。
二,设计散列表
工业级的散列表,需要应对各种异常情况,避免散列冲突下性能急剧下降,抵抗散列碰撞攻击。
PS:散列碰撞攻击指的是恶意的用户构造恶意的数据,使得所有数据经过散列函数之后,都进入了一个bucket(slot,槽),再去查询,这样会大量消耗CPU资源,以此来做Dos攻击。
1,设计散列函数
要求:
- 不能太复杂,不然消耗CPU计算
- 生成的value尽量随机且均匀分布。
- 合理利用关键字的特点,散列表的大小
例如,对于字符串,我们可以用26进制求出值,再模长度
2,装载因子过大如何处理
装载因子过大,说明表中元素过多,空闲位置少,散列冲突的概率会很大,插入数据会多次寻址(开放寻址法)或槽中的链表的很长(链表法),导致查询效率很低。
对于频繁插入的动态散列表,当装载因子超过某个阈值,需要进行扩容,重新申请内存空间,重新计算哈希值,并搬移数据,O(n)的复杂度。
3,一次性扩容并搬移数据,效率很低怎么办?
我们可以分批处理,先申请空间,将插入的数据直接插入新的散列表里,然后旧的散列表里的数据分批逐步的同步到新散列表,当查询时可以先查新散列表,再查旧的。。
解决问题:触发阈值扩容并搬移数据这个过程在数据量很大时效率是极低的,让人崩溃
三,哈希算法
将任意长度的二进制位映射到固定长度的二进制位的映射规则,称为哈希算法。
一个优秀的哈希算法包括以下要求:
- value不能反推出key
- 输入的数据即使相差一个bit,输出的value也会相差很大
- 冲突概率尽可能小。‘
- 算法的执行效率要高,不占用过多计算资源。
我们着重了解哈希算法的应用。
1,安全加密
最常用的安全加密算法:MD5,SHA,DES,AES。
为什么可以用哈希算法做安全加密是由于key的向量空间是非常大的,利用穷举的方法找到两个哈希值一样的文本是几乎不可能的。
2,唯一标识
拿一张图片,去判断图库中是否有这张图片,如何做呢?
对这张图片做哈希运算,将得到的值进行去查,大大节省了时间。
3,数据校验
比如我们下载一个很大的电影,服务器会将这个大文件分拆成上百个小文件发送,那么如何确保数据没有被篡改或者丢失?
利用哈希的思路:将下载下来的文件做哈希运算,得到的值和种子文件做对比。
4,散列函数
散列表是哈希函数的一种应用,区别是散列函数追求简单,快速,对于加密并不重视。
四,哈希算法与分布式
1,负载均衡
如何实现一个会话粘滞(session sticky)的负载均衡算法?非常简单,**通过哈希算法,对客户端IP地址或会话ID计算哈希值,取模运算映射到相应的服务器。
2,数据分片
我们来看两个非常常见的面试题:
Ⅰ 大数据统计“搜索关键字”出现的次数
Description:我们有1T的内存,我们想快速统计每个关键字被搜索的次数,怎么做呢?我们有以下难点:
- 一台机器的内存,无法容纳
- 只用一台机器,处理时间会很长
解决方法:
** 先对数据分片,采用多台机器,提高速度。**
具体思路:
我们用n台机器并行处理,我们从搜索记录的日志文件中,依次读取每个搜索关键字,进行哈希运算,跟n取模,得到值就是分配到的机器编号。
由此一来,哈希值相同的搜索关键字就被分配到了同一台机器。
最后再将n台机器的结果合并在一起。
这正是MapReduce的思想。
Ⅱ 快速判断图片是否在图库中(图库特别大)
如果我们对图片构建散列表,单台机器内存有限。
同样,我们可以进行数据分片,采用多机处理。每台机器都有对应的散列表,我们去判断的时候,先哈希运算,取值模n得到机器号,再由相应的机器进行处理。当然,相应的机器可以构建散列表,由于数据分片了,内存是合适的。
3,分布式存储
面对海量数据,为了提高读写能力,一般用分布式方式存储数据。
跟前面的思路类似,数据分片,哈希运算获得机器号,然后去相应的机器做查询。
问题来了,假如缓存机器不够了,需要做扩容怎么办?麻烦来了,简单的增加机器并不可取。比如本来10台机器,那么15被映射到5号机器,我们增加两台,那么15会被映射到3号机器。也就是说此时缓存失效了(需要搬移数据到正确的机器上),会直接向数据库索要数据,会压垮数据库。
一致性哈希就是解决这个问题的,可以避免大量的数据搬移。
关于一致性哈希,有大牛讲的很好,贴出链接:
http://www.zsythink.net/archives/1182