快速查找-bitmap
1 前置知识补充
1.1 bit位与二进制
- 定义:二进制是以2为基数代表系统的二进位制,这一系统中,通常用两个不同的符号0(代表零)和1(代表一)来表示所有的数值。
- 发现者:二进制的发现者是德国数学家莱布尼茨。
- 基本单位:二进制中的每一位称为一个比特(Bit,Binary digit的缩写),它是二进制系统中最小的信息单位。
1.2 二进制的运算
1.2.1 算数运算
二进制数的算术运算包括:加、减、乘、除四则运算,下面分别予以介绍。
(1)二进制数的加法
根据“逢二进一”规则,二进制数加法的法则为:
0+0=0
0+1=1+0=1
1+1=0 (进位为1)
1+1+1=1 (进位为1)
例如:1001 + 1010
1001
+ 1010
--------
10011 (二进制结果,十进制中为19)
(2)二进制数的减法
根据“借一有二”的规则,二进制数减法的法则为:
0-0=0
1-1=0
1-0=1
0-1=1 (借位为1)
例如:1101 + 1001
1101
- 1001
--------
100 (二进制结果,十进制中为4)
(3)二进制数的乘法
二进制数乘法过程可仿照十进制数乘法进行。但由于二进制数只有0或1两种可能的乘数位,导致二进制乘法更为简单。二进制数乘法的法则为:
0×0=0
0×1=1×0=0
1×1=1
例如:1001和1010相乘的过程如下:
1010
x 11
--------
1010 (1010 * 1,即本身)
+ 1010 (1010左移一位,相当于乘以2)
--------
11110 (二进制结果,十进制中为30)
(4)二进制数的除法
二进制数除法与十进制数除法很类似。可先从被除数的最高位开始,将被除数(或中间余数)与除数相比较,若被除数(或中间余数)大于除数,则用被除数(或中间余数)减去除数,商为1,并得相减之后的中间余数,否则商为0。再将被除数的下一位移下补充到中间余数的末位,重复以上过程,就可得到所要求的各位商数和最终的余数。
例如:100110÷110的过程如下:
1.2.2 逻辑运算
1. 逻辑加法(或运算 OR)
逻辑加法,也称为逻辑或运算,表示两个逻辑变量中只要有一个为1,则结果为1;只有当两者都为0时,结果才为0。这种运算通常用符号“+”、“∨”或“|”来表示。
运算规则:
A | B | A OR B |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 1 |
逻辑加法类似于电路中的并联关系,只要有一个输入为1,输出就为1。
示例说明:
A = 1010 和 B = 0110,进行逻辑加法运算
1010
| 0110
--------
1110
2. 逻辑乘法(与运算 AND)
逻辑乘法,也称为逻辑与运算,表示只有当两个逻辑变量都为1时,结果才为1;否则结果为0。这种运算通常用符号“×”、“∧”、“·”或“&”来表示。
运算规则:
A | B | A AND B |
---|---|---|
0 | 0 | 0 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
逻辑乘法类似于电路中的串联关系,只有当所有输入都为1时,输出才为1。
A = 1010 和 B = 0110,进行逻辑乘法运算
1010
^ 0110
--------
0010
3. 逻辑否定(非运算 NOT)
逻辑否定,也称为逻辑否运算,表示对单一逻辑变量进行取反操作。如果逻辑变量为1,则取反后为0;如果逻辑变量为0,则取反后为1。这种运算通常用符号“~”、“!”或者上方加一横线来表示。
运算规则:
A | NOT A |
---|---|
0 | 1 |
1 | 0 |
逻辑否定用于反转逻辑变量的值。
A = 1010 和 B = 0110,进行逻辑乘法运算
! 1010
--------
0101
4. 逻辑异或运算(XOR)
逻辑异或运算是一种特殊的逻辑运算,它表示当两个逻辑变量不相同时,结果为1;当两个逻辑变量相同时,结果为0。这种运算通常用符号“^”或“⊕”来表示。
运算规则:
A | B | A XOR B |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
A = 1010 和 B = 0110,进行逻辑乘法运算
1010
0110
--------
1100
2 课题引出
- 如何在3亿个整数(0~2亿)中判断某一个数是否存在?内存限制500M,一台机器。
- 如何在亿级数据中快速过滤黑名单?
- 网页爬虫,如何避免重复爬取相同的网页?
- 邮件系统中,在海量数据中对当前邮件进行判别,是否为垃圾邮件?
3 bitmap
3.1 bitmap是什么
- 定义:Bitmap是位(bit)的集合,是一个离散的数组结构,使用一个bit位来标记某个元素的状态,而该元素即为Key。最基本的情况下,一个bit可以表示两种状态(0-不存在,1-存在),但根据业务需求,也可以使用多个bit来表示更多的状态。
- 数据结构:Bitmap底层通常使用byte[]数组实现,一个byte包含8个bit,因此可以通过操作byte数组中的位来存储大量数据。此外,也可以使用int[]或long[]等数据结构,分别表示32bit或64bit的集合。
3.2 java中类型与bit的转换
- byte:
- 占用空间:1 byte
- 位数:8 bits
- 范围:-128 到 127(因为有符号)
- short:
- 占用空间:2 bytes
- 位数:16 bits
- 范围:-32,768 到 32,767(因为有符号)
- int:
- 占用空间:4 bytes
- 位数:32 bits
- 范围:-2,147,483,648 到 2,147,483,647(因为有符号)
- Int a = 1,这个1在计算中是怎么存储的?
- 0000 0000 0000 0000 0000 0000 0000 0001 toBinaryString (1) =1
- long:
- 占用空间:8 bytes
- 位数:64 bits
- 范围:-9,223,372,036,854,775,808 到 9,223,372,036,854,775,807(因为有符号)
- float:
- 占用空间:4 bytes
- 位数:32 bits(但是以IEEE 754标准存储,包括1位符号位、8位指数位和23位尾数位)
- 范围:大约 ±3.4028235E+38F(有效位数约7位十进制数字)
- double:
- 占用空间:8 bytes
- 位数:64 bits(但是以IEEE 754标准存储,包括1位符号位、11位指数位和52位尾数位)
- 范围:大约 ±1.79769313486231570E+308(有效位数约15位十进制数字)
- char:
- 占用空间:2 bytes
- 位数:16 bits
- 范围:U+0000 到 U+FFFF(即0到65,535的Unicode码点)
3.3 bitmap示例
public class MyBitMap {
private byte[] bits;
private int size;
public MyBitMap(int size){
this.size = size;
// 因为 byte = 8bit位 , 所以这里需要 除以8(>>3)
bits = new byte[size >> 3 + 1] ;
}
/**
* 添加元素
* 特殊说明:
* 这里的 1 << offset 用于得到二进制中第offset为的位置为1的位
* 例如:
* 1 << 6 -> 0100 0000
* @param num
*/
public void add(int num){
if (num > size) throw new RuntimeException("num > size");
// 计算 num 在 bits 中的位置
int index = num / 8;
// 计算得到 num 在 byte中的未知
int offset = num % 8;
// 计算 index 位置的 byte
bits[index] = (byte) (bits[index] | 1 << offset );
}
public boolean find(int num){
if (num > size) return false;
// 计算 num 在 bits 中的位置
int index = num / 8;
// 计算得到 num 在 byte中的未知
int offset = num % 8;
return (bits[index] & offset << 1) != 0;
}
public static void main(String[] args) {
MyBitMap bitMap = new MyBitMap(200000001); // 两亿
bitMap.add(2);
bitMap.add(3);
bitMap.add(65);
bitMap.add(66);
System.out.println(bitMap.find(3));
System.out.println(bitMap.find(64));
}
}
3.4 bitmap优缺点
优点
- 空间效率高:由于每个元素仅用一个或多个位(bit)来表示,Bitmap相比使用完整数据结构(如整数、指针等)来存储相同信息,可以极大地节省存储空间。
- 查询效率高:Bitmap的查询操作通常只涉及位运算,这些操作在大多数现代处理器上都是高效的。因此,Bitmap非常适合进行快速查找和判断元素是否存在等操作。
- 便于进行位运算:利用Bitmap可以方便地进行一些位运算相关的操作,如集合的并集、交集、差集等,这些操作在传统数据结构中可能需要更复杂的逻辑和更多的计算资源。
- 去重和排序:Bitmap天然适合用于去重操作,因为它能确保每个元素只被存储一次。同时,通过适当的编码和排序算法,Bitmap还可以用于排序操作,尤其是当数据量较大且重复率较高时。
缺点
- 数据不能重复计数:Bitmap只能表示元素的存在与否(即0和1),无法直接记录元素出现的次数。如果需要知道某个元素出现的具体次数,则需要结合其他数据结构或算法来实现。
- 数据量少时效率不高:当数据量较小时,Bitmap相对于普通的哈希表(Hash Table)等数据结构可能没有显著的优势,因为哈希表等数据结构在数据量较小时也能提供较快的查询速度,并且能够存储更多的信息(如元素值)。
- 无法直接处理字符串:Bitmap本身是基于位的,它直接处理的是整数或可以通过某种方式映射为整数的元素。对于字符串等复杂数据类型,Bitmap无法直接处理,需要通过哈希函数等方式将其映射为整数,但这样做可能会引入哈希冲突的问题。
- 动态扩展困难:Bitmap在初始化时需要指定其大小(即位数),一旦确定后就很难动态扩展。如果后续需要存储的元素数量超过了Bitmap的初始大小,则需要重新分配更大的内存空间,并重新填充Bitmap,这可能会导致性能下降。
- 哈希冲突问题:虽然这一点在Bitmap的直接实现中不常见,但在将字符串等复杂数据类型映射到Bitmap时,如果使用了哈希函数,就可能会面临哈希冲突的问题。哈希冲突可能导致不同的元素被映射到Bitmap的同一位置,从而影响查询和存储的准确性。