前言
之前在讲散列表的时候,提到过位图的概念。位图(Bitmap)作为一种特殊的数据结构,它使用一系列位来表示数据,每个位只有两个状态(0或1)。由于它的高效性和节省空间的特性,位图在很多场景中都有广泛的应用。本节,我们就位图展开详细介绍。
1. 简介
位图使用位来表示数据,这使得它在存储和处理大量数据时具有高效性和节省空间的优点。例如,如果我们需要存储一亿个整数,使用普通的数组需要消耗大约381MB的内存(假设一个整数占用4字节,100000000*4/1024/1024=381MB),而使用位图只需要消耗大约11.92MB(100000000/8/1024/1024)的内存。
如下图所示,可以看到一个整型占用四个字节,一个字节八位,那么一个整型可以表示32个数字。那么理论上来说,如果用位图的话,我们的存储空间,相对于直接整型存储,可以缩小32倍,也就是上面的 381MB / 32 约等于 11.92 MB。
2. 代码实现
2.1 java工具包自带
import java.util.BitSet;
public class Client {
@Test
public void testBitMap(){
BitSet bitSet=new BitSet(100000000);
for (int i = 0; i < 1000000; i++) {
bitSet.set(i);
}
System.out.println(bitSet.get(200));
System.out.println(bitSet.get(343535353));
}
}
代码运行结果:
true
false
2.2 手写实现位图
2.2.1 基础知识
- 移位
移位运算是指将一个数字转换为2进制后向前向后进位的运算,具体计算举例如下:
1<<2 : 00000001 转换后为 00000100
8>>3 : 00001000 转换后为 00000001 - 与
与运算规则如下:1 & 0= 0 ,1&1=1 ,0&0=0
所以可得出结论:通过和1 按位与运算,可以得出对应的比特位是0还是1 - 或
或运算规则如下:0|1=1,1|1=1, 0|0=0
所以可得出结论:通过与1 按位或运算,最后得到的结果对应比特位一定是1
2.2.2 代码实现
package org.wanlong.hash;
import java.util.Arrays;
/**
* @author wanlong
* @version 1.0
* @description:
* @date 2023/6/25 14:37
*/
public class MyBitMap {
//元素存储
private byte[] elem;
//已有元素个数
private int usedSize;
public MyBitMap() {
this.elem = new byte[1];
}
/**
* @param n:
* @return
* @Description: 初始化 n代表要使用多少位,也即存储的最大范围值
* @Author: wanlong
* @Date: 2023/6/25 15:07
**/
public MyBitMap(int n) {
this.elem = new byte[n / 8 + 1];
}
/**
* @param val:
* @return void
* @Description: 设置值
* @Author: wanlong
* @Date: 2023/6/25 15:05
**/
public void set(int val) {
if (val < 0) {
throw new IndexOutOfBoundsException();
}
//整数除8 得到元素应该放到哪个下标
int arrayIndex = val / 8;
//整数除8求余,得到元素在这个下标的整型的哪个比特位
int bitIndex = val % 8;
//扩容
if (arrayIndex > elem.length - 1) {
elem = Arrays.copyOf(elem, arrayIndex + 1);
}
// 或运算 可以保证如果之前插入过这个值,不会影响这个值还是1,也不会更改别的值
// 1 向左移位 bitindex 后,则对应的bitindex为1
// 如 00000001 向左移动 3 则 为 00001000
//或运算 0|1=1 ,1|1=1 ,则,按位或运算后,固定的位置bitIndex一定是1 ,其他位置不变
elem[arrayIndex] |= (1 << bitIndex);
//元素个数加一
usedSize++;
}
/**
* @param val: 待查找值
* @return boolean
* @Description 判断值是否存在
* @Author: wanlong
* @Date: 2023/6/25 15:04
**/
public boolean get(int val) {
if (val < 0) {
throw new IndexOutOfBoundsException();
}
int arrayIndex = val / 8;
int bitIndex = val % 8;
//如果算出来查找元素下标大于数组长度,一定不在数组中,返回false
if (arrayIndex >= elem.length) {
return false;
}
// 1 向左移位 bitindex 后,则对应的bitindex为1
// 如 00000001 向左移动 3 则 为 00001000
// 与运算 0&1 =0,1&1=1 则,如果与的结果不为0,一定是对应的bitIndex=1 ,即,元素存在
if ((elem[arrayIndex] & (1 << bitIndex)) != 0) {
return true;
}
return false;
}
/**
* @param val: 待重置值
* @return void
* @Description: 将val的对应位置置为0
* @Author: wanlong
* @Date: 2023/6/25 15:06
**/
public void delete(int val) {
if (val < 0) {
throw new IndexOutOfBoundsException();
}
int arrayIndex = val / 8;
int bitIndex = val % 8;
//对应下标置为0
// 1 向左移位 bitindex 后,则对应的bitindex为1
// 如 1<<3 , 00000001 向左移动 3 则 为 00001000
// ~(1<<3), 00001000 , 11110111 对应的bitindex设为0
// 1&0=0, 0&0=0
// 所以,最终对应的下标bitIndex一定是0
elem[arrayIndex] &= ~(1 << bitIndex);
//元素个数减一
usedSize--;
}
/**
* @return int
* @Description: 当前位图记录的元素个数
* @Author: wanlong
* @Date: 2023/6/25 15:06
**/
public int getUsedSize() {
return usedSize;
}
}
2.2.3 测试验证
@Test
public void testMyBitMap() {
MyBitMap myBitMap = new MyBitMap(1000000);
for (int i = 0; i < 1000000; i++) {
myBitMap.set(i);
}
System.out.println(myBitMap.get(200));
System.out.println(myBitMap.get(343535353));
}
运行结果:
true
false
3. 优缺点
3.1 优点
- 可存储的数据变多了
- 占用空间小,索引速度快
3.2 缺点
- 可读性差
- 位图存储的元素个数虽然比一般做法多,但是存储的元素大小受限于存储空间的大小。位图存储性质:存储的元素个数等于元素的最大值。比如, 1K 字节内存,能存储 8K 个值大小上限为 8K 的元素。(元素值上限为 8K ,这个局限性很大!)比如,要存储值为 65535 的数,就必须要 65535/8=8K 字节的内存。要就导致了位图法根本不适合存 unsigned int 类型的数(大约需要 2^32/8=5 亿字节的内存)。
- 位图对有符号类型数据的存储,需要 2 位来表示一个有符号元素。这会让位图能存储的元素个数,元素值大小上限减半。 比如 8K 字节内存空间存储 short 类型数据只能存 8K*4=32K 个,元素值大小范围为 -32K~32K
4. 应用
4.1 大数据去重
当我们需要处理大量的数据,并且需要去除重复的数据时,可以使用位图。例如,我们可以使用位图来记录用户的访问记录,以去除重复的访问。
4.2 布隆过滤器
布隆过滤器是一种使用位图实现的概率型数据结构,它可以用于检测一个元素是否在一个集合中。由于布隆过滤器可能会有误判,所以它通常用于需要快速检查但可以接受一定误判率的场景,例如网页爬虫、垃圾邮件过滤等。
4.3 位图索引
在数据库中,位图索引是一种使用位图来加快数据检索速度的技术。它特别适用于处理低基数数据(即数据的唯一值数量相对较少)。
5. 经典案例
- 给40亿个不重复的unsigned int的整数,没排过序的,然后再给一个数,如何快速判断这个数是否在那40亿个数当中
首先,将这40亿个数字存储到bitmap中,然后对于给出的数,判断是否在bitmap中即可。 - 使用位图法判断整形数组是否存在重复
遍历数组,一个一个放入bitmap,并且检查其是否在bitmap中出现过,如果没出现,放入,否则即为重复的元素。 - 使用位图法进行整形数组排序
首先遍历数组,得到数组的最大最小值,然后根据这个最大最小值来缩小bitmap的范围。(这一步很关键,通过确定最大最小值,可以将位图的大小缩小,比如数字范围是3000~~10000时,可以先将数据减去3000,再放入bitmap中,次数bitmap大小会变小),这里需要注意对于int的负数,都要转化为unsigned int来处理,而且取位的时候,数字要减去最小值。 - 在2.5亿个整数中找出不重复的整数,注,内存不足以容纳这2.5亿个整数
采用2-Bitmap(每个数分配2bit,00表示不存在,01表示出现一次,10表示多次,11无意义)。其实,这里可以使用两个普通的Bitmap,即第一个Bitmap存储的是整数是否出现,如果再次出现,则在第二个Bitmap中设置即可。这样的话,就可以使用简单的1- Bitmap了。
以上,本人菜鸟一枚,如有错误,请不吝指正。