哈希
1. 设计RandomPool结构
题目:设计一种结构,在该结构中有如下三个功能:
insert(key):将某个key加入到该结构,做到不重复加入。
delete(key):将原本在结构中的某个key移除。
getRandom(): 等概率随机返回结构中的任何一个key。要求: Insert、delete和getRandom方法的时间复杂度都是 O(1)
思路:使用两个相互对应的HashMap,map的尺寸为size。第一个keyIndexMap存[key, value]
,第二个IndexKeyMap存[vlaue, key]
。其中value为该key存入map的次序(从0开始),将其作为索引。
getRandom()方法:使用Math.random() * size,根据size值,等概率地从尺寸为size的map中获取到一个value,从而获取到对应的键值对。
delete(key)方法:修改最后一个键值对,修改后将其覆盖到相应的删除位置,然后删除键值对含“key”的键值对。
package hash;
import java.util.HashMap;
public class Solution_RandomizedSet {
public static class RandomizedSet{
private HashMap<Integer, Integer> keyIndexMap;
private HashMap<Integer, Integer> indexKeyMap;
private int size;
public RandomizedSet(){
keyIndexMap = new HashMap<>();
indexKeyMap = new HashMap<>();
size = 0;
}
public boolean insert(int val){
if(keyIndexMap.containsKey(val)){//如果已包含val,则返回false
return false;
}
keyIndexMap.put(val, size);
indexKeyMap.put(size, val);
size++;
return true;///插入成功返回true
}
public boolean remove(int val){
if(!keyIndexMap.containsKey(val)){
return false;
}
//要删除的key对应的索引,其键值对为[key,deleteIndex]
int deleteIndex = keyIndexMap.get(val);
int lastIndex = size - 1;
//indexKeyMap中的最后一个,其键值对为[lastIndex,lastKey]
int lastKey = indexKeyMap.get(lastIndex);
//使用put(key,value)函数,如果哈希表中已经存在键值key,则其对应的值被value覆盖
keyIndexMap.put(lastKey, deleteIndex);
keyIndexMap.remove(val);
indexKeyMap.put(deleteIndex, lastKey);
indexKeyMap.remove(lastIndex);
size--;
return true;
}
public int getRandom(){
int random = (int) (Math.random() * size);
return indexKeyMap.get(random);
}
}
public static void main(String[] args) {
RandomizedSet set = new RandomizedSet();
System.out.println(set.insert(1));
System.out.println(set.remove(2));
System.out.println(set.insert(2));
System.out.println(set.getRandom());
System.out.println(set.remove(1));
System.out.println(set.insert(2));
System.out.println(set.getRandom());
}
}
381. O(1) 时间插入、删除和获取随机元素 - 允许重复
2. 布隆过滤器
引例:一个搜索引擎公司要制作一个黑名单,这些黑名单存放着一些网站的url。当访问一个url时,如果这个url在这个黑名单中,返回true,则不会跳转到该网站,否则返回false。
解决方案:
- 如果数据量比较小,那么用HashMap是一个不错的解决方案,而且理论上的时间复杂度可以达到O(1),而且非常精确。
- 如果黑名单中url的数据量非常大,比如说100亿条数据。假设一个url为64字节,则100亿条数据就至少需要640GB的服务器来存储,实际需要的内存比640GB要大得多,所以这种方案不太现实。可以使用布隆过滤器来解决。但是会有一定的误判率:有的url不在黑名单里,得到的结果也可能为true。如果在黑名单里,肯定会返回true。
布隆过滤器实现原理
-
假设有一个长度为1的int类型的数组,则这个数组可存储4 byte,即32 bit 的数据,对这32个比特位进行标号(0 - 31)。
-
假设有一个url要存入黑名单,对这个url使用三个不同的哈希函数计算出三个不同的哈希值,然后将这三个哈希值mod 32,得到3个小于32的值,并将数组中对应标号的比特位标记。
其他后续的url也按如此方式修改数组。 -
判断url是否在黑名单中,先将这个url使用那三个哈希函数算出哈希值,然后模32,得到的三个数值对应的格子是否都已经被标记。如果全都被标记,则此url在黑名单中(存在一定的误报率)。
当存入的url比较多,就会把这个数组所有比特位全都标记,这个时候误报率就会很大。如何保证误报率比较小,需要用到以下三个公式,以选取合适的数组长度(比特)、哈希函数的个数
-
数组长度(bit),即需要多个比特位: ,其中n为样本量(有多少条url要存储),p为期望的误报率。
-
哈希函数的个数:。
-
得到的k和m是小数,需要向上取整,实际的误报率为:
3. 一致性哈希
当系统数据增多时,需要多个数据库来存储同类型的数据。假设有三个数据库,分别为database1, 2, 3。那么对于新增的数据userId,是用什么策略来将其分到某个数据库。
-
简单哈希:求userId的哈希值,然后将其值模3,从而得到数据库的标号。
缺点:如果用户激增,需要添加一个数据库。就需要将前三个数据库中的所有数据重新计算哈希值然后模4分配数据库,代价相当大。 -
一致性哈希:一致性 Hash 算法也是使用取模的思想,只是,刚才描述的取模法是对数据库数量进行取模,而一致性Hash算法是对
2^32
取模,什么意思呢?简单来说,一致性Hash算法将整个哈希值空间组织成一个虚拟的圆环,如假设某哈希函数H的值空间为0 - 232-1(即哈希值是一个32位无符号整形),整个哈希环如下,从 0 ~ 232-1 代表的分别是一个个的节点,这个环也叫哈希环然后将数据库当做一个节点进行哈希运算,将其落在哈希环上。(这四个数据库有可能不能把哈希环平分,就需要用到虚拟节点技术)
我们约定:数据 key 的哈希值落在哈希环上的节点,如果命中了数据库节点就落在这个数据库上,否则落在顺时针直到碰到第一个数据库。(即每个数据库承包环上的一部分区间。如上图中D2数据库中存储的是落在D0和D2区间上的节点)- 新增数据库节点:(以上图为例)如果4个数据库还不够,需要再加一个数据库D5,将D5当做一个节点计算哈希值然后经过哈希运算取模。加入D5落在D2和D1之间,那么只需将D2和D5之间的数据节点重新哈希,D5和D1之间的数据依旧保留在D1中
- 删除数据库接节点:(以上图为例)如果要删除D2数据库,只需要将D0到D2之间的数据节点重新哈希即可。
虚拟节点技术
一致性Hash算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜(被缓存的对象大部分集中缓存在某一台服务器上)问题。
为了避免出现数据倾斜问题,一致性 Hash 算法引入了虚拟节点的机制,也就是每个机器节点会进行多次哈希,最终每个机器节点在哈希环上会有多个虚拟节点存在,使用这种方式来大大削弱甚至避免数据倾斜问题。
如上图所示,系统中只有两个数据库,D0和D1,但是总共有5个虚拟节点D0#1、D0#2、D1#1、D1#2、D1#3,
其中落在(D1#2,D0#1]和(D1#1,D0#2]这两个区间上的数据节点存储在D0里,其他数据节点存储在D1里。
来源:CSDN
作者:GoSantiago
链接:https://blog.csdn.net/GoSantiago/article/details/104703521