简单理解:Bitmap和布隆过滤器(Bloom Filter)

本文介绍了Bitmap算法及其紧凑的数据存储结构,对比哈希表,适用于海量数据去重。此外,还介绍了布隆过滤器,一种引入多个哈希函数以减少内存消耗的高效判重方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Bitmap算法

与其说是算法,不如说是一种紧凑的数据存储结构。其实如果并非如此大量的数据,有很多排重方案可以使用,典型的就是哈希表。

public int[] removeDuplicates(int[] array) {
	int index = 0;
	int[] newArray = new int[array.length];
    Map<Integer, Boolean> maps = new LinkedHashMap<Integer, Boolean>();
    for(int num : array) {
        if(!maps.contains(num)) {
	        newArray[index++] = num;
            maps.put(num, true);
        }
    }

    return newArray;
}

实际上,哈希表实际上为每一个可能出现的数字提供了一个一一映射的关系,每个元素都相当于有了自己的独享的一份空间,这个映射由散列函数来提供(这里我们先不考虑碰撞)。实际上哈希表甚至还能记录每个元素出现的次数,这样的数据结构完成这个任务有点“大材小用”了。

我们拆解一下我们的需求:

  1. 集合中每个元素(示例中是int)有一个独享的空间
  2. 找到一个到这个空间的映射方法

这个空间要多大?对于我们的问题来说,一个boolean就够了,或者说,1个bit就够了,我们只想知道某个元素出现过没有。如果为每个所有可能的值分配1个bit,32bit的int所有可能取值需要内存空间为:

232 bit=229 Byte=512MB

那怎么样完成这个映射呢?其实就是Bitmap所要完成的工作了。如果我们把整型1-8依次映射到第一个Byte上,整型9-16依次映射到第二个Byte上,每个bit就代表这个int值是否出现过,初值为0(false)。

若扩展到整个int取值域,申请一个byte[2^32^/8](一共2^32^个数,1byte占8bit)即可,示例代码如下:

public static final int _1MB = 1024 * 1024;
//每个byte记录8bit信息,也就是8个数是否存在于数组中
public static byte[] flags = new byte[ 512 * _1MB ];


public static void main(String[] args) {
	//待判重数据
    int[] array = {255, 1024, 0, 65536, 255};

    int index = 0;
    for(int num : array) {
	    if(!getFlag(num)) {
	        //未出现的元素
	        array[index] = num;
	        index = index + 1;
            //设置标志位
            setFlag(num);
            System.out.println("set " + num);
        } else {
	        System.out.println(num + " already exist");
	    }
    }
}

public static void setFlag(int num) {
	//使用每个数的低三位作为byte内的映射
    //例如: 255 = (11111111)
    //低三位(也就是num & (0x07))为(111) = 7, 则byte的第7位为1, 表示255已存在
	flags[num >> 3] |= 0x01 << (num & (0x07));
}

public static boolean getFlag(int num) {
	return (flags[num >> 3] >> (num & (0x07)) & 0x01) == 0x01;
}

其实,就是按int从小到大的顺序依次摆放到byte[]中

很显然,对于小数据量、数据取值很稀疏,上面的方法并没有什么优势,但对于海量的、取值分布很均匀的集合进行去重,Bitmap极大地压缩了所需要的内存空间。于此同时,还额外地完成了对原始数组的排序工作。缺点是,Bitmap对于每个元素只能记录1bit信息,如果还想完成额外的功能,恐怕只能靠牺牲更多的空间、时间来完成了。

布隆过滤器(Bloom Filter)

然而Bitmap不是万能的,如果数据量大到一定程度,如开头写的64bit类型的数据,还能不能用Bitmap?我们来算一算:

264bit = 261 Byte = 2048PB = 2EB

EB(Exabyte,艾字节)这个计算机科学中统计数据量的单位有多大,有兴趣的小伙伴可以查阅下资料。这个量级的Bitmap,已经不是人类硬件所能承担的了。我相信谁也不会想用集群去计算这么一个问题吧1?所以Bitmap的好处在于空间复杂度不随原始集合内元素的个数增加而增加,而它的坏处也源于这一点——空间复杂度随集合内最大元素增大而线性增大。

所以接下来,我们要引入另一个著名的工业实现——布隆过滤器(Bloom Filter)。如果说Bitmap对于每一个可能的整型值,通过直接寻址的方式进行映射,相当于使用了一个哈希函数,那布隆过滤器就是引入了k ( k > 1 )个相互独立的哈希函数,保证在给定的空间、误判率下,完成元素判重的过程。下图中是k = 3 时的布隆过滤器。

在这里插入图片描述
x,y,z经由哈希函数映射将各自在Bitmap中的3个位置置为1,当w出现时,仅当3个标志位都为1时,才表示w在集合中。图中所示的情况,布隆过滤器将判定w不在集合中。

那么布隆过滤器的误差有多少?k的值与数据量n有什么关系吗?详细数学分析请参考原文
我在这只给出结论:

