散列表与哈希算法学习笔记

散列表与哈希算法

一,散列表原理(Hash Table)

散列表来源于数组具有下标随机访问特性,理解这点非常重要。可以说散列表是由数组进化来的。将输入的键通过哈希函数映射得出的value作为index去table中查询,这便是散列的思想。

graph LR A[键值key] -->|哈希函数|B(结果value)

我们了解到为什么散列表的查询复杂度是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,设计散列函数

要求:

  1. 不能太复杂,不然消耗CPU计算
  2. 生成的value尽量随机且均匀分布。
  3. 合理利用关键字的特点,散列表的大小

例如,对于字符串,我们可以用26进制求出值,再模长度

2,装载因子过大如何处理

装载因子过大,说明表中元素过多,空闲位置少,散列冲突的概率会很大,插入数据会多次寻址(开放寻址法)或槽中的链表的很长(链表法),导致查询效率很低。

对于频繁插入的动态散列表,当装载因子超过某个阈值,需要进行扩容,重新申请内存空间,重新计算哈希值,并搬移数据,O(n)的复杂度。

3,一次性扩容并搬移数据,效率很低怎么办?

我们可以分批处理,先申请空间,将插入的数据直接插入新的散列表里,然后旧的散列表里的数据分批逐步的同步到新散列表,当查询时可以先查新散列表,再查旧的。。
解决问题:触发阈值扩容并搬移数据这个过程在数据量很大时效率是极低的,让人崩溃

三,哈希算法

将任意长度的二进制位映射到固定长度的二进制位的映射规则,称为哈希算法。
一个优秀的哈希算法包括以下要求:

  1. value不能反推出key
  2. 输入的数据即使相差一个bit,输出的value也会相差很大
  3. 冲突概率尽可能小。‘
  4. 算法的执行效率要高,不占用过多计算资源。

我们着重了解哈希算法的应用。

1,安全加密

最常用的安全加密算法:MD5,SHA,DES,AES

为什么可以用哈希算法做安全加密是由于key的向量空间是非常大的,利用穷举的方法找到两个哈希值一样的文本是几乎不可能的。

2,唯一标识

拿一张图片,去判断图库中是否有这张图片,如何做呢?
对这张图片做哈希运算,将得到的值进行去查,大大节省了时间。

3,数据校验

比如我们下载一个很大的电影,服务器会将这个大文件分拆成上百个小文件发送,那么如何确保数据没有被篡改或者丢失?

利用哈希的思路:将下载下来的文件做哈希运算,得到的值和种子文件做对比。

4,散列函数

散列表是哈希函数的一种应用,区别是散列函数追求简单,快速,对于加密并不重视。

四,哈希算法与分布式

1,负载均衡

如何实现一个会话粘滞(session sticky)的负载均衡算法?非常简单,**通过哈希算法,对客户端IP地址或会话ID计算哈希值,取模运算映射到相应的服务器。

2,数据分片

我们来看两个非常常见的面试题:

Ⅰ 大数据统计“搜索关键字”出现的次数

Description:我们有1T的内存,我们想快速统计每个关键字被搜索的次数,怎么做呢?我们有以下难点:

  1. 一台机器的内存,无法容纳
  2. 只用一台机器,处理时间会很长
    解决方法:
    ** 先对数据分片,采用多台机器,提高速度。**
    具体思路:
    我们用n台机器并行处理,我们从搜索记录的日志文件中,依次读取每个搜索关键字,进行哈希运算,跟n取模,得到值就是分配到的机器编号。
    由此一来,哈希值相同的搜索关键字就被分配到了同一台机器。
    最后再将n台机器的结果合并在一起。

这正是MapReduce的思想。

Ⅱ 快速判断图片是否在图库中(图库特别大)

如果我们对图片构建散列表,单台机器内存有限。

同样,我们可以进行数据分片,采用多机处理。每台机器都有对应的散列表,我们去判断的时候,先哈希运算,取值模n得到机器号,再由相应的机器进行处理。当然,相应的机器可以构建散列表,由于数据分片了,内存是合适的。

3,分布式存储

面对海量数据,为了提高读写能力,一般用分布式方式存储数据。

跟前面的思路类似,数据分片,哈希运算获得机器号,然后去相应的机器做查询。

问题来了,假如缓存机器不够了,需要做扩容怎么办?麻烦来了,简单的增加机器并不可取。比如本来10台机器,那么15被映射到5号机器,我们增加两台,那么15会被映射到3号机器。也就是说此时缓存失效了(需要搬移数据到正确的机器上),会直接向数据库索要数据,会压垮数据库。
一致性哈希就是解决这个问题的,可以避免大量的数据搬移。

关于一致性哈希,有大牛讲的很好,贴出链接:
http://www.zsythink.net/archives/1182

posted @ 2020-03-19 16:02  傻蜗牛  阅读(398)  评论(0编辑  收藏  举报