教你用BitMap排序、查找和存储大量数据

Bit-map的基本思想就是用一个bit位来标记某个元素对应的Value,而Key即是该元素。由于采用了Bit为单位来存储数据,因此在存储空间方面,可以大大节省。

Bit-map概述

假设现在有这样一个需求:在20亿个随机整数中找出某个数是否存在其中,并假设32位操作系统,4G内存,你会怎么做?我们知道在java中,一个int占4字节,1字节=8位(1 byte = 8 bit),如果每个数字用int存储,那就是20亿个int,因而占用的空间约为:

2000000000*4/1024/1024/10247.45 G

显然消耗的内存空间太多了。如果按位存储就不一样了,20亿个数就是20亿位,占用空间约为:

2000000000/8/1024/1024/10240.23 G

那么使用Bit-map到底是怎么来存储的以及它的过程是什么样子的呢?接下里带大家一起研究一下。
在最开始的时候我们说过,Bit-map每一位表示一个数,0表示不存在,1表示存在。例如我们要存储数字{1,3,4,6},则可以使用Bit-map存储结构如下:
在这里插入图片描述
计算机内存分配的最小单位是字节,也就是8位,那如果要表示{12,13,15}怎么办呢?当然是在另一个8位上表示了,如下图所示:
在这里插入图片描述
大家有没有感觉这样得结构有点眼熟?不错,好像变成一个二维数组。我们知道1个int占32位,如果我们要存储得数字得最大数字是N,则我们只需要申请一个int数组长度为 int b[1+N/32] 即可存储,其中:

b[0]:可以表示0~31;
b[1]:可以表示32~63;
b[2]:可以表示64~95;
...

这样,给定任意整数N,那么N/32就得到下标,N%32就知道它在此下标的哪个位置。

Bit-map操作原理

以上简单概括性的对Bit-map的原理进行了介绍,接下来对于常用的操作我们具体看一下是怎么对数据进行处理的。

添加数据

如果现在我们想把5这个数字放进去,怎么做呢?上面提到了,给定任意整数N,那么N/32就得到下标,N%32就知道它在此下标b的哪个位置。首先,5/32=0,也是说它应该在b[0],5%32=5,说明它应该在b[0]的第5个位置,那我们把1向左移动5位,然后与原数据按位或:
在这里插入图片描述
换成二进制就是:01001110|0010000=01101110,这就相当于 78 | 32 = 110,也就是说,要想插入一个数,将1左移代表该数字的那一位,然后与原数进行按位或操作,即:

b[0] = b[0] | (1<<5)

因此,公式可以概括为:p + (i/8)|(1<<(i%8)) 其中,p表示现在的值,i表示待插入的数。

清除数据

假设我们要移除某个数字N,该怎么做呢?只需将该数所在的位置为0即可,那么怎么可以做到呢?为了便于理解,我们还是将结构看成是一个二位数组。则步骤可以归纳如下:

  1. N/32得到下标i,N%32得到此下标的位置index;
  2. 将1左移index位,就到达index这个数字所代表的位;
  3. 按位取反,最后与原数按位与,将改位置置0。

假设我们要6移除,我们用图示来表示以一下执行过程:
在这里插入图片描述

查找数据

前面我们也说了,每一位代表一个数字,1表示有(或者说存在),0表示无(或者说不存在)。通过把该为置为1或者0来达到添加和清除的效果,那么判断一个数存不存在就是判断该数所在的位是0还是1。所以查找相对来说比较简单。假设,我们想知道6在不在,那么只需判断 b[0] & (1<<6) 如果这个值是0,则不存在,如果是1,就表示存在。

Bitmap用途

快速排序

假设我们要对0-7内的5个元素(4,7,2,5,3)排序(这里假设这些元素没有重复),我们就可以采用Bit-map的方法来达到排序的目的,要表示8个数,我们就只需要8个Bit(1Bytes):

  1. 首先我们开辟1Byte的空间,将这些空间的所有Bit位都置为0,然后将对应位置为1。
  2. 遍历一遍Bit区域,将该位是1的位的编号输出(2,3,4,5,7),这样就达到了排序的目的,时间复杂度O(n)。

优点:

  • 运算效率高,不需要进行比较和移位;
  • 占用内存少,比如N=10000000,只需占用内存为N/8=1250000Byte=1.25M。

缺点:

  • 所有的数据不能重复,即不可对重复的数据进行排序和查找;
  • 只有当数据比较密集时才有优势

快速去重

假设让你从20亿个整数中找出不重复的整数的个数,提前是内存不足以容纳这20亿个整数,那么你会用什么办法?使用Bit-map就可以很好的解决。关键的问题就是怎么设计Bit-map来表示这20亿个数字的状态了。

