哈希&布隆过滤器

哈希

1. 设计RandomPool结构

380. 常数时间插入、删除和获取随机元素

题目:设计一种结构,在该结构中有如下三个功能:

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。

解决方案:

  1. 如果数据量比较小,那么用HashMap是一个不错的解决方案,而且理论上的时间复杂度可以达到O(1),而且非常精确。
  2. 如果黑名单中url的数据量非常大,比如说100亿条数据。假设一个url为64字节,则100亿条数据就至少需要640GB的服务器来存储,实际需要的内存比640GB要大得多,所以这种方案不太现实。可以使用布隆过滤器来解决。但是会有一定的误判率:有的url不在黑名单里,得到的结果也可能为true。如果在黑名单里,肯定会返回true。

布隆过滤器实现原理

  1. 假设有一个长度为1的int类型的数组,则这个数组可存储4 byte,即32 bit 的数据,对这32个比特位进行标号(0 - 31)。
    在这里插入图片描述

  2. 假设有一个url要存入黑名单,对这个url使用三个不同的哈希函数计算出三个不同的哈希值,然后将这三个哈希值mod 32,得到3个小于32的值,并将数组中对应标号的比特位标记。
    在这里插入图片描述
    其他后续的url也按如此方式修改数组。

  3. 判断url是否在黑名单中,先将这个url使用那三个哈希函数算出哈希值,然后模32,得到的三个数值对应的格子是否都已经被标记。如果全都被标记,则此url在黑名单中(存在一定的误报率)。

    当存入的url比较多,就会把这个数组所有比特位全都标记,这个时候误报率就会很大。如何保证误报率比较小,需要用到以下三个公式,以选取合适的数组长度(比特)、哈希函数的个数

  4. 数组长度(bit),即需要多个比特位: m = ( n ln p ) / ln 2 2 m = {(n * \ln p)} / {\ln 2}^ 2 ,其中n为样本量(有多少条url要存储),p为期望的误报率。

  5. 哈希函数的个数: k = ln 2 ( m / n ) k = \ln 2 * (m/n)

  6. 得到的k和m是小数,需要向上取整,实际的误报率为: p = ( 1 e ( n k ) / m ) k p' = {(1 - e^{-(n*k)/m})}^k

3. 一致性哈希

出处

当系统数据增多时,需要多个数据库来存储同类型的数据。假设有三个数据库,分别为database1, 2, 3。那么对于新增的数据userId,是用什么策略来将其分到某个数据库。

  1. 简单哈希:求userId的哈希值,然后将其值模3,从而得到数据库的标号。
    缺点:如果用户激增,需要添加一个数据库。就需要将前三个数据库中的所有数据重新计算哈希值然后模4分配数据库,代价相当大。

  2. 一致性哈希:一致性 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里。

发布了108 篇原创文章 · 获赞 22 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/GoSantiago/article/details/104703521