bitmap代码实现前置知识
在正式介绍代码实现前, 有必要先跟大家科普下关于bitmap的知识。
概念
Bitmap(位图)是一种数据结构,用于高效地表示和操作大量的布尔值或整数集合。位图利用位(bit)来表示元素是否存在,例如,用 1 表示存在,0 表示不存在,适合在需要处理大量数据且仅需简单存在性查询的场景中使用。
为什么要使用BitMap这种数据结构?
使用 Bitmap 的主要原因在于其内存效率高和查询速度快,尤其在以下场景中特别有用:
- 大规模数据存在性判断:Bitmap 可以节省大量内存来存储布尔状态或集合成员,例如用于黑名单、白名单、会员列表等场景。
- 快速集合操作:位运算(如 AND、OR)可以同时对位图的多个值进行操作,快速完成集合间交集、并集的计算。
- 压缩存储:当数据稀疏或重复较多时,位图可以通过稀疏位图或压缩位图进一步节省空间。
对比其他数据结构的优势
- 内存高效:每个元素仅占用一位,而不是字节或更大的存储单位,因此在表示大量布尔数据时非常节省空间。
- 快速查询和更新:通过索引直接访问某个位的值,查询和更新操作都是 O(1)。
- 并行处理友好:Bitmap 操作可以利用并行位运算,如批量 AND、OR、XOR,实现高效的集合运算。
- 简洁的布尔运算:适用于大规模数据的集合判断,如大数据处理中去重、去噪、集合合并等操作。
适用场景
- 布隆过滤器(Bloom Filter):用于快速判断元素是否在集合中,常用于缓存、数据库等。
- 数据去重:大规模去重场景下,使用位图表示已处理的数据,防止重复处理。
- 图像处理:位图用于表示图像的像素点,常用于图像存储和压缩。
- 权限控制:位图用于权限系统,快速标记用户权限或角色是否存在某权限。
示例
假设需要判断一个百万级用户列表中某个用户 ID 是否出现过。用普通的布尔数组可能会占用大量内存,而使用位图,可以用不到 1 MB 的空间来记录百万个用户的存在性,非常高效。
go代码实现
结构定义
type Bitmap struct {
mp map[string]int // 存储连续的bitmap
base int64 // 每个int存储key的个数, 最多为64
lock map[string]*sync.RWMutex // 读写锁, 支持并发安全
}
func NewBitmap(base int64) *Bitmap {
return &Bitmap{
mp: make(map[string]int, 0),
base: base,
lock: make(map[string]*sync.RWMutex, 0),
}
}
base决定一个int拿多少位来存储数据
mp为实际存储bitmap数据的字段, 它是一个键值对, key表示对一个数字对base作除法的商, value为一个存了base(最高为64)位的bitmap
lock 为每一个key配的一把读写锁, 保证并发读写安全
bitmap添加值
// Set 设置值
func (bm *Bitmap) Set(index string) error {
// 创建一个新的 big.Int
bi := new(big.Int)
mod := new(big.Int)
quotient := new(big.Int)
// 将字符串转换为 big.Int
_, success := bi.SetString(index, 10)
if !success {
return errors.New("the index is not a number")
}
// 对index作除法, 取得key, 即存放在bm字段的位置
quotient.Div(bi, big.NewInt(bm.base))
// 检查锁是否存在, 如果不存在则创建一个新的读写锁, 并赋值给当前的key值
if _, ok := bm.lock[quotient.String()]; !ok {
bm.lock[quotient.String()] = new(sync.RWMutex)
}
// 上锁操作
bm.lock[quotient.String()].Lock()
defer bm.lock[quotient.String()].Unlock()
// 对index取模
mod.Mod(bi, big.NewInt(int64(bm.base)))
if !mod.IsInt64() {
return errors.New("cant not set this index")
}
intMod := mod.Int64()
// 存储到bitmap中
bm.mp[quotient.String()] |= 1 << intMod
return nil
}
使用golang官方提供的math/big包可以对高精度数字进行操作
对数字取商做key, 取模算出该数字在bitmap中对应的位置, 并通过bm.mp[quotient.String()] |= 1 << intMod将值存入其中
bitmap判断值存不存在
// Get 判断该值是否存在
func (bm *Bitmap) Get(index string) (bool, error) {
// 创建一个新的 big.Int
bi := new(big.Int)
mod := new(big.Int)
quotient := new(big.Int)
// 将字符串转换为 big.Int
_, success := bi.SetString(index, 10)
if !success {
return false, errors.New("the index is not a number")
}
quotient.Div(bi, big.NewInt(bm.base))
if _, ok := bm.lock[quotient.String()]; !ok {
bm.lock[quotient.String()] = new(sync.RWMutex)
}
bm.lock[quotient.String()].RLock()
defer bm.lock[quotient.String()].RUnlock()
mod.Mod(bi, big.NewInt(int64(bm.base)))
if !mod.IsInt64() {
return false, errors.New("cant not set this index")
}
intMod := mod.Int64()
return (bm.mp[quotient.String()] & (1 << intMod)) != 0, nil
}
两个bitmap取交集
对bitmap取交集的操作在很多地方都会用到, 如搜索多个关键词, 取这几个关键词的交集, 本文章也实现了对bitmap取交集的函数, 具体代码如下
// Intersect 将会取bm跟bm2的交集, 需保证base相同
func (bm *Bitmap) Intersect(bm2 *Bitmap) *Bitmap {
res := NewBitmap(bm.base)
for s, v1 := range bm.mp {
bm.lock[s].RLock()
if v2, ok := bm2.mp[s]; ok {
bm2.lock[s].RLock()
res.lock[s] = new(sync.RWMutex)
res.mp[s] = v1 & v2
bm2.lock[s].RUnlock()
}
bm.lock[s].RUnlock()
}
return res
}
通过遍历第一个bitmap的key值, 判断第二个bitmap是否存在该key, 如果存在, 对两个数做与运算就能得出它们的交集, 计算时间为0(1),遍历key值的复杂度为o(n), 整体复杂度为o(n), 速度还是挺快的, 这也是bitmap的优势所在
实现迭代器便于对bitmap进行遍历
// 创建bitmap迭代器
func (bm *Bitmap) CreateBitMapIterator() *BitMapIterator {
keys := maps.Keys(bm.mp)
sort.Strings(keys)
return &BitMapIterator{
bitmap: bm,
keys: keys,
idx: 0,
}
}
// bitmap迭代器定义
type BitMapIterator struct {
bitmap *Bitmap
keys []string
idx int
count int
}
// 返回结果定义
type BitMapResult struct {
Data string
}
func (iter *BitMapIterator) Next() *BitMapResult {
if iter.idx >= len(iter.keys) {
return nil
}
// 初始化赋值
if iter.count == 0 {
iter.count = iter.bitmap.mp[iter.keys[iter.idx]]
// 处理为0的情况
if iter.count == 0 {
iter.idx++
return iter.Next()
}
}
iter.bitmap.lock[iter.keys[iter.idx]].RLock()
defer iter.bitmap.lock[iter.keys[iter.idx]].RUnlock()
res := new(big.Int)
quotient := new(big.Int)
quotient.SetString(iter.keys[iter.idx], 10)
res.Mul(quotient, big.NewInt(iter.bitmap.base))
mod := int64(bits.TrailingZeros(uint(iter.count)))
res.Add(res, big.NewInt(mod))
iter.count -= 1 << mod
if iter.count == 0 {
iter.idx++
}
return &BitMapResult{
Data: res.String(),
}
}
总结
Bitmap 是一种空间效率高、查询效率快的数据结构,特别适合海量布尔数据的存储与集合操作。本实现采用高精度数处理、大规模并行操作与并发控制的设计,既支持超大数据集,又保证了数据的并发安全性,非常适合在大数据处理、权限控制、去重等应用场景中使用。