数据结构 Bitmap(位图)完整详解

数据结构 Bitmap(位图)完整详解

一、什么是 Bitmap?

Bitmap(位图) 是一种基于位的数组结构,使用每一个 bit 位来存储一个二元状态(0/1,true/false,存在/不存在)。它将一个范围(如整数 ID、枚举值)直接映射到内存中的某个 bit 位置,从而实现极高的空间效率和快速的位置访问。

核心思想:用 1 个 bit 代替原本需要 1 个 byte(甚至更多)才能存储的布尔信息。


二、核心原理

假设我们要记录 0n-1 共 n 个元素的存在性。传统做法是用一个 boolean[],每个布尔值在大多数语言中占用 1 字节(Java)甚至 4 字节(C# bool)。而 Bitmap 将 n 个 bit 连续存储,这些 bit 被组织成若干个字节(或字)。

映射关系:

  • k 个 bit 对应数字 k 的状态。
  • 内存中第 i 个字节的第 j 位(通常 j 从 0 到 7,从低位到高位或相反):
    • 字节索引 byteIndex = k / 8
    • 位偏移 bitOffset = k % 8

访问公式:

  • 读取:(bytes[byteIndex] >> bitOffset) & 1
  • 设置 1:bytes[byteIndex] |= (1 << bitOffset)
  • 设置 0:bytes[byteIndex] &= ~(1 << bitOffset)
  • 翻转:bytes[byteIndex] ^= (1 << bitOffset)

三、基本操作

操作 描述 时间复杂度
set(k) 将第 k 位设为 1 O(1)
clear(k) 将第 k 位设为 0 O(1)
test(k) 返回第 k 位的值 O(1)
count() 统计所有 1 的个数 O(n) 或硬件加速
bitwise AND/OR/XOR/NOT 两个 bitmap 之间的集合运算 O(n)
nextSetBit(from) 从某个位置向后找第一个 1 O(n) 或通过分段优化

位运算操作是 bitmap 相对于其他集合(如 HashSet)的独特优势,可以极快地完成并集、交集、差集,非常适合批处理和索引合并。


四、空间占用与计算

  • 理论最小内存size_in_bits = max_element + 1 时,内存占用 = ceil(size_in_bits / 8) 字节。

  • 实例

    • 记录 0~99 共 100 个数 → 100 bits ≈ 13 字节
    • 记录 0~10 亿(10^9) → 10^9 bits ≈ 119 MB
    • 记录 0~2^32-1(约 43 亿) → 512 MB
  • 对比

    数据结构 存储 1 亿个布尔值 内存
    boolean[] (Java) 100M × 1 字节 100 MB
    BitSet (Java) 100M bits 12.5 MB
    HashSet<Integer> (存储存在的那些值) 若存在 5000 万个数,每个 Integer 对象+指针 ≈ 1.2 GB+

但注意:bitmap 的空间开销与值域范围 成正比,与实际元素个数无关。如果值域稀疏(如只有 10 个元素,但它们的值在 0~10^9 之间),单纯 bitmap 会非常浪费。此时需用稀疏优化方案(Roaring Bitmap)。


五、实现细节(以 Java 和 Python 为例)

Java java.util.BitSet 的内部实现
  • 内部使用 long[] words,每个 long 存储 64 个 bit。
  • 动态扩容:当需要的索引超出当前大小时,自动创建更大的数组。
  • 提供了丰富的集合运算:and, or, xor, andNot
java 复制代码
BitSet bs = new BitSet();
bs.set(100);           // 设置第100位为1
bs.set(200, 300);      // 设置区间 [200,300) 为1
boolean exist = bs.get(100); // true
bs.clear(100);         // 清除
int next = bs.nextSetBit(0); // 找到第一个1的位置
Python 使用 bytearray 实现简易 bitmap
python 复制代码
class Bitmap:
    def __init__(self, size):
        self.size = size
        self.bytes = bytearray((size + 7) // 8)

    def set(self, k):
        if k >= self.size: raise IndexError
        self.bytes[k >> 3] |= 1 << (k & 7)

    def clear(self, k):
        self.bytes[k >> 3] &= ~(1 << (k & 7))

    def test(self, k):
        return (self.bytes[k >> 3] >> (k & 7)) & 1

    def count(self):
        # bin(x).count('1') 效率低,可查表或使用 int.bit_count() (Python 3.8+)
        return sum(b.bit_count() for b in self.bytes)

六、优点与局限性

✅ 优点
  1. 极致空间效率:比基于字节的布尔数组节省 8~32 倍内存。
  2. CPU 缓存友好:连续内存布局,批量操作时高速缓存命中率高。
  3. 并行/向量化:一次 64 位运算可同时处理 64 个元素(SIMD 友好)。
  4. 集合运算极快:交集、并集只需逐字进行位运算,时间复杂度 O(N/word_size)。
❌ 局限性
  1. 固定值域:需要预先知道最大元素范围,范围过大而元素稀疏时浪费严重。
  2. 只能存储二元状态:不能直接存储关联数据(如权重、附加属性)。
  3. 非动态稀疏:若最大 ID 上亿但只有几千个元素,512 MB 内存远高于红黑树或哈希表。
  4. 计数与遍历 :统计 1 的个数(count)需要扫描整个位图(O(内存大小)),除非用硬件 popcount 指令加速,但扫描依然需要遍历所有 bits。

七、高级变体:解决稀疏问题

为解决"值域大而元素少"时的内存浪费,学术界和工业界提出了多种压缩 bitmap 结构,其中最著名的是 Roaring Bitmap

Roaring Bitmap 核心思想
  • 将 32 位整数范围按高 16 位分成 2^16 个桶(chunk),每个桶最多包含 65536 个可能的低 16 位值。
  • 桶内根据元素密度选择不同容器:
    • Array Container:稀疏时(元素数 < 4096),存储有序 16 位整数数组,内存约 2 字节/元素。
    • Bitmap Container:稠密时(元素数 ≥ 4096),使用 8KB 的位图(65536 bits = 8KB),内存固定。
    • Run Container(可选):对连续区间(如 [100,200])使用 RLE 编码,空间更优。

效果:对随机分布的元素,Roaring 比普通 bitmap 节省 90% 以上内存,且交集、并集性能仍然极高。已被广泛应用于:Spark、Druid、ClickHouse、Lucene 等。

其他变体
  • EWAH (Compressed Word-Aligned Bitmap):使用运行长度编码,适合数据库。
  • Concise:对连续 0/1 压缩,查询略慢。
  • Java BitSet 的简单扩展:不支持压缩,仅固定长度。

八、典型应用场景深度剖析

应用领域 具体说明 为何用 Bitmap
海量整数判存(布隆过滤器底层) 如网页 URL 去重、垃圾邮件过滤。布隆过滤器使用多个 bitmap 和多个哈希函数。 极低内存 + 可容忍小概率误判。
用户签到/活跃统计 每个用户一条记录,每天一个 bit 表示是否签到。1 亿用户 × 365 天 = 365 亿 bits ≈ 4.3 GB,比传统方式小几十倍。 按天按位运算,快速统计连续签到天数等。
数据库位图索引 对低基数列(性别、地区、产品类别)建立位图索引,每个值对应一个 bitmap,标记哪些行具有该值。适合 OLAP 多维分析。 支持快速 AND/OR 多条件过滤,空间远小于 B-Tree。
权限系统 使用 32 位整数作为权限掩码,每个 bit 代表一种权限(读、写、删除、审核...)。 位运算组合权限,操作简单,存储仅 4 字节/用户。
磁盘块/内存页管理 操作系统的空闲块位图,每个 bit 表示磁盘块或内存页是否空闲。 内存小,查询和修改极快。
图着色/状态标记 BFS/DFS 中记录节点是否访问过,当节点数达百万级以上时,bitmap 比 bool 数组节省内存。 节省内存,避免 OOM。
倒排索引压缩 搜索引擎中 docID 列表可用 bitmap 或 Roaring 存储,用于快速求交集(多关键词搜索)。 位图压缩 + 高性能集合运算。
实时风控/反作弊 记录用户在最近 N 小时内是否触发某个事件(如 24 小时滑动窗口),每个时间片用一位。 滑动窗口可通过移位和位运算实现,高效无锁。

九、性能考量与优化技巧

  1. 计数优化 :使用 CPU 提供的 POPCNT 指令(通过 Long.bitCountint.bit_count)硬件加速,比逐位检查快 10 倍以上。
  2. 循环遍历所有 1 :不要逐位扫描,使用 nextSetBit 跳跃到下一个 1,内部实现通常按字查找。
  3. 批量操作 :尽可能使用 andor 等矢量操作,而不是对每个 bit 单独循环。
  4. 选择合适的变体
    • 稠密、值域固定 → 普通 bitmap
    • 值域很大(> 2^31)但分布未知 → Roaring Bitmap
    • 需要持久化到磁盘 → 考虑 EWAH 或 Roaring 的序列化格式
  5. 字节对齐 :在 C/C++ 中访问时使用 uint64_t 对齐,避免跨字访问开销。

十、总结

特性 描述
本质 以 bit 为基本单位的数组,映射整数到状态
内存 N bits 存储 N 个二元值,约 N/8 字节
速度 单点 O(1),批量运算 O(N/字长)
最佳适用 值域紧凑、元素稠密或需要高速集合运算的场景
稀疏优化 Roaring Bitmap 等压缩结构

Bitmap 是计算机科学中"用空间换时间"的反面------用极致的空间压缩换取大量内存节省,同时在批量集合运算上甚至超过传统结构。理解 bitmap,不仅是掌握一个数据结构,更是学会如何用位这一最底层的单元去表示海量信息,这是高级系统设计和大数据处理的基石之一。

相关推荐
008爬虫实战录9 小时前
【码上爬】 题十八:模拟大厂加密算法, 堆栈分析找加密点,扣自执行函数,jsdom补环境
开发语言·javascript·ecmascript
skywalk81639 小时前
言知中文编程语言计划书 by WorkBuddy
开发语言·编程
代码中介商9 小时前
排序算法完全指南(五):快速排序深度详解
数据结构·算法·排序算法
NiceCloud喜云9 小时前
AutoClaw 接入自定义 Anthropic 端点:让 Kanban 工作流跑在自己的模型路由上
java·开发语言·c++·人工智能·python·eclipse·batch
skywalk81639 小时前
脚本 isMobile.js(移动设备检测库)的核心实现
开发语言·javascript·ecmascript
jieyucx9 小时前
从基础语法到面向对象:Go语言如何实现封装、继承与多态?
开发语言·后端·golang
SuperArc19999 小时前
jar包文件修改(java编译与反编译)
java·开发语言·后端·jar·反编译
计算机安禾9 小时前
【c++面向对象编程】第49篇:面向对象的单元测试:用GoogleTest测试类
开发语言·c++·单元测试
lly2024069 小时前
Python3 条件控制
开发语言