给定40亿个不重复的正整数,如何快速判断一个数组是否在这40亿个数中。解决方案:
以上方法使能解决,但如果考虑到空间复杂度40亿个正整数需要将近15G的内存,如果内存不存就无法做到。那么此时就可以引入位图。
位图是一种数据结构,它通过每一个比特位来存放某一种状态,适合存储海量的数据,整数,数据无重复的场景,一般是用来判断某一个数据是否存在的。
比如上面那个例子,一个整数就是4个字节, 4 ∗ 4000000000 / 1024 / 1024 / 1024 4*4000000000/1024/1024/1024 4∗4000000000/1024/1024/1024,大约是占用了15G的内存,而如果使用位图存储这40亿个正整数, 4000000000 / 32 ∗ 4 / 1024 / 1024 4000000000/32*4/1024/1024 4000000000/32∗4/1024/1024,大约就是480M的内存。这个内存占用直接降低了一个数量级。
一个数据是否存也就两种状态,在或者不在,通过二进制的0和1就可以表示,位图存储数据就是通过比特位来判断数据是否在位图中存在,0表示不存在1表示存在。通过公式计算出一个数据在哪个比特位。下图分别表示3个数组下标,每个下标都是1个字节(Byte)。
数值/8
数值%8
3/8
得到数组下标为0
,3%8
得到比特位为3,所以元素3要存放到下标为0位置的第3个比特位处。位图实现主要的3个功能:
设置元素到位图中,通过按位与运算给对应比特位设置为1,假设存14到位图中
查询一个元素是否在位图中,以同样的方法进行按位与运算判断对应位置是否为1,如图下标为1的位置的第6个比特位是否为1
把一个元素从位图中移除,通过按位取反再进行按位与运算来移除对应标注的二进制位,假设要移除的是19
import java.util.Arrays;
public class MyBitSet {
private byte[] elem;
public MyBitSet() {
this.elem = new byte[8];
}
public MyBitSet(int nBits) {
this.elem = new byte[nBits];
}
/**
* 添加 bitIndex元素到位图中
* @param bitIndex
*/
public void set(int bitIndex) {
if (bitIndex < 0) {
throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);
}
// 计算数组下标
int index = bitIndex/8;
if (index >= elem.length) {
// 扩容
elem = Arrays.copyOf(elem,index+1);
}
// 计算二进制位
int bit = bitIndex%8;
elem[index] |= (1<<bit);
}
/**
* 判断 bitIndex元素在位图中是否存在
* @param bitIndex
* @return
*/
public boolean get(int bitIndex) {
if (bitIndex < 0) {
throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);
}
// 计算数组下标
int index = bitIndex/8;
if (index >= elem.length) {
return false;
}
// 计算二进制位
int bit = bitIndex%8;
if ((elem[index]&(1<<bit)) != 0) {
return true;
}
return false;
}
public void remove(int bitIndex) {
if (bitIndex < 0) {
throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);
}
// 计算数组下标
int index = bitIndex/8;
if (index >= elem.length) {
return;
}
// 计算二进制位
int bit = bitIndex%8;
elem[index] &= (~(1<<bit));
}
}
可以利用位图来进行简单的排序,也就是从第一个比特位开始判断是否有元素,有元素以存入的方式进行取出。
public void sort(int[] elem) {
for (int i = 0; i < elem.length; i++) {
for (int j = 0; j < 8; j++) {
if (((elem[i]>>>j)&1) == 1) {
System.out.print(j+(i*8)+" ");
}
}
}
}
在编程时,我们经常要判断一个元素是否在一个集合中出现,第一时间想到的应该是哈希表,它的优点就是增删改查快速又准确,缺点也是有的那就是存在着空间的浪费,当数据量非常大时就凸显出来了,数据量非常大时哈希表会存在非常多的key,同时发送大量的哈希冲突,使用链地址方法来解决,从而产生大量的节点占用大量的空间,如果使用线性探测就会降低查询效率。假设要判断一个email是否在几十亿的email中出现。
如果用哈希表存储一亿个 email 地址, 就需要 1.6GB 的内存(用哈希表实现的具体办法是将每一个
email 地址对应成一个八字节的信息指纹,然后将这些信息指纹存入哈希表,由于哈希表的存储效率一般只有
50%,因此一个 email 地址需要占用十六个字节。一亿个地址大约要 1.6GB, 即十六亿字节的内存)。因此
存贮几十亿个邮件地址可能需要上百 GB 的内存。
方法:
布隆过滤器是一种比较巧妙的概率形数据结果,其特点是高效地插入和查询,可以用来告诉你“某样东西一定不存在或者可能存在”,布隆过滤器使用多个哈希函数,将一个数据映射到位图结构中。从而提升查询效率,也可以节省大量的内存空间。
假设布隆过滤器中有3个hash函数,要往布隆过滤器中插入"hello world",通过3个不同的hash函数得到不同的hash值,然后再把hash值设置在位图的3个比特位中。
布隆过滤器的基本思想是将一个元素通过多个hash函数得到hash值后,设置到位图的不同比特位中。查询通过同样的hash函数去判断位图对应的比特位是否为1,只要一个比特位不唯一,那么这个元素就一定不存在,相反查询的比特位全部唯一也只是可能存在。因为通过hash函数必定会发生hash冲突,可能会出现下图这种情况:
"hello java"和"hello world"两个元素已经存放到布隆过滤器中,此时"test code"未存放到布隆过滤器中,此时去查询"test code"中查询的时候恰巧hash到的位图中的位置,都被"hello"和“hello world“标记过了,此时就返回"test code"这个元素存在吗?显然是不存在的。所以布隆过滤器如果说某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可能存在,因为有些哈希函数存在一定的误判
通过参考HashMap的哈希函数再随缘加上一些随机因子,使用了3个哈希函数对元素进行哈希并设置到位图当中。
/**
* 布隆过滤器模拟实现
* @param
*/
public class BloomFilter<E> {
static class RandomHash {
// 容量
int size;
// 随机种子
int seed;
public RandomHash(int size, int seed) {
this.size = size;
this.seed = seed;
}
/**
* 计算机哈希值
* @param key 元素
* @return
*/
private int hash(Object key) {
int h;
// (n-1)&hash == hash%n
return (key == null) ? 0 : (seed*(size-1))&(h = key.hashCode()) ^ (h >>> 16);
}
}
// 位图默认容量,参照hashmap,方便hash函数计算
private static final int DEFAULT_SIZE = 1 << 21;
// 随机因子
private static final int[] RANDOM_FACTOR = {3,7,11};
// 布隆过滤器元素个数
private int size;
private BitSet bitSet;
// 哈希函数
private RandomHash[] randomHashes;
public BloomFilter() {
bitSet = new BitSet(DEFAULT_SIZE);
randomHashes = new RandomHash[RANDOM_FACTOR.length];
for (int i = 0; i < randomHashes.length; i++) {
randomHashes[i] = new RandomHash(DEFAULT_SIZE,RANDOM_FACTOR[i]);
}
}
/**
* 添加元素到布隆过滤器中
* @param key
*/
public void add(E key) {
for (int i = 0; i < randomHashes.length; i++) {
int hash = randomHashes[i].hash(key);
bitSet.set(hash);
}
}
/**
* 判断元素是否可能存在布隆过滤中
* @param key
* @return
*/
public boolean contains(E key) {
for (int i = 0; i < randomHashes.length; i++) {
int hash = randomHashes[i].hash(key);
if (!bitSet.get(hash)) {
return false;
}
}
return true;
}
public static void main(String[] args) {
BloomFilter<Integer> bloomFilter = new BloomFilter<>();
for (int i = 1000; i < 2000; i++) {
bloomFilter.add(i);
}
int count = 0;
for (int i = 1000; i <= 70000000; i++) {
if (bloomFilter.contains(i)) {
count++;
}
}
System.out.println(count);
}
}
布隆过滤器是无法直接删除的,和查询类似会存在误删除。因为存储元素必定会存在哈希冲突的,如果删除一个元素则会把对应的比特位标记为0,一旦有其它元素也哈希到了这个位置,那么也会就会影响到其它元素,因为布隆过滤器查询的时候只要有一个比特位为0,就认为这个元素一定不存在。所以布隆过滤器不能直接删除。
如果想实现布隆过滤器的删除:可以采用加减标记的方式,一起添加元素是将hash过后的比特位标记成1,可以改成给位图对应的位置+1,删除就给对应的位图下标-1,但这样做需要更大的空间,而且+1和-1存溢出问题。
优点:
增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关
哈希函数相互之间没有关系,方便硬件并行运算
布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
使用同一组散列函数的布隆过滤器可以进行交、并、差运算
缺点:
总的来说,布隆过滤器适用于需要快速判断某个元素是否存在的场景,并且能够容忍一定的错误率
问题1
给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址? 与上题条件相同,如何找到top K的IP?
该问题可以使用哈希切割,新建200个文件,每个文件存储500M的数据,从文件中开始读取100G的数据,对读取到的ip地址进行哈希后,再对hash值%200存放到对应的文件中。最后依次读取200个文件,记录ip出现的次数即可。同理,如果是TOP k问题,也是如此。
3.2 问题2
给定100亿个整数,设计算法找到只出现一次的整数? 100亿个整数大概是 10000000000 ∗ 4 / 1024 / 1024 / 1024 10000000000*4/1024/1024/1024 10000000000∗4/1024/1024/1024,40G的大小,显然内存是存不下的。
解法1:
同理可以使用Hash切割,将这100亿个数hash到不同文件上,再遍历文件即可。
解法2:
100亿个整数存放到一个位图大概是 10000000000 / 32 ∗ 4 / 1024 / 1024 / 1024 10000000000/32*4/1024/1024/1024 10000000000/32∗4/1024/1024/1024,大概就是1个G的内存。
使用两个位图,可以表示数组出现了多少次,1次、2次或者3次。
比如说:
解法3:使用一个位图
和解法2类似,采用类似二进制的表示出现的次数,只不过使用一个位图要两个比特位才能表示一个数字。
原本的公式是:下标: i n d e x = k e y / 8 index = key/8 index=key/8,比特位: b i t = k e y bit = key bit=key% 8 8 8
采用两个比特位标识一个数字:下标 i n d e x = k e y / 4 index = key/4 index=key/4,比特位: b i t = k e y bit = key bit=key% 4 ∗ 2 4*2 4∗2
问题3
给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
解法1:
使用hash切割,把两个文件先分别分割成200个文件,也就是400个文件,读取文件1分割的问题件的同时去读取文件2对应分割的文件,由于hash函数是一致的,相同的数字会被放到同一个文件中,比如说文件1分割出来的文件1.1和文件2分割出来的2.1,使用的是同一个哈希函数,只要在两个文件里找相同的数字即可。
解法2:
使用位图把第一个文件中的数据存到位图中,再遍历第二个文件中的数字看看是否在位图中存在,存在就是交集。
问题4
1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数
解法1:
hash切割,切割到多个文件,一个一个文件统计
解法2:
使用两个位图,只要两个位图没有同时为1,那就是没有超过两次
问题5
给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和
近似算法
精确算法:两个文件分别哈希切割成多个文件,分两组。在依次对应读取两个文件判断是否有相同的存在。
近似算法:把一个文件的数据读取到布隆过滤器,再读取第二个问题,判断是否存在,显然会存在误判。