哈希表与布隆过滤器

文章目录

  • 一、哈希函数是什么?
    • 哈希函数的应用场景
    • 哈希函数的构造方法
  • 二、哈希表
      • 哈希表的底层设计
      • 题型
  • 三、布隆过滤器
      • 布隆过滤器优点
      • 布隆过滤器缺陷
      • 布隆过滤器使用场景
    • 一致性哈希算法.
    • 位图
  • 3. 海量数据面试题


一、哈希函数是什么?

1 : 哈希函数没有随机值
2 : 理论上的哈希碰撞是不可避免的(因为,输入域无限,输出域有限)
3 : 哈希函数具有离散性和均匀性

离散型指 : 即使是有规律的数字算出来的结果也是大不相同的
均匀性指 : 哈希函数算出来的结果总是均匀分布在输出 域中

哈希函数的应用场景

1:对主要操作是查找
2:对数据没有逻辑上的要求
3:先让数据均匀分配

哈希函数的构造方法

掌握常用的俩种就行

  1. 直接定制法-
    取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B 优点:简单、均匀 缺点:需要事先知道关
    键字的分布情况 使用场景:适合查找比较小且连续的情况 面试题:字符串中第一个只出现一次字符
  2. 除留余数法–
    设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:
    Hash(key) = key% p(p<=m),将关键码转换成哈希地址

二、哈希表

哈希表,就是一种运用哈希函数的数据结构
哈希表空间的使用-只和输入不同的数有关
哈希表在使用上是可以认为O(1)的但不是理论上-理论上就是logN的

哈希表的底层设计

给定一个输出域,通过哈希函数算出一个值,模一个值给定到输出域中,然后后来来的值以单链表的形式加到这个值的后面.
如果这个单链表的值的数量过大了,那就的扩容.也就是将原来的哈希表中的每一个值,重新算一遍哈希模一个新值,然后在以链表形式串起来.

哈希表查找和删除为什么是O(1),因为通过哈希函数算肯定是O(1)的(即使是这个时间长),而通过单链表查询的话(如果这个单链表很短,那么也认为是O(1),单链表长度一旦过长就出发扩容操作)
扩容的时间复杂度怎么算 ,假设一次扩容一倍,时间复杂度就是logN,那么总的给定N个数的时间复杂度O(NlogN),而单次扩容的时间复杂度O(NlogN) / N = logN 这是一个小常数,而且这个哈希表不一定一次只扩容1倍,那么通过增加扩容的数量,这个logK会更小,可以认为单次的logK = O(1)
而且Java有一种技术就是离线扩容技术,可以进一步加速这个时间复杂度(C++没有)
因为Java有jvm会保存哈希表,所以Java可以一边扩容一边使用老的哈希表,等新表扩容(不占用用户时间)完,在释放旧表使用新表

题型

1:如果给定你1G的内存,给你40亿的数,要求里找出出现次数最多的数
解题思路:
首先经典解题思路,就是将所有数都加入到哈希表中然后去找,但是内存肯定会爆掉
怎样设计,就是将40亿的数都算一遍哈希值,然后模个100,放到0-99的文件(这些文件肯定是能放到内存中的)中,然后在这些文件中找出现次数最多的数,然后比较所有文件中出现次数最多的数.

原理:就是哈希相同的值一定会算到同一个文件中,不同的数会根据离散型均匀的分配到文件中.

2 : 哈希表题:设计一个哈希表,能够随机的返回哈希表中的每一个值
哈希表与布隆过滤器_第1张图片

 //实现哈希随机
    public static class Pool<K>{
        private HashMap<K,Integer> keyIndexMap;
        private HashMap<Integer,K> indexKeyMap;
        private int size;

        public Pool(){
            this.keyIndexMap = new HashMap<>();
            this.indexKeyMap = new HashMap<>();
            this.size = 0;
        }
        public void insert(K key){
            if (!keyIndexMap.containsKey(key)){
                this.keyIndexMap.put(key,size);
                this.indexKeyMap.put(size++,key);
            }
        }
        public void delete(K key){
            if (keyIndexMap.containsKey(key)){
                //记录位置,然后堵洞
                int deleteIndex = keyIndexMap.get(key);
                //将最后一个位置拿出来
                int lastIndex = --size;
                K lastKey = indexKeyMap.get(lastIndex);
                //堵洞交换
                keyIndexMap.put(lastKey,deleteIndex);
                indexKeyMap.put(deleteIndex,lastKey);
                keyIndexMap.remove(key);
                indexKeyMap.remove(lastIndex);
            }
        }
        public K getRondom(){
            if (size == 0)return null;
            int randomIndex =(int) (Math.random()*size);//0 ~ size -1
            return indexKeyMap.get(randomIndex);
        }
    }

三、布隆过滤器

布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间

解决,类似于黑名单查找的问题(爬虫行为)-没有删除行为,允许一定的失误率(失误率很低)

