场景复现
今天看见室友小叶一个人在寝室emo,小张一问,原来是面试碰壁了,小叶给我来了个情景复现。
面试官:假设我现在有一个需求10亿个类型为int的整数,我要怎么查找一个元素是否存在呢。
小张:这个简单,我们可以用一个HashSet来存下所有的数,然后通过调用contains方法来判断是否存在。
面试官:嗯嗯HashSet是可以解决这个问题,但是用HashSet来存储这10亿个数,那么内存占用是多少呢?
小张:在Java中一个int类型大小为4字节,不考虑Hashset内部数据结构所占用内存,10亿个int至少占用3.72GB内存。
面试官:那么大的内存可能一下就把我们服务给撑爆了,你有没有更好的办法来完成这个需求呢?
小张:emm...暂时想不到了。
面试官:好的,今天的面试就到这里了。
问题分析
这里的问题就在于如何通过少量内存,存下这10亿数据。我们是否有更好的方式来存下这些数,我们都知道一个int类型的数据总共32位,我们是否可以用其中的一位来表示数据呢?这里我们假设使用byte类型,总共8位,其中的一位用来表示某数,我们可以规定第0位表示0,第1位表示1,以此类推。我们知道二进制只有0和1两种值,那么是否存在可以使用0或1来判断。
如上图,下标从0开始,下标为2和5的值为1,代表整数2和5存在。现在我们有了这个可以表示0-7的byte结构了,但是我们可是有10亿个整数啊,这里远远不够存下。聪明的小伙伴已经想到了,我们可以用数组来解决这个问题。比如整数8,我们就可以创建大小为2的数组,将第二个byte中的第0位设置为1。
使用这种数据结构来存下这些数据,相比使用int来存下这些数据,内存减少了32倍,10亿个数据也就仅仅占用到120MB,这种结构就叫做位图,下面我们就使用代码的方式来实现这种结构。
位图实现
构建byte数组
public class BitMap {
private byte[] origin;
public BitMap(int size) {
if (size < 0) {
throw new IllegalArgumentException();
}
// size右移三位代表 size/8,一个byte可以标识8位整数这样子可以算出总共需要多少个byte数组
origin = new byte[size >> 3 + 1];
}
}
计算待加入的数字在byte数组中的位置
public int getIndex(int data) {
return data >> 3;
}
计算加入数字在byte[index]中的位置
private int getPos(int data) {
// 与操作,相当于data%8
return data & 7;
}
添加
按照上面的设计思路,需要将对应的位置设置为1代表该数存在,我们都知道位运算中有或运算,我们是不是可以将1进行右移运算然后进行或运算呢?
如上图,我们需要添加数字2,仅需要将1进行左移2位之后进行或运算,那么字节内容中第三位就变成1,代表2存在,下面展示代码的处理方式。
public void add(int data) {
// 获取数据在字节数组的下标
int index = getIndex(data);
// 将存在的位置设置为1
origin[index] = (byte) (origin[index] | 1 << getPos(data));
}
查找
按照上面的思路,判断某元素是否存在,我们仅需要知道在字节中该位是否为1即可。我们可以将1右移和字节内容进行与操作判断结果是否大于0。
如果元素不存在,则与操作得出来的结果肯定为0,具体看下图。
按照上面的思路代码实现为:
public boolean contains(int data) {
int index = getIndex(data);
int res = origin[index] & 1 << getPos(data);
return res != 0;
}
删除
按照位图设计某位为0代表不存在,我们只要将某位设置为0就是进行了删除操作。
按上图所示,我们需要删除元素2,也就是将位置3置为0,首先我们先将1右移两位,再将结果取反,得到的结果就是第三位为0,其余都为1的数据,将原数据与上一步相与就将位置3置为了0,下面附上代码实现 。
public void del(int data) {
int index = getIndex(data);
origin[index] = (byte) (origin[index] & ~(1<<getPos(data)));
}
扩展内容
上面我们实现了位图的基本操作,位图在实际使用场景上不仅包括大数据的查找、去重,还包括一些扩展应用。例如爬虫应用,一天需要爬取成千上万的网站,那么它是如何去判断该网站是否爬取过呢。目前大多数采用的Bloom filter(布隆过滤器),它的特点就是可以用来判断某个元素是否在集合内并且具有运行快速,内存占用小的特点。
它的底层也是采用了位图,当某元素加入时,它通过n个Hash函数计算出元素的下标,将下标置为1。
判断元素是否存在时,同样去执行n个Hash函数判断下标位置是否为1。不难看出布隆过滤器是一种基于概率的数据结构,因为Hash函数肯定会发生碰撞,不过在于一些场景比如缓存、爬虫都非常适合这个场景,只要牺牲一点正确率就可以得到一个高效的数据结构。
好了今天的内容就到此为止了,具体的布隆过滤器可以去使用guava中提供的类。