海量数据下的去重和查重(一):BitMap位图法
2020-01-06 本文已影响0人
雪飘千里
在一些海量数据的场景中,做一些查重、去重、排序,一般的方法难以实现,因为内存占用太大了,比如以下问题:
问题一:10亿个正整数,给定一个数值,如何快速判定该数值是否在10亿个正整数当中?假如机器只有1G内存?
问题二:比如说是一组数字,它是保存在一个很大的文件中。它总体的个数为400亿个,里面有大量重复的数据,如果去除重复的元素之后,大概的数据有40个亿,那么,假定我们有一台内存为2GB的机器。我们该如何来消除其中重复的元素呢?再进一步考虑,如果我们消除了重复的元素之后,怎么统计里面元素的个数并将去重后的元素保存到另外的一个结果文件里呢?
我们来估计一下上面所需要用到的内存,1G 约等于 10^9(10的9次方)个字节,上面的数字都是int型,一个int 占用4字节,所以问题一可能需要占用4G内存,而问题二中,需要占用16G内存,显然需要的内存太大了,传统的方法是不能用了,这时候就需要引出今天的主角了——BitMap位图法。
1、思想
位图法的思想类似于hash寻址,首先初始化一个int数组,每个元素对应32位比特(一个int 占用4字节,1字节8个比特位),将10亿个元素分别读入内存,对int数组中的元素比特位进行标记,如果存在,标记为1即可。标记完之后,即可快速判定某个元素是否在10亿个正整数当中,时间复杂度为O(1)。
image.png原先一个int类型只能表示一个整数,现在一个int能表示32个整数,相当于是节省了32倍的内存。
那么具体我们是怎么操作??
- 寻址
比如元素 119,如何确定其对应的数组比特位 index ?
1)确定数组 arrayIndex :119/32 = 3,也就是第 4 个数组元素,a[3] 的位置。——num >> 5(num右移5位,相当于是除以2的5次方);
2)确定比特位 bitIndex:119%32 = 23,第23个比特位。—— num & 31
- 设置比特位
- 将比特位设置为1
bigArray[arrayIndex] |= 1 << bitIndex(1左移到目标位置,然后进行或运算); - 将比特位设置为0
bigArray[arrayIndex] &= ~(1 << bitIndex);(1左移到目标位置,然后取反,然后再进行与运算);
- 将比特位设置为1
- 判断某一元素是否存在
只需将 1 左移bitIndex位数,然后与原来的值进行与运算。只要与运算结果中有1,即表示元素存在。所以可以用运行结果是不为0作为元素是否存在依据。
2、实现
2.1 自定义实现
import java.util.BitSet;
public class bitMap {
private int[] bigArray;
public bitMap(long size){
bigArray = new int[(int) (size/ 32 + 1)];
}
public void set1(int num){
//确定数组 index
int arrayIndex = num >> 5;
//确定bit index
int bitIndex = num & 31;
//设置0
bigArray[arrayIndex] |= 1 << bitIndex;
}
public void set0(int num){
//确定数组 index
int arrayIndex = num >> 5;
//确定bit index
int bitIndex = num & 31;
//设置0
bigArray[arrayIndex] &= ~(1 << bitIndex);
System.out.println(get32BitBinString(bigArray[arrayIndex]));
}
public boolean isExist(int num){
//确定数组 index
int arrayIndex = num >> 5;
//确定bit index
int bitIndex = num & 31;
//判断是否存在
return (bigArray[arrayIndex] & (1 << bitIndex))!=0 ? true : false;
}
/**
* 将整型数字转换为二进制字符串,一共32位,不舍弃前面的0
* @param number 整型数字
* @return 二进制字符串
*/
private static String get32BitBinString(int number) {
StringBuilder sBuilder = new StringBuilder();
for (int i = 0; i < 32; i++){
sBuilder.append(number & 1);
number = number >>> 1;
}
return sBuilder.reverse().toString();
}
public static void main(String[] args) {
int[] arrays = new int[]{1, 2, 35, 22, 56, 334, 245, 2234, 54};
bitMap bigMapTest = new bitMap(2234-1);
for (int i : arrays) {
bigMapTest.set1(i);
}
System.out.println(bigMapTest.isExist(36));
}
}
2.1 jdk实现
在java.util包中有个BitSet类也是用同样的思想实现的,不过BitSet的底层实现是long数组(long是64位,占用8个字节)。
public class BitSetTest {
public static void main(String[] args) {
int [] array = new int [] {1,2,3,22,0,3,63};
BitSet bitSet = new BitSet(1);
System.out.println(bitSet.size()); //64
bitSet = new BitSet(65);
System.out.println(bitSet.size()); //128
bitSet = new BitSet(23);
System.out.println(bitSet.size()); //64
//将数组内容组bitmap
for(int i=0;i<array.length;i++)
{
bitSet.set(array[i], true);
}
System.out.println(bitSet.get(22));
System.out.println(bitSet.get(60));
System.out.println("下面开始遍历BitSet:");
for ( int i = 0; i < bitSet.size(); i++ ){
System.out.println(bitSet.get(i));
}
}
}
3、优缺点
- 优点:是海量数据下去重时,用这种数据结构可以大量的节省空间;
- 缺点:
- 空间占用大小是根据最大值来的,如果数据量本身不多,但是最大值特别大,那不但不会节省空间,反而会浪费空间
- 功能有限,由于bit位只能是0和1,所以只能用于去重和查重,如果要统计每个元素出现的个数,就不能实现了。