算法与数据结构——算法基础——哈希(java)(左程云b站课程笔记总结)

哈希

认识哈希函数

性质:

  • 输入域无限,输出域有限
  • MD5(02^64-1)、SHa1(02^128-1)
  • 相同的输入值一定有相同的输出值(不随机)
  • 相同的输出值不一定是相同的输入值(哈希碰撞,概率很低)
  • 映射有很平均的离散型(均匀)
  • 一堆数取哈希值并%m,得到的一堆数在0-(m-1)上均匀分布

题目1

image-20220619154721118

给定一堆无符号整数,范围如上图,有40亿个,求出现最多次数的是哪个数(限制:1G内存)

思路:

经典方法:使用哈希表key存数,value存次数

内存不够:key和value存数int都是4字节,一条记录至少8字节(还不包括索引等内存)

最差情况:40亿条记录都不一样,320亿B的内存大约是32G

哈希表内存增加是由于不同的数据,如果记录都是相同的,改变的只是value,内存不会骤增

优化思路:

把40亿个数取哈希,再分别%100,得到40亿个范围在0-99的数,因为相同的数取哈希得到的结果一样,所以再%100结果也是一样的,而且这些数可以看作均匀分布再0-99这个范围,然后准备一个文件(相同的数一定进同一个文件),先放结果为0的数,利用哈希表得到在结果为0中出现次数最多的数并记录该数以及出现的次数,清除文件内容并得到结果为1中出现次数最多的数并记录该数以及出现的次数,周而复始,统计完0-99这个范围上每个数中的结果,再统一作一次比较(比较一百个数出现的次数),得到最终结果,这样内存空间就只占用了32G/100=0.32G(大约)

认识哈希表的实现

经典哈希表的实现:哈希-取模-放置

时间复杂度:哈希O(1)、取模O(1)、数组寻址O(1)、链表中取O(k)(k较小,可以认为是O(1))、单次扩容代价O(logN)

为什么说哈希表在使用上的各种操作效率是O(1)?(理论上不是O(1),理论上是O(logN))

  1. 因为可以控制k的大小(链表的长度),减少扩容代价(k^n=N)(逼近O(1))
  2. Java虚拟机JVM可以使用离线扩容技术,不占用用户在线时间,降低哈希表的使用代价

不同的语言还有不同的优化方式:java红黑树

设计RandomPool结构

image-20220619163423989

准备两个HashMap 保证index和str能互相取到以及一个变量size(只用一个HashMap做不到时间复杂度都是O(1))

getRandom可以使用系统带的random函数来随机获取index从而返回结构中的任何一个key

注意移除方法:在移除时不能单纯的删掉HashMap中的元素,还要保证连续性(即不要在中间出现洞,会影响getRandom方法)

public class Pool<K>{
    private HashMap<K,Integer> keyIndexMap;
    private HashMap<Integer,K> indexKeyMap;
    private int size;
    
    public Pool(){
        this.ketIndexMap=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);
        }
    }
    
    public void delete(K key){
        if(this.keyIndexMap.containsKey(key)){//先判断是否有该key
            int deleteIndex=this.keyIndexMap.get(key);
            int lastIndex=--this.size;
            K lastKey=this.indexKeyMap.get(lastIndex);
            this.KeyIndexMap.put(lastKey,deleteIndex);
            this.keyIndexMap.remove(key);
            this.indexKeyMap.put(deleteIndex,lastKey);
   
            this.indexKeyMap.remove(lastIndex);
        }
    }
    
    public K getRandom(){
        if(this.size==0){
            return null;
        }
        int randomIndex=(int)(Math.random()*this.size);//0~size-1
        return this.indexKeyMap.get(randomIndex);
    }
}

详解布隆过滤器

一个集合只有添加和查询操作,没有删除行为(黑名单、爬虫去重问题)

需要较快的查询时间(比硬盘快)

牺牲掉一点的准确性去换取速度

这里的失误是指:比如黑名单,不是黑名单但我给我反馈是在黑名单中的(错杀)

不会有是黑名单但查出来不是黑名单的失误

布隆过滤器虽然可以减少内存的使用,但失误率不可避免,但可以通过设计降低失误率

比特类型的数组

int[] arr=new int[10]//长度为10的int数组是32bit*10 ->320bits

arr[0]可以表示0-31位的信息

arr[1]可以表示32-63位的信息

即一个长度为10的整型数组可以表示320个bit的bit类型数组

int i=178;//想取得178个bit的状态

int numIndex=i/32;//去哪个数上面找
int bitIndex=i%32;//去这个数上的哪一位找

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