哈希表与布隆过滤器_第2张图片

失误率:假如一个号是黑号,那么这个黑号一定能被查找到,但是也允许一定的白号但是被误杀了

布隆过滤器是哈希表和位图的结合
先将字符串用字符串哈希算法映射到哈希表中
但是由于哈希冲突,我们可以把一个字符串用多个不同的字符串哈希算法同时映射在整个哈希表中
要判断一个字符串是否在这堆字符串中,我们可以算出这个字符串的位置,当且仅当这个字符串每个映射位置都是1的时候才表示存在,只要有一个位置为0,就表示不存在

俩个问题:用多少个哈希函数,位图到底定多大(m)
哈希函数:根据样本量和位图大小来订(类似于猜指纹,不能太多要不位图全部被填满了,太少可能不准确)

三个公式
哈希表与布隆过滤器_第3张图片

package bloomfilterdemo;

import java.util.BitSet;

/**
 * @Author 12629
 * @Description:
 */
class SimpleHash {

    public int cap;//当前容量
    public int seed;//随机

    public SimpleHash(int cap,int seed) {
        this.cap = cap;
        this.seed = seed;
    }

    //根据seed不同 创建不能的哈希函数
    int hash(String key) {
        int h;
        //(n - 1) & hash
        return (key == null) ? 0 : (seed * (cap-1)) & ((h = key.hashCode()) ^ (h >>> 16));
    }

}
public class MyBloomFilter {

    public static final int DEFAULT_SIZE = 1 << 20;
    //位图
    public BitSet bitSet;
    //记录存了多少个数据
    public int usedSize;

    public static final int[] seeds = {5,7,11,13,27,33};

    public SimpleHash[] simpleHashes;

    public MyBloomFilter() {
        bitSet = new BitSet(DEFAULT_SIZE);
        simpleHashes = new SimpleHash[seeds.length];
        for (int i = 0; i < simpleHashes.length; i++) {
            simpleHashes[i] = new SimpleHash(DEFAULT_SIZE,seeds[i]);
        }
    }

    /**
     * 添加元素 到布隆过滤器
     * @param val
     */
    public void add(String val) {
        //让X个哈希函数  分别处理当前的数据
        for (SimpleHash simpleHash : simpleHashes) {
            int index = simpleHash.hash(val);
            //把他们 都存储在位图当中即可
            bitSet.set(index);
        }
    }
    /**
     * 是否包含val ,这里会存在一定的误判的
     * @param val
     * @return
     */
    public boolean contains(String val) {
        //val  一定 也是通过这个几个哈希函数去 看对应的位置
        for (SimpleHash simpleHash : simpleHashes) {
            int index = simpleHash.hash(val);
            //只要有1个为 0     那么一定不存在
            boolean flg = bitSet.get(index);
            if(!flg) {
                return false;
            }
        }
        return true;
    }

    public static void main(String[] args) {
        MyBloomFilter myBloomFilter = new MyBloomFilter();
        myBloomFilter.add("hello");
        myBloomFilter.add("hello2");
        myBloomFilter.add("bit");
        myBloomFilter.add("haha");

        System.out.println(myBloomFilter.contains("hello"));
        System.out.println(myBloomFilter.contains("hello3"));
        System.out.println(myBloomFilter.contains("he"));
    }
}

模拟实现2

import java.util.BitSet;
	// 构建哈希函数
	class SimpleHash {
	//容量
		private int cap;
		//随机种子
		private int seed;
		public SimpleHash( int cap, int seed) {
		this.cap= cap;
		this.seed =seed;
	}
	/**
	* 把当前的字符串转变为1个哈希值
	* @param value
	* @return
	*/
	public int hash(String value) {
		int result=0 ;
		
		int len= value.length();
		for (int i= 0 ; i< len; i ++ ) {
			result = seed* result + value.charAt(i);
		}
		return (cap - 1 ) & result;
		}
	}
	public class BloomFilter {
		private static final int DEFAULT_SIZE = 1 << 24 ;//方便哈希函数的计算
		private static final int [] seeds = new int []{5,7, 11 , 13 , 31 , 37 , 61};
		private BitSet bits; // 位图用来存储元素
		private SimpleHash[] func; // 哈希函数所对应类
		private int size = 0;
		//初始化bits和func
		public BloomFilter() {
		bits= new BitSet(DEFAULT_SIZE);
		func = new SimpleHash[seeds.length];
		//把所有哈希对象进行初始化
		for( int i= 0 ; i< seeds.length; i ++ ) {
			func[i]=new SimpleHash(DEFAULT_SIZE, seeds[i]);
		}
	}
	public void set(String value) {
		if(null == value)
		return;
		for(SimpleHash f : func) {
			bits.set(f.hash(value));
		}
		size++;
	}
	public boolean contains(String value) {
		if(value ==null ) {
			return false;
		}
		for(SimpleHash f : func) {
			if(!bits.get(f.hash(value))){
				return false;
			}
		}
		return true;//会有误判
	}
	public static void main(String[] args) {
		String s1 = "欧阳锋";
		String s2 = "欧阳克";
		
		String s3 = "金轮法王";
		String s4 = "霍都";
		BloomFilter filter=new BloomFilter();
		filter.set(s1);
		filter.set(s2);
		filter.set(s3);
		filter.set(s4);
		System.out.println(filter.contains("杨过"));
		System.out.println(filter.contains("金轮法王"));
	}
}

