【左程云 数据结构与算法笔记】P12 基础提升 哈希函数与哈希表

260 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第21天,点击查看活动详情 

下面是我整理的跟着b站左程云的数据结构与算法学习笔记,欢迎大家一起学习。

认识哈希函数和哈希表的实现

哈希函数

  1. 输入可以无穷大 输出域相对有限且有对应
  2. 相同输入相同输出
  3. 不同输入可能有相同输出(哈希碰撞) 哈希碰撞的概率极低,只有在输入极大量才有可能

认识哈希函数的均匀性与离散性

假设有1000个输入在圆圈上有映射

能够几乎均匀的离散到区域中,任取一块区域其包含的点数几乎一致

输出没有规律性,不会集中在某一个区域上 离散性:f函数能够将输出无规律的映射,彻底离散掉

均匀性与离散性(哈希函数两个重要的性质)得到保证

哈希函数的特征:离散性和均匀性越好,哈希函数越优秀

思考,用1G内存实现HashMap统计大量数中最多次使用的数

一条哈希表记录假设8B,若有42亿+数,最差情况每个都只有一个,需要32G以上内存,内存不足 可以将f函数得到的结果对100求模,放进0-99含有不同种数的数量差不多,可以将42亿+的数均匀放进100个文件中去,相当于将得到的哈希值%100再放到对应的文件中去,使用32G/100的内存,在这100个文件统计寻找出现次数最大的数后释放空间,得到一百个出现次数最大的数

碰撞的种数也是均分的, 主要是利用哈希函数在种数上均分达到防止内存不足

哈希表的实现

可以将哈希函数的结果对某一个数取模,分配到不同位置中,并用链表相连 扩容机制 当某一条线上长度不足时,因为总体分布均匀,所有线都可以扩容,也可以做到均分的长度增大,如17变为34,将所有链表的长度也能缩小一半,重新分配,扩容相当于在保持链表长度的基础上扩容哈希表的分配情况

扩容代价 加入了N个str,经历logN次扩容 初始时链长度不超过K,最差情况为logN(K=2),可能更小但也是logN级别 每次扩容之后全部重新计算哈希值重新挂在不同格子上 代价为O(N)

整体的扩容代价为O(N * logN)

单次扩容代价为O(N * logN) 即每次增删改查单次的平均代价为logN

但一般可以认为使用哈希表增删改查单次可以为O(1)

原因:

  1. 可以将k变长,使扩容代价变小,将logN变成小常数,逼近O(1)
  2. 离线扩容机制(主要是利用JVM的一些机制)将链表长度较长的哈希表自动扩容并指向新的哈希表 进一步降低哈希表的使用代价 哈希表在使用时可以认为增删改查的代价为O(1)常数级别,理论上为O(logN)

开放寻址法

在桶中,用数组实现单链表结构,当内存不够时,指向下一个新开辟数组,开放地址

设计一个RandomPool结构

使用HashMap结构设计 准备两个哈希表 map1(str->index) map2(index->str) size=0

insert

若没有删除,构建了一个index连续的hashMap,等概率返回一个字符串,相当于返回一个26以内的随机数 删除时应保证index连续防止出现很多空洞 假如删D时 ,找到D的位置,将index[size-1]填上去到D位置上替换D,size-- 此时z在hashMap上的对应为3 相当于用最后一条记录填补空白,并将最后一条记录去掉 保证index上的数字连续且有对应值 ,getRandom()方法不用考虑跳过某些index 删除的核心代码

public void delete(K key) {  
    if (this.keyIndexMap.containsKey(key)) {  
        int deleteIndex = this.keyIndexMap.get(key);  
        int lastIndex = --this.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);  
    }  
}

布隆过滤器

