04哈希表

124 阅读4分钟

哈希函数

哈希函数性质:

  • 1.输入域无穷大;
  • 2.输出域有穷大;
  • 3.若输入相等,则输出相等;
  • 4.若输入不相等,输出也可能相等(哈希碰撞)
  • 5.输出域上均匀分布

哈希函数特征:

  • 1.与输入规律无关;
  • 2.哈希函数生成16位的码每一位都是相互独立的(因此利用一个哈希函数可造出很多哈希函数)

哈希表的复杂度

  • 哈希表的增删改查时间复杂度是 O(1)

哈希表的扩容复杂度log以n为底N,n为扩容的倍数,比如每次扩大为原来的两倍,故时间复杂度为㏒₂N,很低,次数也少。

另外还可以用离线扩容的机制,当发现hash节点上的链表长度(经典哈希表结构)超过一定值时,在线使用的时候仍正常向原hash表中放值,但可以在不使用时,扩容,重新计算hash放入新哈希表中。

利用以上的各种优化技巧,因此哈希表的增删改查复杂度可以认为是O(1)

  • 造出多个hash函数的方式:可以通过将某一个hash函数,乘上某个系数,再加上某个常数得到一个新的hahs函数
  • 利用哈希函数做分流,处理大数据问题

极大文件中,找出重复的行

将大文件的每一行hash之后取1000的模,这样相同的字符串一定会落入同一台机器上,在同一台机器判读(可再hash,更细化一些)

设计RandomPool结构

初级6-00:56

【题目】 设计一种结构,在该结构中有如下三个功能:insert(key):将某个key加入到该结构,做到不重复加入。delete(key):将原本在结构中的某个key移除。 getRandom():等概率随机返回结构中的任何一个key。

【要求】 Insert、delete和getRandom方法的时间复杂度都是O(1)

package com.zuogod.java;

import java.util.HashMap;

/**
 * @author quanquan
 * @create 2020-04-30-13:24
 */
//使用两个哈希表
public class RandomPool {
    public static class Pool<K>{
        private HashMap<K, Integer> keyIndexMap;
        private HashMap<Integer, K> indexKeyMap;
        private int size;
        public Pool(){
            this.keyIndexMap = new HashMap<K, Integer>();
            this.indexKeyMap = new HashMap<Integer, K>();
            this.size = 0;
        }

        public void insert(K key){
            if (!this.keyIndexMap.containsKey(key)){
                this.keyIndexMap.put(key,this.size);
                this.indexKeyMap.put(this.size++,key);
            }
        }
		// 为了能够保证getRandom严格的等概率,remove中最重要的一个操作,就是把最后一个值填补至remove的元素的位置
		// 否则,一remove,hash表上就一个洞,即无法等概率返回了。
        public void remove(K key){
            if(this.keyIndexMap.containsKey(key)){
                int deleteIndex = keyIndexMap.get(key);
                int lastIndex = --size;
                K lastKey = this.indexKeyMap.get(lastIndex);
                this.keyIndexMap.put(lastKey,deleteIndex);
                this.indexKeyMap.put(deleteIndex,lastKey);
                this.keyIndexMap.remove(key);
                this.indexKeyMap.remove(lastIndex);
            }
        }
        public K getRandom(){
            if (this.size == 0){
                return null;
            }
            int index = (int)(Math.random() * this.size); //0 ~ size-1
            return this.indexKeyMap.get(index);
        }
    }
    public static void main(String[] args) {
        Pool<String> pool = new Pool<String>();
        pool.insert("zuo");
        pool.insert("cheng");
        pool.insert("yun");

        pool.remove("zuo");
        System.out.println(pool.getRandom());
        System.out.println(pool.getRandom());
        System.out.println(pool.getRandom());
        System.out.println(pool.getRandom());
        System.out.println(pool.getRandom());
    }
}

\

布隆过滤器

  • 本质为一个比特(Bit,是Binary digit(二进制数)位)数组
  • 会有一定的失误率,不会把本该命中的的判不命中,但可能把不中的判中

如上图,想要将第30000位置的比特描黑,的代码:

使用的是int,4个字节,32位

public static void main(String[] args){
	int[] arr = new int[1000]; //长度1000的整数数组,可以表示的比特位有32000个
	//也可以使用long,那就可以表示64000位
	//也可使用矩阵来描黑这个“坐标系”中的点 long[][] map = new long[1000][1000]  即 1000*1000*64个
	
	int index = 30000; //计划将index位置描黑

	int intIndex = index / 32; //算出在第几个整数
	int bitIndex = index % 32; //模 算出在整数的第几个比特位

	//将1左移bitIndex位,即第bitIndex位为1(其余位为0)或运算 arr[intIndex],即将第intIndex个整数的bitIndex位(总共的第30000位)描黑
	arr[intIndex] = (arr[intIndex]|(1 << bitIndex)); 

}

比如说有个url的黑名单,有100亿个,每次需要查看某个url是否在这个黑名单中。

  • 是可以使用前一小节讲到的利用hash函数分流的方式,将如此大数量分割至不同机器,分布式处理
  • 那么使用布隆过滤器的方式:

假如准备的比特位有m位,准备K个hash函数,将100亿的每个url,通过第一个hash1函数计算,然后% m取模(取余),将该位置描黑;

再使用hash2计算url,然后%模m,将该位置也描黑;

..... 一直使用到K个hash函数,将k个位置都描黑,。 则一个url算是在该过滤器中记录完成。

在判断的时候,一个陌生url进来,经过k个hash函数判断,其每个对应位是否都是黑的,是则名中,否则不命中

  • 从上面的设计方式来看,其失误率p,位数m大小,K大小之间是有关系的。 数学公式:

n为样本量,p为预期失误率,K为hash函数的个数:

需要的Bit位数 m:m = - (n * lognp) / (logn2)2

需要的byte字节数为:m/8

哈希函数的个数:K = ln2 * m/n = 0.7 * m/n

数组大小和哈希函数个数向上取整,真实失误率为:(1 - e ^ (- n*K/m))^K

  • 上例实际计算如下图:

勘误,上面的16G应该是22.3G,忘记平方了