布隆过滤器优点

  1. 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关
  2. 哈希函数相互之间没有关系,方便硬件并行运算
  3. 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
  4. 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
  5. 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
  6. 使用同一组散列函数的布隆过滤器可以进行交、并、差运算

布隆过滤器缺陷

  1. 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白
    名单,存储可能会误判的数据)
  2. 不能获取元素本身
  3. 一般情况下不能从布隆过滤器中删除元素
  4. 如果采用计数方式删除,可能会存在计数回绕问题

布隆过滤器使用场景

  1. google的guava包中有对Bloom Filter的实现
  2. 网页爬虫对URL的去重,避免爬去相同的URL地址。
  3. 垃圾邮件过滤,从数十亿个垃圾邮件列表中判断某邮箱是否是垃圾邮箱
  4. 解决数据库缓存击穿,黑客攻击服务器时,会构建大量不存在于缓存中的key向服务器发起请求,在数
    据量足够大的时候,频繁的数据库查询会导致挂机。
  5. 秒杀系统,查看用户是否重复购买

一致性哈希算法.

这里不写了,看下这篇博客

https://zhuanlan.zhihu.com/p/129049724

哈希表与布隆过滤器_第4张图片

位图

所谓位图,就是用每一位来存放某种状态,适用于海量数据,整数,数据无重复的场景。通常是用来判断某个数据存不存在的

用来快速判断一个整数是否在一堆整数中

二进制用0和1来表示数据,位图根据0和1来存储对应的数据,可以大大节省存储空间,并具备排序特性。

public static void main(String[] args) {
        int a =0;// a 32 bit
        int [] arr =new int[10];// 32*10 -> 320bits数组
        /* arr [0] int 0 - 31
        *  arr [1] int 32 - 63 */
        int i =178;//想取第178个bit 状态

        int numIndex = 178/32;//找到178所在的数字
        int bitIndex = 178%32;//找到178所在数字中的位数

        //拿到178位的状态
        int s = (   (arr[numIndex] >> (bitIndex))       & 1);

        //将178位的状态改成 1
        arr[numIndex] = arr[numIndex] | ( 1 << (bitIndex) );


        //将178位的状态改成 0
        arr[numIndex] = arr[numIndex] & ( ~ 1 << (bitIndex) );

    }
public class MyBitSet {
	private byte[] elem;
	public int usedSize;
	public MyBitSet() {
	//默认只给一个大小
		elem = new byte[1];
	}
	/**
	* n个比特位
	* @param n
	*/
	public MyBitSet(int n) {
		elem = new byte[n/8+1];
	}
	/**
	*
	* @param val 可以等价于 将数据的对应位置置为1
	*/
	public void set(int val) {
		if(val < 0 ) {
			throw new IndexOutOfBoundsException();
		}
		int arrayIndex = val/8;
		int bitIndex = val % 8;
		this.elem[arrayIndex] |= (1 << bitIndex);
		usedSize++;
	}
	/**
	* 测试该数字是否存在
	* @param val
	* @return
	*/
	public boolean get(int val) {
		if(val < 0 ) {
			throw new IndexOutOfBoundsException();
		}
		int arrayIndex = val/8;
		int bitIndex = val % 8;
		//判断 elem[index] 的对应位是不是1 是的话与的结果是不等于0的
		if((this.elem[arrayIndex] & (1L << bitIndex)) != 0) {
		return true;
	}
	
	return false;
	}
	/**
	*
	* @param val 可以等价于 将数据的对应位置置为0
	*/
	public void reSet(int val) {
		if(val < 0 ) {
			throw new IndexOutOfBoundsException();
		}
		int arrayIndex = val/8;
		int bitIndex = val % 8;
		this.elem[arrayIndex] &= ~(1L << bitIndex);
		usedSize--;
	}
	/**
	* 当前比特位有多少个1
	* @return
	*/
	public int getUsedSize() {
		return this.usedSize;
	}
}

3. 海量数据面试题

哈希切割
给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址? 与上题条件相同,
如何找到top K的IP?
位图应用

  1. 给定100亿个整数,设计算法找到只出现一次的整数?
  2. 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
  3. 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数
    布隆过滤器
  4. 给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和
    近似算法
  5. 如何扩展BloomFilter使得它支持删除元素的操作

你可能感兴趣的:(数据结构与算法,散列表,数据结构)