刷题(6)-哈希

本文深入探讨哈希函数与哈希表的工作原理,包括拉链法与开放地址法解决冲突的策略;设计O(1)时间复杂度的RandomPool结构;解析布隆过滤器的实现与应用;以及一致性哈希在负载均衡中的作用。

1、哈希函数和哈希表

 

步骤:

第一步 用一个哈希函数(相同输入 相同输出  不同输入  输出均匀分布)把key转为数组下标

第二步 处理碰撞冲突 
(1) 拉链法  (数组的元素是个链表)
(2) 开放地址法 用数组的空位来处理冲突 (最简单的 线性探测法)   

1)拉链法:

拉链法使用链表来存储 hash 值相同的键,从而解决冲突。查找需要分两步,首先查找 Key 所在的链表,然后在链表中顺序查找。 (数组大小小于键的个数)

2)开发地址法:

开放寻址法的核心思想是,如果出现了散列冲突,我们就重新探测一个空闲位置,将其插入。那如何重新探测新的位置呢?我先讲一个比较简单的探测方法,线性探测(Linear Probing)。当我们往散列表中插入数据时,如果某个数据经过散列函数散列之后,存储位置已经被占用了,我们就从当前位置开始,依次往后查找,看是否有空闲位置,直到找到为止。(所以要求数组大小大于键的个数)

开放地址法优点:

        1)散列表中的数据都存储在数组中,可以有效地利用 CPU 缓存加快查询速度。

        2)序列化起来比较简单。链表法包含指针,序列化起来就没那么容易。

  开放地址法缺点:

        1) 删除数据的时候比较麻烦,需要特殊标记已经删除掉的数据。

        2)冲突的代价更高。装载因子比较大的时候,就可能会有大量的散列冲突,导致大量的探测、再散列等,性能会下降很多。所以,使用开放寻址法解决冲突的散列表,装载因子的上限不能太大。这也导致这种方法比链表法更浪费内存空间。

拉链法优点:

       1)链表法对内存的利用率比开放寻址法要高。因为链表结点可以在需要的时候再创建,并不需要像开放寻址法那样事先申请好。

        2)链表法比起开放寻址法,对大装载因子的容忍度更高。开放寻址法只能适用装载因子小于 1 的情况。接近 1 时,就可能会有大量的散列冲突,导致大量的探测、再散列等,性能会下降很多。但是对于链表法来说,只要散列函数的值随机均匀,即便装载因子变成 10,也就是链表的长度变长了而已,虽然查找效率有所下降,但是比起顺序查找还是快很多。

拉链法缺点:

        1)链表因为要存储指针,所以对于比较小的对象的存储,是比较消耗内存的,还有可能会让内存的消耗翻倍。

         2)因为链表中的结点是零散分布在内存中的,不是连续的,所以对 CPU 缓存是不友好的,这方面对于执行效率也有一定的影响。当然,如果我们存储的是大对象,也就是说要存储的对象的大小远远大于一个指针的大小(4 个字节或者 8 个字节),那链表中指针的内存消耗在大对象面前就可以忽略了。

        3)实际上,我们对链表法稍加改造,可以实现一个更加高效的散列表。那就是,我们将链表法中的链表改造为其他高效的动态数据结构,比如跳表、红黑树。这样,即便出现散列冲突,极端情况下,所有的数据都散列到同一个桶内,那最终退化成的散列表的查找时间也只不过是 O(logn)   

总结:

        当数据量比较小、装载因子小的时候,适合采用开放寻址法。

        基于链表的散列冲突处理方法比较适合存储大对象、大数据量的散列表,而且,比起开放寻址法,它更加灵活,支持更多的优化策略,比如用红黑树代替链表。


2、设计RandomPool结构

题目要求:

 设计一种结构,在该结构中有如下三个功能:
         insert(key):将某个key加入到该结构,做到不重复加入。
         delete(key):将原本在结构中的某个key移除。

         getRandom():等概率随机返回结构中的任何一个key。