哈希个数 k = n/m * ln2 ≈ 0.7n/m
误差率 ε ≈ 0.5^k^

若以m = 8n , k = 0.7 * m/n = 5.6计算,Bitmap集合的大小为242 bit = 239 Byte = 512GB,此时的ε = 0.55.6 ≈ 0.02 。并且要知道,以上计算的都是误差的上限。当输入元素个数逼近集合总元素n时,误差率便逐渐逼近这个上界。

布隆过滤器通过引入一定错误率,使得海量数据判重在可以接受的内存代价中得以实现。从上面的公式可以看出,随着集合中的元素不断输入过滤器中(n增大),误差将越来越大。但是,当Bitmap的大小m(指bit数)足够大时,比如比所有可能出现的不重复元素个数还要大10倍以上时,错误概率是可以接受的。相比于单纯的bitmap,这个算法跳出了空间复杂度对待判元素值域的依赖,转而依赖总元素个数,这是一个更加工程可实现的算法——前者不管你的数集有多大,所需要的内存空间是一定的;后者,数集越大,想要达到相同误判率,所需要的内存空间就越大。

最后我们所要做的,就是实现一个布隆过滤器,然后利用它对硬盘上的5TB数据一一判重,并写回硬盘中。

### 基于 Redis Bitmap布隆过滤器方法及原理 #### 方法概述 布隆过滤器Bloom Filter)是一种高效的空间节省型概率数据结构,主要用于快速判断某个元素是否属于一个集合。其核心是一个位数组多个哈希函数。通过将输入元素映射到位数组中的特定位置来存储信息。 Redis 提供了一种名为 **Bitmap** 的功能,可以用来模拟布隆过滤器所需的位数组[^1]。具体来说,Redis 中的 `SETBIT` `GETBIT` 命令允许操作二进制位,从而支持构建高效的布隆过滤器实现。 --- #### 实现步骤说明 以下是基于 Redis Bitmap 构建布隆过滤器的核心逻辑: 1. 初始化位数组: 使用 Redis 的键值对机制创建一个新的 Bitmap 键,初始状态为全零。 2. 定义哈希函数集: 需要定义一组独立的哈希函数 \( h_1, h_2, \ldots, h_k \),这些函数会将输入元素映射到位数组的不同索引上[^3]。 3. 插入元素: 对于每一个待插入的元素,计算该元素经过所有哈希函数后的结果,并将其对应的位设置为 1。这可以通过 Redis 的 `SETBIT key offset value` 命令完成。 4. 查询元素: 当查询某元素是否存在时,同样利用相同的哈希函数组计算目标位的位置,并检查这些位是否全部为 1。如果任意一位不为 1,则可断定该元素不存在;否则认为可能存在(存在误判的可能性)。 5. 删除元素: 布隆过滤器本身并不支持删除操作,因为单个比特可能对应多个不同元素的结果。但如果确实需要移除某些记录,可通过额外维护反向计数等方式间接解决这一问题。 --- #### 示例代码展示 下面提供一段 Python 脚本演示如何使用 redis-py 库配合 Redis 来实现上述过程: ```python import hashlib import math from redis import StrictRedis class BloomFilter: def __init__(self, capacity, error_rate=0.01, redis_key="bloomfilter", host='localhost', port=6379): self.redis_client = StrictRedis(host=host, port=port, decode_responses=True) self.capacity = capacity self.error_rate = error_rate self.bit_size = int(-capacity * math.log(error_rate) / (math.log(2)**2)) self.hash_count = int((self.bit_size/capacity) * math.log(2)) self.key = redis_key def _hashes(self, item): hashes = [] for seed in range(self.hash_count): md5_hasher = hashlib.md5(f"{seed}{item}".encode('utf-8')) hash_value = int(md5_hasher.hexdigest(), 16) % self.bit_size hashes.append(hash_value) return hashes def add(self, item): positions = self._hashes(item) pipe = self.redis_client.pipeline() for pos in positions: pipe.setbit(self.key, pos, 1) pipe.execute() def check(self, item): positions = self._hashes(item) result = all([int(self.redis_client.getbit(self.key, pos)) for pos in positions]) return result # 测试部分 bf = BloomFilter(capacity=1000, error_rate=0.01) test_items = ["apple", "banana", "cherry"] for i in test_items: bf.add(i) print(bf.check("apple")) # True print(bf.check("orange")) # False ``` 此脚本展示了如何初始化布隆过滤器实例并执行基本的操作——添加新项目以及检测指定项的存在情况。 --- #### 性能特点分析 1. **优点** - 占用内存较小,适合大规模数据场景下的成员关系测试。 - 查找速度极快,时间复杂度接近 O(k),其中 k 是使用的哈希函数数量。 2. **缺点** - 存在一定的假阳性率(False Positive Rate),即可能会错误报告某些未加入过的元素已存在于集合中[^2]。 - 不具备真正的删除能力,除非采用更复杂的变体设计。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值