arr[numIndex]=arr[numIndex]|(1<<(bitIndex));//把第178位的状态改成1,其他位置不变

arr[numIndex]=arr[numIndex]&(~(1<<bitIndex));//把第178位的状态改成0,其它位置不变

黑名单实际操作过程(url)

  1. 准备一个位图,长度为m,实际占用m/8个字节数(byte)
  2. url1通过hash函数得到out1然后%m得到相应的位置并做标记,然后通过下一个hash函数得到out2然后%m得到相应的位置并做标记,设定了几个hash函数就做几次这样类似的操作(k个hash函数就做k次),每一个url都做这样的操作
  3. 查url时,urlx按上述的hash函数依次调用并%m,只有结果全是1(即全都做了标记),才认为该url在黑名单中,只要有一个结果不是1就没加入过该url,因此不会出现加入过但查询结果显示没加入过这样的失误;如果m较小且数据量较大,则有可能出现没加入过黑名单的url查询结果为加入过黑名单
  4. 关键因素:m的大小,如果m很大,失误率就会很低
  5. 另外一个因素:k的大小,k实际与样本量和m相关(也可以说是根据m的大小来决定),有点类似于采集指纹

image-20220619195233291

实际需要的参数就只有两个:n样本量和p失误率

每个样本的大小只要满足能传入hash函数就可以了

面试场合有相关的问题要问是否允许一定的失误率

三个公式

image-20220619195856499

100亿的数据最后大约只需要26G(原本需要640G)(一台服务器就能搞定)

k值最后算出来向上取整

若需要26G而服务器给了32G,就有第三个公式可以算实际的失误率

详解一致性哈希原理

分布式:谈论数据服务区怎么组织的问题

逻辑层任何一个实例都是等效的(从数据库获取数据,不需要维持自己特有的数据)

数据端服务器(分布式)(可以承载更大的数据量)

假设有三台数据段服务器,每一台上面的数据都是独立的,现在要存一个数据,先经过逻辑层,再根据数据的key值经过hash函数的计算然后%3得出一个结果,结果范围0-2,分别对应三个数据库

查该数据时,也是同样的经过逻辑层(不同的逻辑服务器没有区别),然后再经过对key值进行hash函数的计算然后%3得到我应该从哪个数据服务器上取数据

底层数据段服务器组织是否负载均衡是要看高频的key、中频的key、低频的key有没有足够的数量来决定的,只要都有足够的数量,那就是能够满足负载均衡

key的选择很重要:拿什么样的key来做这样的数据划分,要选取种类多的key,如姓名和身份证号码等,像国家名就不适合做,中美的查询频率最高,高频的数量就两个,如果有三台服务器,至少有一台数据服务区压力很低,没有实现负载均衡

image-20220619204500241

如果突然数据量变大,数据库服务器不够用了(分布式)

如果增减数据库意味着全量的数据需要重新对key进行hash函数的计算和取模的操作(模的数量是数据库服务器的数量)

如何降低数据迁移的代价?

一致性哈希

一致性哈希是没有取模这一操作的

假设现在使用MD5来做hash,把hash的结果范围(0-264-1)想象成一个环,即264-1的下一位是0

把能区分机器的信息作为机器的hash值(将该信息进行hash函数的计算并放到环中)

在逻辑层把每台机器的hash值保存起来(有序数组)(每一个逻辑服务器维持的数据一样),进来想要保存一个数据时,对数据的key进行hash函数的计算,然后在逻辑层通过二分法找到hash离他最近的机器(顺时针最近,即大于等于的最近的)

image-20220619211548497

现在加一个数据服务器,比如是m4,则只需要将原本一部分归属于m3服务器的数据分给m4,减少数据服务器的时候也是同样的道理,不用对全量的数据进行迁移

image-20220619211631082

但是存在两个问题:

  1. 机器数量很少的时候,可能做不到一上来环就是均分的(hash函数均分的前提是数据量足够大)
  2. 即便数据服务器很少的时候能把环均分,一旦数据服务器增加或者减少的时候,马上会变成负载不均衡

如何解决以上问题?

虚拟节点

比如,三个服务器分别构建一千个虚拟节点,环由这些虚拟节点去抢,按比例去抢环

增加服务器时,也让第四个服务器由1000个虚拟节点去抢环,该抢谁的数据就抢谁的,数据的迁移由

按比例抢就能很好的解决初始时不均和增减服务器时不均的问题

h函数均分的前提是数据量足够大)

  1. 即便数据服务器很少的时候能把环均分,一旦数据服务器增加或者减少的时候,马上会变成负载不均衡
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值