【要求】 Insert、delete和getRandom方法的时间复杂度都是   O(1)

做法:

两个哈希map (unordered_map)还有一个size 用来保存现在有多少个元素
假如要插入的那个key是string

insert和getRandom:
map1 key是 string value是int (插入的顺序)
map2 key是int, value 是 string
插入的时候  两个map都插
getrandom的时候, random一个数字 [0-size-1]    然后返回map2[random]的值

delete  str:
先得到map1[str]的值 i 就是str在map2的key 然后得到 map2[size-1] 的值 就是最后插入的 last string 
然后把map2[i] 改成 last string  然后 把map1[last string] 改成i  
最后删掉map1[str] (map1.erase(str)) 和 map2[size-1] 再  --size  
因为直接  删掉后存在洞,这些洞影响性能,无法保证o( 1 )的getrandom。 所以做法:拿最后一个值和洞进行交换,size – 1。
 

3、布隆过滤器

比特数组  用 int 数组 就ok了

有失误率,宁可错杀三千不可放过一个。
准备一个 bit数组 假设长度为m   然后对于一个url,分别用k个哈希函数进行计算 得到的hashcode % m (m就是布隆过滤器的大小),对这个位置置1,这样一个url会对k个位置进行置1。
现在有100亿个黑名单,对每个url都这么干
当需要检验一个url在不在黑名单上时,分别对k个哈希函数进行计算,得到的每个位置假设全部为1,那么就认为它在黑名单上,否则,认为不在。

布隆过滤器开多大由 样本量和预期失误率来计算
                                                              m = -n * lnP / (ln2)^2

(m是布隆过滤器的大小(开的bit数组),p是预期失误率 n是样本量(就是黑名单里的url))
m(布隆过滤器大小)越大,失误率越小,n(样本量)越大,失误率越大。

而k(哈希函数个数)有一个最佳大小
                                                        k = ln2 * m/n  = 0.7* m/n ; 哈希函数的个数。

真实失误率 : (1-e^(-n*k/m))^k
 

4、一致性哈希(负载均衡)
转自:http://www.zsythink.net/archives/1182

需求: 有多台后端服务器,希望进行负载均衡。举个例子:现在有3台服务器,那么读取图片的请求会均衡的负载在三台服务器上。
错误做法:一种做法是对请求进行哈希计算,计算结果对服务器数量去模,例如3台服务器,取模结果为0,1,2。这种做法存在致命的缺点,当服务器数量进行变化,模的底数变了,那么哈希计算出来的值就发送了改变。那么在实际中的体现是这样的:用户A的读取个人资料请求一直交给01服务器处理,现在请求被提交到了02服务器,那么就读取到错误的信息。
矫正数据:收集原来的请求哈希值对应的服务器,转存到新的哈希值计算的服务器上。代价是非常大的。

一致性哈希解决:对每台服务器的IP地址(唯一标识)进行哈希算法,得到一个值。 根据这个值可以在环上进行划分区域,这样每台服务器掌管一段区间。 当请求发过来时,把请求通过哈希函数进行计算,得到环上的一个位置, 然后通过二分算法找到离他最近的一个服务器的位置(超过容器大小,就从0开始),这个请求就交给了某台服务器。

矫正数据:添加或减少服务器时,矫正的代价只有一个结点的数据搬运到另一个结点的数据而已。

å¨è¿éæå¥å¾çæè¿°

不过服务器IP在环上的映射并不均衡。很容易映射成这种情况。

å¨è¿éæå¥å¾çæè¿°

那么解决方案就是:虚拟节点机制,对每台机器通过多个哈希函数计算出多个哈希值,然后在每个哈希值上面都放置一个虚拟节点。 请求和虚拟节点进行匹配。 虚拟节点和物理服务器之间是多对一的关系。虚拟节点再发请求发送到自己对应的服务器上即可。 这样根据哈希函数的性质(均匀分布),负载就会均衡

å¨è¿éæå¥å¾çæè¿°
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值