更好的扩容与提升负载 能够解决黑名单和查重去重 假如有大量的url需要存进一个集合中,当用户访问时需要判断黑名单中是否存在url,即构建一个有大量数据的黑名单,不用删除 若单纯存进HashSet,需要耗费大量的空间 内存使用过多,布隆过滤器能极大减少内存,但会有一定失误率 布隆过滤器可以实现增加和查询,但没有删除 只可能出现不存在却返回存在的情况,即可能出现错杀,但不会漏杀 如黑名单中 先看一个位图

public static void main(String[] args) {  
    int a=0;  
    //a 32bit  
    int[] arr=new int[10];  
    int i=178;  
    //得到i上bit的状态  
    int numIndex=178/32;  
    int bitIndex=178%32;  
    //得到i上bit的状态  
    int s=((arr[numIndex]>>bitIndex)&1);//s ->0  
    System.out.println(arr[numIndex]);  
    //将178位上的状态改为1  
    arr[numIndex]=arr[numIndex]|(1<<bitIndex);  
    System.out.println(arr[numIndex]);  
  
    //把i位上状态改为0  
    i=178;  
    arr[numIndex]=arr[numIndex]&(~(1<<bitIndex));
}

一个int[10]数组能表示320bits 注意取出某一位上的bit数时,使用&1,一个奇数(十进制)位与1的结果是1一个偶数(十进制)位与1的结果是0 从而得到最后一个比特数 下面是布隆过滤器的实现原理 准备一个长度为m的数组 **插入url:**通过k个不同的哈希函数计算得到不同的哈希值%m得到不同的位置,将指定位置标黑 判断状态:通过调用k个哈希函数%m,只有指定位置全为1时,则url在黑名单里,通过调用k个不同的哈希函数,将对应位置描黑,不可能出现下次同样调用函数得到上次调用位置而数据不一致,所以只可能有错杀的情况,即白名单被判断为黑名单(概率极小) 决定失误率:取决于位图的大小m,若太小,很容易失误 K根据m和样本量决定,不宜过小与过大

失误率与m和k的关系

布隆过滤器主要跟以下有关

  1. set(不用删)
  2. 失误率 ps:单样本大小无关 n=样本量 p=失误率 m=- ( n* lnp )/((ln2)^2) bit 原本需要640G的内存 用布隆过滤器可以减少到只需要26G k=ln2* n/m 约等于0.7m/n 个

详解一致性哈希原理

一致性哈希 讨论数据与服务器之间如何组织 相当于在分布式架构中确定底层数据归属 数据种类均匀分配,也可以达到总体负载均衡(高频key与中频,低频)总体分配均匀,提升服务器负载能力 hashKey比较适合人名等不容易重复的名词, key的种类应该很多,像男女只有匹配两个服务器 数据迁移时增加数据的代价是全量的,如增加或修改一个服务器,需要将服务器的数据取出再计算存入哪台服务器中 可以通过加服务器的信息存进一个有序列表,按顺序寻找第一个>=它的服务器,不再通过取模 在添加服务器时,可以只在某一服务器上取数据 其他服务器上的数据不用动,当m4减少时,将m4的数据直接传给顺时针找到比他大的值m3 增加机器与减少机器数据迁移的代价小了很多 可能面临的问题

  1. 机器数量很少时,很难做到一上来环就均衡
  2. 增加或减少一个机器时,负载马上不均衡 可以使用虚拟节点技术解决 机器m1,m2,m3分配1000个字符串 虚拟节点抢环 用m1,m2,m3所分配的字符串计算出哈希值抢环 环上有3000个点 虚拟节点之间的数据迁移 很容易实现 在实际的m1中取一个数据给m2中一个位置 在环中取一小段来自m1,m2,m3的数量相差不大,按比例抢环 增加机器时,均匀增加,概率相等;减少数据时,将数据均匀几乎等量的给其他机器,也实现了超均衡 按比例抢能解决初始分配不均和增加减少机器时不均衡的问题

基于此方法还能够管理负载 可以视机器情况分配不一样的节点,若A机器强,则可以分配更多的虚拟节点,从而实现更好的性能,实现管理负载