一个数字的状态只有三种,分别为不存在,只有一个,有重复。因此,我们只需要2bits就可以对一个数字的状态进行存储了,假设我们设定一个数字不存在为00,存在一次01,存在两次及其以上为11,那我们大概需要存储空间2G左右。所以可以这样进行操作:

  1. 把这20亿个数字放进去(存储),如果对应的状态位为00,则将其变为01,表示存在一次;
  2. 如果对应的状态位为01,则将其变为11,表示已经有一个了,即出现多次;
  3. 如果为11,则对应的状态位保持不变,仍表示出现多次。
  4. 最后,统计状态位为01的个数,就得到了不重复的数字个数,时间复杂度为O(n)。

快速查找

int数组中的一个元素是4字节占32位,那么除以32就知道元素的下标,对32求余数(%32)就知道它在哪一位,如果该位是1,则表示存在。

Bloom Filters

Bloom Filters概述

Bloom filter 是一个数据结构,它可以用来判断某个元素是否在集合内,具有运行快速,内存占用小的特点。这个存储的原理与Bit-map类似,所以我们在这里一并介绍一下。
Bloom Filter 是一个基于概率的数据结构:它只能告诉我们一个元素绝对不在集合内或可能在集合内。如果想判断一个元素是不是在一个集合里,一般想到的是将集合中所有元素保存起来,然后通过比较确定。链表、树、散列表(哈希表)等等数据结构都是这种思路,但是随着集合中元素的增加,需要的存储空间越来越大;同时检索速度也越来越慢,检索时间复杂度分别是O(n)、O(log n)、O(1)。
布隆过滤器的原理是:

  • 当一个元素被加入集合时,通过 K 个散列函数将这个元素映射成一个位数组(Bit array)中的 K 个点,把它们置为 1 。
  • 检索时,只要看看这些点是不是都是1就知道元素是否在集合中;
  • 如果这些点有任何一个 0,则被检元素一定不在;
  • 如果都是1,则被检元素很可能在(之所以说“可能”是误差的存在),也可能不在。

Bloom Filters流程

  1. 首先需要 k 个 hash 函数,每个函数可以把 key 散列成为 1 个整数;
  2. 初始化时,需要一个长度为 n 比特的数组,每个比特位初始化为 0;
  3. 某个 key 加入集合时,用 k 个 hash 函数计算出 k 个散列值,并把数组中对应的比特位置为 1;
  4. 判断某个 key 是否在集合时,用 k 个 hash 函数计算出 k 个散列值,并查询数组中对应的比特位,如果所有的比特位都是1,认为在集合中;
  5. 如果这些点有任何一个 0,则被检元素一定不在。

在这里插入图片描述
在实际的项目应用中,布隆过滤器经常会被用在一些大规模去重,但又允许有小概率误差的场景中,例如说我们对一组爬虫网页地址的去重操作,或者统计某些大型网站每天的用户访问数量(需要对相同用户的多次访问进行去重),还可以解决缓存穿透问题等。

实际上,关于bitmap和布隆过滤器这类工具在大型互联网企业上已经受到了广泛使用,例如说java里面提供了BitSet类,Redis也提供了相应的位图类,Google里面的guava工具包中的BloomFilter也已经实现类布隆过滤器,所以在实际应用的时候只需要直接使用这些现有的组件即可,避免重复造轮子的情况发生。

  • 9
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Java可以使用Jedis库来操作Redis进行Bitmap数据存储和操作。以下是一个示例代码: ```java import redis.clients.jedis.Jedis; public class RedisBitmapExample { public static void main(String[] args) { // 创建Jedis对象,连接Redis Jedis jedis = new Jedis("localhost"); // 设置位图的某一位为1 jedis.setbit("mybitmap", 0, "1"); // 获取位图的某一位的值 boolean bitValue = jedis.getbit("mybitmap", 0); System.out.println("Bit value at index 0: " + bitValue); // 统计位图中值为1的位数 long bitCount = jedis.bitcount("mybitmap"); System.out.println("Number of bits set to 1: " + bitCount); // 使用位图进行位运算 jedis.setbit("bitmap1", 0, "1"); jedis.setbit("bitmap2", 1, "1"); jedis.bitop(BitOP.AND, "bitmapResult", "bitmap1", "bitmap2"); // 获取结果位图的某一位的值 boolean resultBitValue = jedis.getbit("bitmapResult", 0); System.out.println("Result bit value at index 0: " + resultBitValue); } } ``` 在上述示例中,我们使用Jedis库连接到本地Redis服务。然后,我们使用`setbit`方法设置位图的某一位为1,并使用`getbit`方法获取该位的值。我们还使用`bitcount`方法统计位图中值为1的位数。 另外,我们还展示了如何使用位图进行位运算。通过`bitop`方法,我们将两个位图进行逻辑与运算,并使用`getbit`方法获取结果位图的某一位的值。 请注意,以上示例仅为演示目的,并没有考虑异常处理和连接池管理等最佳实践。在实际使用中,请根据需求进行适当的优化和改进。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值