位图
1.位图的概念
引入一道面试题
给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数 中。
解法1:直接使用暴力遍历查找该数,时间复杂度为O(N)
解法2:使用排序并且二分查找,时间复杂度为O(N*logN+logN)
上述两种解法的时间复杂度都较高并且内存也不够,使用外部排序也会进行多次IO操作,如何在较短的时间内查找该数?
位图:所谓位图,就是用每一位来存放某种状态,适用于海量数据,整数,数据无重复的场景。通常是用来判 断某个数据存不存在的。
int[] array = {1, 3, 7, 4, 12, 16, 19, 13, 22, 18};
在该数组中,10个整数本来应该存放40个字节。
7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 1 0 0 1 1 0 1 0 0 0 1 1 0 0 0 0 0 1 0 0 1 1 0 1 每8个元素代表着一个byte,每一个元素代表着一个bit
1代表着 1=1/8+1%8 此时在第一个byte的1元素为1,代表着1存在并且存储
3=3/8+3%8此时在第一个byte的3元素为1,代表着3存在并且存储
13=13/8+13%8代表着第二个byte的5元素为1,代表着13存在并且存储
22=22/8+22%8代表着第三个byte的6元素为1,代表着22存在并且存储
上述存储的数据结构就是位图,这里我们可以记住:10亿个字节大概是0.9G,可看作1G,10亿个比特位大概是119兆,看作128兆
如何实现位图?下面是代码实现
2.代码实现
package myBitSet;
/**
* @version 1.0
* @auther zhou
*/
public class BItSet {
private byte[] BitSet;
private static final int DEFAULT_CAPACITY = 10;
private int remainder;
private int divisor;
private int usedsize;
BItSet() {
BitSet = new byte[DEFAULT_CAPACITY];
}
BItSet(int number) {
BitSet = new byte[number / 8 + 1];
}
public void setBitSet(int number) {
if(number<0) {
throw new IndexOutOfBoundsException();
}
remainder = number % 8;
divisor = number / 8;
BitSet[divisor] |= (1 << remainder);
usedsize++;
}
//测试该数字是否存在
public int getBitSet(int number) {
if(number<0) {
throw new IndexOutOfBoundsException();
}
remainder = number % 8;
if ((BitSet[number / 8] & (1 << (remainder))) == 0) {
return 0;
}
return 1;
}
//这一部分最开始写的是等于1的时候返回,在测试的时候总是不对,后来想了想,有1的那一位不一定是 在第一位,但是在=0的时候肯定不存在,剩下的情况一定是存在,在测试的时候通过
//把对应位置 置为0
public void getZero(int val) {
if(val<0) {
throw new IndexOutOfBoundsException();
}
remainder = val % 8;
BitSet[val / 8] &= ~(1 << remainder);
usedsize--;
}
//在最开始写的是BitSet[val / 8] |= ~(1 << remainder);后来改成&
//val可以等价于将数据的对应位置置为1
public void getOne(int val) {
if(val<0) {
throw new IndexOutOfBoundsException();
}
remainder = val % 8;
BitSet[val / 8] |= 1 << remainder;
usedsize++;
}
//当前比特位有多少个1
public int howManyOne() {
return usedsize;
}
}
如果想要增加存储数字的容量(范围),将byte改成int或者long类型即可,测试时也没问题
3.JDK中的位图(BitSet)
在JDK中自带位图,BitSet
在jdk1.8中使用的是long类型的数组
4.位图的优点:
4.1节省空间资源,将原来多个字节表示的值转换为一个字节表示一个数,大大节省了空间资源
4.2.可以快速进行排序,使用计数排序的思想,还可以进行去重
public static void main(String[] args) { //计数排序思想 // 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 // 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 BItSet bItSet = new BItSet(10); bItSet.setBitSet(10); bItSet.setBitSet(9); bItSet.setBitSet(8); bItSet.setBitSet(7); bItSet.setBitSet(6); bItSet.setBitSet(9); for (int i = 0; i < BitSet.length; i++) { for (int j = 0; j < 8; j++) { System.out.println(BitSet[i]*8+j); } } }
4.3.还可以求两个集合的交集,并集等:建立两个位图,进行两个位图之间的& | ^ 操作
5.位图的缺点:不能查看某元素存在多少次,并且只能存储数字,如果想要存储其他信息需要转化成数字
5.1可以引入一个计数器,此时两个bit为一个单位,第一个bit存储元素,第二个bit存储计数器,但是这种方式
5.2位图存储元素计数器第二种解决方法
布隆过滤器
1.布隆过滤器概念
什么是布隆过滤器,我们在日常生活中常常需要判断一个元素是否在集合中,如果我们使用哈希表当然可以在O(1)的时间复杂度查找出该元素是否在该集合中,但是缺点是消耗存储空间,如果是海量数据,此时哈希表性能就会下降,我们引出布隆过滤器
布隆过滤器概念:如果想要判断一个元素是不是在一个集合里,一般想到的是将所有元素保存起来,然后通过比较确定。链表,树等等数据结构都是这种思路. 但是随着集合中元素的增加,我们需要的存储空间越来越大,检索速度也越来越慢(O(n),O(logn))。不过世界上还有一种叫作散列表(又叫哈希表,Hash table)的数据结构。它可以通过一个Hash函数将一个元素映射成一个位阵列(Bit array)中的一个点。这样一来,我们只要看看这个点是不是1就可以知道集合中有没有它了。这就是布隆过滤器的基本思想。
通俗点解释就是一个元素在布隆过滤器中的状态只能是可能存在或者一定不存在,那么如何实现这种状态,我们看到位图就会联想到位图中存在的元素一定存在,或者一定不存在
我们引入多个哈希函数来确定该元素在位图中的位置,假设我们引入三个哈希函数,第一个哈希函数称为A,后两个分别被称为BC,一个字符串str经过三个函数函数分别变为f(str),g(str),h(str),三个映射后的字符串分别记录在位图中,此时位图中寻找该元素,只用通过O(3)的时间复杂度就可以确定该元素是否可能存在,
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;
}
//在创建实例时,要把容量值调成大于要传的字符串的长度
//生成一个简单的哈希函数,通过所给的参数不同从而达到生成不同的哈希函数
2.生成的哈希函数
class simpleHash {
private int seed;
private int cap;
public simpleHash(int cap, int seed) {
this.cap = cap;
this.seed = seed;
}
//根据seed不同,创建出不同的哈希函数
public int Hashfunc(String val) {
return (val == null) ? 0 : (seed * (cap - 1) & (val.hashCode() ^ (val.hashCode() >>> 16)));
}
}
//生成的关于cap,seed参数的哈希函数
3.布隆过滤器框架
class bloom {
public static final int DEFAULT_CAPACITY = 1 << 20;
private int[] seeds = new int[]{1, 2, 675, 13, 46, 1, 432};
public BitSet bitset;
private int usedsize;
private static simpleHash[] simpleHashes;
public bloom() {//初始化布隆过滤器,此时需要给种子不同的参数,所以在创建种子时,需要循环时生成seeds.length个哈希函数
}
public boolean contains(String val) {//该方法判断布隆过滤器可能存在某字符串
}
public boolean add(String val) {//该方法向布隆过滤器加入对应映射的值
}
}
4.布隆过滤器初始化
public bloom() {
simpleHashes = new simpleHash[seeds.length];
bitset = new BitSet(DEFAULT_CAPACITY);
for (int i = 0; i < simpleHashes.length; i++) {
simpleHashes[i] = new simpleHash(DEFAULT_CAPACITY, seeds[i]);
}
}
5.布隆过滤器加入映射方法
public boolean add(String val) {
for (simpleHash simpleHash : simpleHashes) {
int index = simpleHash.Hashfunc(val);
bitset.set(index);
}
return true;
}
6.布隆过滤器判读是否包含某值的映射
public boolean add(String val) {
for (simpleHash simpleHash : simpleHashes) {
int index = simpleHash.Hashfunc(val);
bitset.set(index);
}
return true;
}
7.测试
public static void main(String[] args) {
bloom bloom = new bloom();
bloom.add("hello");
bloom.add("helo");
bloom.add("hlo");
bloom.add("ho");
System.out.println(bloom.contains("hello"));//此时应该返回true
System.out.println(bloom.contains("helo"));//true
System.out.println(bloom.contains("o"));//false
System.out.println(bloom.contains("ho"));//true
}
此时证明布隆过滤器的add和contains方法是正确无误的,虽然有误判,但这里体现不出来
8.布隆过滤器实现:guava实现布隆过滤器
在pom.xml文件插入以下代码
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>19.0</version>
</dependency>
就可以使用Google里面的布隆过滤器
布隆过滤器扩展阅读: 布隆过滤器(java) - 小子,你摊上事了 - 博客园 (cnblogs.com)
9.布隆过滤器优点:
1.时间复杂度与哈希函数的个数有关,如果哈希函数为k,那么查找或者增加的时间复杂度是O(K)
2.哈希函数之间没有关系,方便硬件并行计算
3.布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
4.在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
5.数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
6.使用同一组散列函数的布隆过滤器可以进行交、并、差运算
10.布隆过滤器缺点:
1. 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白 名单,存储可能会误判的数据)
2.不能获取元素本身
3.一般情况下不能从布隆过滤器中删除元素
4.如果采用计数方式删除,可能会存在计数回绕问题,也就是溢出问题
11.布隆过滤器使用场景
1. google的guava包中有对Bloom Filter的实现
2.网页爬虫对URL的去重,避免爬去相同的URL地址。
3.垃圾邮件过滤,从数十亿个垃圾邮件列表中判断某邮箱是否是垃圾邮箱。
海量数据面试题
3.1 哈希切割
给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址?
与上题条件相同, 如何找到top K的IP?
解答:
1.把100G文件通过哈希函数分到200个文件中,这样相同的哈希函数就到同一个文件中,这时找到200个文件中最大的200个出现次数最多的IP地址,此时进行比较,就能找到出现次数最多的IP地址
2.找到top K的IP,找到每个文件的Top KIP地址,在把200个文件综合起来比较,此时就能找到TopK的IP地址
3.2 位图应用
1. 给定100亿个整数,设计算法找到只出现一次的整数?
解答:如果忽略数据量大小,可以使用哈希表,此时直接遍历找到出现一次的整数
考虑数据量大小
使用两个位图,如果该数未出现,两个位图置为0;若出现一次,第一个位图所对的位置置为0,第二个位图该数所在的位置置为1,即二进制中的01;若出现三次及三次以上,直接两个位图所在的位置均置为1
使用一个位图,则两个bit为一个位置,延续两个位图的做法
2. 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
解答:第一个文件称为A,第二个文件称为B
则两个文件分别创建两个位图,两个位图进行&&操作,此时就可以找到两个文件的交集
一个位图也可以当两个位图使用,一个字节当作一个单位,即8个bit当作一个位图的一部分,下一个字节当作第二个位图的第一部分
3. 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数
解答:100亿个int大约40G内存,我们分成80个文件,按照哈希函数的方式进行映射到80个文件,此时在使用哈希表,可以直接找到不超过2次的所有整数;
第二种方式就是使用哈希切割之后,使用两个位图,若出现第一次则置为0 1,出现两次记为10,按照之前的方式进行计数,此时就可以看到不超过两次的整数有多少个
3.3 布隆过滤器
1. 给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和 近似算法 2. 如何扩展BloomFilter使得它支持删除元素的操作
解答:近似算法当然是使用布隆过滤器,将url或者sql语句放进去,此时两个布隆过滤器进行&&操作,此时就可以看到两个文件的交集
精确算法使用位图,两个文件使用哈希切割分别放入两个位图,此时两个位图进行交集操作,如果求差集,可以使用^操作
拓展阅读
一致性哈希指的是当服务器增加或减少时,以前所有的元素都要重新进行计算
例如以前有3台服务器,以前的哈希函数是%3,现在有2台服务器,现在是%2,但是以前的元素也要重新分配,服务器有可能会直接崩掉,所以这种情况怎么处理
这种情况要使用一致性哈希算法,引入一个哈希环,例如要存储图片,将该图片的编码通过哈希函数加工,hashCode(picture) % (2^32),此时可以在哈希环中均匀存储图片,即下图所示,三角形代表存储的图片,按照顺时针方向进行存储,上面四张图片存储到B服务器,右下角两张图片存储到C服务器,左下角三张图片存储到A服务器,这样就解决了服务器崩溃导致雪崩现象,如果图片经过哈希函数存储到一块,仍然有可能会导致雪崩现象
此时我们引入虚拟节点来解决这种情况,即下下图所示,B服务器有可能会导致雪崩现象,但是这回的存储情况是上面的若干个图片还存储到虚拟节点ABC三台服务器中,这样就避免了B服务器的数据倾斜问题
哈希与加密:详情请阅读此文!