8.哈希函数与哈希表

166 阅读3分钟
0. 基础知识

哈希函数:输入无限,输出有限,一个输入对应一个输出,但一个输出可能对应多个输入(哈希碰撞),且输出域具有离散性,均匀分布

MD5:长度为16位,采用16进制(0~f),范围为[ 0, 2^64-1 ]

SHA1:长度为32位,采用16进制,范围为[ 0, 2^128-1]

字节换算

1 T = 2^10 G = 2^10 * 2^10 MB = 2^10 * 2^10 *2^10 KB = 2^40 B = 2^40 * 8 b

1 B = 8 b
1 kB = 2^10 B
1 MB = 2^10 KB
1 G = 2^10 MB
1 T = 2^10 G

1 G = 2^30 B = 1073,741,824 B ~= 10亿B
// 一个int整型长度为 4B = 32b,范围为[-2^31,2^31-1]
1.设计一个RandomPool结构

有三个功能,时间复杂度为 O(1)

insert(key): 将key插入结构中,且不重复

delele(key): 将结构中的key删除

getRandom(): 等概率随机返回结构中的任意一个key

class RandomPool {
  constructor() {
    // 维护两个哈希表,记录key-index和index-key,且结构中有size属性
    this.keyIndexMap = new Map()
    this.indexKeyMap = new Map()
    this.size = 0
  }
  insert(key) {
    if (!this.keyIndexMap.has(key)) {
      this.keyIndexMap.set(key, this.size)
      this.indexKeyMap.set(this.size, key)
      this.size++
    }
  }
  delete(key) {
    // 要实现等概率随机返回key,那么key索引应保持连续,
    // 删除操作时采用最后一个元素替换当前元素的方式实现
    if (this.keyIndexMap.has(key)) {
      this.size--
      let curIndex = this.keyIndexMap.get(key)
      let lastKey = this.indexKeyMap.get(this.size)
      this.keyIndexMap.set(lastKey, curIndex)
      this.indexKeyMap.set(curIndex, lastKey)
      
      this.keyIndexMap.delete(key)
      this.indexKeyMap.delete(this.size)
    }
  }
  getRandom() {
    if (this.size == 0) return null
    return this.indexKeyMap.get(Math.floor(Math.random() * this.size))
  }
}
2.布隆过滤器(大厂常考)

用于设计黑名单系统、解决爬虫去重问题,采用hash存储,极大的减少存储空间

考虑set的addcheck

黑名单不会判断为白,但白名单有极小几率判断为黑

// 位图存储
function example() {
  let arr = new Array(10) // int[]  32bit * 10 -> 320 bits
  // arr[0] int 0 ~ 31
  // arr[1] int 32 ~ 63
  // arr[2] int 64 ~ 95

  let i = 178 // 取178位

  let numIndex = i / 32 // 数索引
  let bitIndex = i % 32 // 位索引

  // 1.查i位状态
  let status = (arr[numIndex] >> bitIndex) & 1 // (11101 >> 2) & 0001 == 1,2位状态为1
  // 2.改i位状态为1
  arr[numIndex] = arr[numIndex] | (1 << bitIndex)
  // 3.改i位状态为0
  arr[numIndex] = arr[numIndex] & (~(1 << bitIndex))

}
// 假如使用m bit的长度的数组来存储黑名单Un,fk为哈希函数
U1 --f1--> out1 --%m--> 0到m-1的随机数
U1 --f2--> out1 --%m--> 0到m-1的随机数
U1 --fk--> out1 --%m--> 0到m-1的随机数

U2 --f1--> out2 --%m--> 0到m-1的随机数
U2 --f2--> out2 --%m--> 0到m-1的随机数
U2 --fk--> out2 --%m--> 0到m-1的随机数

...
Un --f1--> outn --%m--> 0到m-1的随机数
Un --f2--> outn --%m--> 0到m-1的随机数
Un --fk--> outn --%m--> 0到m-1的随机数

// 将数组上随机数索引处置1表示黑名单
// 这样在调用k个哈希函数查某个黑名单时返回k个值都为1,查白名单时返回的K个值中只要有0那么必然是白名单
// 存储数组位长度m及调用的哈希函数数目k根据Un长度以及可容错率p设定,尽可能减少hash碰撞

布隆过滤器考虑点:

样本量n、失误率p、存储长度m、哈希调用次数k

m = (-n*lnP) / (ln2)^2   bit
k = Math.ceil(ln2 * m / n) ~= 0.7*m/n 个
3.一致性哈希

存在问题:

  • 服务器负载不均衡
  • 即使负载均衡了,在添加或移除某个服务器时,又不均衡了

一致性哈希:

  • 在服务器数量少时,如何把环均分?
  • 如何做到负载均衡?
// 机器生成1000个哈希值(虚拟节点),最后再去抢环
// 由于哈希输出具有离散性,固只要将一台机器切分成足够多的虚拟节点,在环上分布就足够均匀!
m1(a1, a2, ... , a1000)
m2(b1, b2, ... , b1000)
m3(c1, c2, ... , c1000)
// 如此占环之后,环上任意一段分布的不同机器虚拟节点数一定相同!
// 若m1是强负载,m2是中负载,m3是低负载,那么m1切分出1500个,m2切分出1000个,m3切分出500个即可做到负载均衡