1 哈希性质
- 输入无穷,输出有穷
- 无随机机制,一个输入有且只有唯一的输出
- 不同的输入输出可能相同(哈希碰撞),保证尽量不同
- 散列尽量均匀
- 具有不可逆性 -> 单向加密
2 资源限制
举例:40 亿整数文件,整数 4 个字节,只有1GB内存,返回次数最多的数
一个数最多出现 40 亿次,如果每个都出现一次,也就最多 40 条记录
所以使用哈希表则最少需要 4 B 的 key 和 4 B 的 value,其他都不算的情况下,最少也得需要 (4 + 4)* 40 亿 = 320 亿
也就是 32G,所以这个方案不可行
这时我们可以使用哈希函数的特性
- 一个数经过哈希函数再模 40 丢到一个文件里 -> 相同的数一定会在同个文件(性质 2)
- 经过哈希函数后,散列尽量是均匀的 -> 每个文件就会大概是 1 亿上下,所以内存足够
3 哈希表
经典哈希表设计
- 通过哈希函数获得一个值,直接定位到位置,所以 CRUD 为
O(1)
- 当发生哈希碰撞的时候,一般采用拉链法
- 一个链表太长,扩容桶大小,重新哈希 -> 因为散列近似均匀,所以当一个链表太长时,就可以断定为其他链表也都很长了
- 桶扩容成本为
O(N)
,均摊下来哈希表的时间复杂度为O(1)
- 要尽量避免桶扩容的出现 -> 离线扩容、挂有序表 等改进
Java 改进
- 挂的链表太长时,转换为红黑树
- 红黑树操作成本是
logN
,同样是 8 次操作,红黑树可以检索 256 个,也就是同样操作次数下,可允许哈希碰撞 256 次 - 通过这种方式几乎在实际应用上扩容不可能出现
4 布隆过滤器
位图:通过标记二进制位上是否为 1,来确认是否加入黑名单(同理,你也可以构建白名单)
int[] arr = new int[100]
-> 只需要 100 个字节就能标志 3200 个记录- 找到 453 个记录的状态,
status = (arr[453/32] >> (453%32)) & 1
- 一维数组不够,可扩展为二维数组,同理可无限扩展
- 一定会把黑名单的内的拦截,但是会造成误判,不在黑名单内却被误认为在黑名单内 -> 因为存在哈希碰撞
- 位图不能太小,因为将标志位填满,不管什么数对应的标志都是 1,所以会提高误判率
布隆过滤器:上述只经过一次哈希函数,所以一位相同就会产生误判
- 通过多个哈希函数,获取多个位标志,这几个位标志同时为 1 时,才确认在黑名单内,大大降低误判率
- 因为同时标志多个位,所以当哈希函数太多,也很快就会把整个比特序列标志满,从而导致误判率上升。所以 哈希函数个数也不能太多
从而产生以下问题:位图要选多大?哈希函数又要多少个?首先要知道数据量 n
和预期失误率 p
(实际证明略,实际只需要公式)
- 位图大小: m = − n × ln p ( ln 2 ) 2 \mathrm{m}=-\frac{n \times \ln p}{(\ln 2)^{2}} m=−(ln2)2n×lnp
- 哈希个数: k = ln 2 × m n \mathrm{k}=\ln 2 \times \frac{m}{n} k=ln2×nm
- 真实失误率: ( 1 − e − n k m ) k \left(1-e^{-\frac{n k}{m}}\right)^{k} (1−e−mnk)k
ln2
可取 0.7相互独立的哈希函数,只需要两个使用两个独立的哈希函数线性组合