10 KiB
github仓库存储地址:https://github.com/hlccd/goSTL
概述
Bit-map的基本思想就是用一个bit位来标记某个元素对应的Value,而Key即是该元素。由于采用了Bit为单位来存储数据,因此可以极大的节省存储空间。
原理
对于计算机来说,它可以分配的最小单位是一个字节即8位,一位有且仅有1和0两种表示可能。而如果要表示0~7八个数字在某一个集合内是否存在,比如表示{0,3,5}在集合中是否存在,可以使用一个字节进行存储,即对应位为1的表示存在,为0的表示不存在,即:
1 | 0 | 0 | 1 | 0 | 1 | 0 | 0 |
---|
而如果要表示超过8个的元素是否存在的话,仅仅使用一个字节是远远不够的。
为了解决这个问题,可以选择使用包含更多字节的基本元素,比如uint64,选用uint64而不是int64的原因是因为,在计算机内部存储数字采用补码的形式,对于一个数字如果不是非负整数的话,它需要有一位去标注这个数字是否为负数,而在使用bitmap的时候,使用基本类型仅仅是为了使用它的位,至于是否是负数并不重要,既然如此,舍弃掉正负数标志位就可以多利用一位进行表示。
除此之外,还可以选择多个基本元素进行叠加,即多组混合表示,以表示{0,3,5,8,14,15}为例:
1 | 0 | 0 | 1 | 0 | 1 | 0 | 0 |
---|---|---|---|---|---|---|---|
1 | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
即一第一行为07,第二行及以后为n*8+(07)进行表示,而对于golang实现来说,可以选择用uint64的切片进行表示,即在换算时候对于一个数字num,它所属的行(从0开始)为:
num/64
它所属的列(从0开始)为:
num%64
用处
bitmap由于其本身的有序性和唯一性,一来可以实现快速排序:即对于一组无需且稠密的数字,可以将其放入bitmap中,然后再遍历获取出来,从而使得其元素是单调递增的;而来可以实现去重:由于bitmap是使用bit进行表示一个元素是否存在其中,而bit有且仅有1和0两种情况,所以在一个位置插入多个元素时,它表示的都是1,从而可以实现插入多个保留一个的去重;三来可以极大的节省存储空间:由于对基本类型数组的下标赋予了一定的含义,导致可以利用其含义来表示一个数字,而不是去存储一个数字,即要表示一个2^16的数字时候,原本需要16位,现在只需要1位即可实现。
实现
结构定义
由上文可知,所要使用的bitmap内部其实是存储的uint64的切片,同时增加一个zero,,即
type bitmap struct {
bits []uint64
}
接口集合
type bitmaper interface {
Insert(num uint) //在num位插入元素
Delete(num uint) //删除第num位
Check(num uint) (b bool) //检查第num位是否有元素
All() (nums []uint) //返回所有存储的元素的下标
Clear() //清空
}
New
创建部分主要做的就是返回一个初始化后的bitmap指针
func New() (bm *bitmap) {
return &bitmap{
bits: make([]uint64, 0, 0),
}
}
Insert
对于bitmap的插入来说,会出现两种情况,第一种是要插入的位恰好可以在位图中容纳,此时将对应位设为1即可,并不需要对位图进行扩容,实现起来十分简单,找到对应位即可,第二章就是要插入的位超过了位图可容纳的范围,次数就需要一个扩容策略对其进行扩容以保证插入的值在位图内可以存储。
扩容策略
对于bitmap扩容来说,它的扩容主要是由于新要插入的位超出了位图所能表示的位,从而需要对位图进行扩增以满足其范围,而扩容则可以采取两种策略:
- 固定扩容:对于要扩容的位图,进行一个固定的量进行扩容,该方案的好处实现简单,但坏处也明显,如果固定扩容后还是不足以覆盖还需要再次扩容,同时如果扩容太大也会需要更多的空间;
- 按需扩容:对于要扩容的位图,按照它需要扩容的范围来进行扩容,该方案的好处是几乎可以恰好容纳需要存储的位,但如果存在连续的几个较短的扩容出现时,即先扩容到64->128>256>512>1024这种情况时,需要连续的扩容复制,会浪费一定的性能。
对此,我们可以考虑将两种方案结合起来,即当要增加的范围并不太大的时候,牺牲一定量的空间进行固定扩容,从而避免连续的小范围扩容多次出现降低性能,而对于大范围扩容时,则使用按需扩容,以此来提高单次扩容速度。
本次实现中,固定扩容采用一次增加2^10个uint64。
func (bm *bitmap) Insert(num uint) {
//bm不存在时直接结束
if bm == nil {
return
}
//开始插入
if num/64+1 > uint(len(bm.bits)) {
//当前冗余量小于num位,需要扩增
var tmp []uint64
//通过冗余扩增减少扩增次数
if num/64+1 < uint(len(bm.bits)+1024) {
//入的位比冗余的多不足2^16即1024*64时,则新增1024个uint64
tmp = make([]uint64, len(bm.bits)+1024)
} else {
//直接增加到可以容纳第num位的位置
tmp = make([]uint64, num/64+1)
}
//将原有元素复制到新增的切片内,并将bm所指向的修改为扩增后的
copy(tmp, bm.bits)
bm.bits = tmp
}
//将第num位设为1即实现插入
bm.bits[num/64] ^= 1 << (num % 64)
}
Delete
对于位图的删除,会出现三种情况,第一种是要删除的范围不在位图范围内,直接忽略即可;第二种是在位图范围内,且位图尾部存在连续为0的uint64时,此时就需要执行缩容策略。
缩容策略
对于缩容策略,同样采取三种方案:
- 固定缩容:从尾部删除固定数量的为0的组,该方案删除的量是固定的,优点在于尾部量很多时可以慢慢减少,剩下的可当作冗余量避免多次增加,缺点是缓慢减少需要一定时间开销
- 按需缩容:当存在需要删除的组时,直接进行删除,优点的空间利用率更高,缺点是冗余基本没有,同时删除过于频繁
- 折半删除:当需要删除的组超过一半时,删掉一半即可,该方案删除切合slice底层实现,但在普遍场景来说,可能并不会经常使用。
为此,可以选择将三种结合起来,当总组数很长时,且需要删除的量过多,可以使用固定缩容,留出一定余量,当删除量不多时,且需要删除的量超过了总组数的一半时,在进行按需删除,即把要删除的全部删完。
为了避免固定扩容和固定缩容循环出现,对两者数值进行错位,由于固定扩容选择的是1024,所以固定缩容选为256。
func (bm *bitmap) Delete(num uint) {
//bm不存在时直接结束
if bm == nil {
return
}
//num超出范围,直接结束
if num/64+1 > uint(len(bm.bits)) {
return
}
//将第num位设为0
bm.bits[num/64] &^= 1 << (num % 64)
if bm.bits[len(bm.bits)-1] == 0 {
//最后一组为0,可能进行缩容
//从后往前遍历判断可缩容内容是否小于总组数
i := len(bm.bits) - 1
for ; i >= 0; i-- {
if bm.bits[i] == 0 && i!=len(bm.bits)-1024{
continue
} else {
//不为0或到1024个时即可返回
break
}
}
if i <= len(bm.bits)/2 || i==len(bm.bits)-1024 {
//小于总组数一半或超过1023个,进行缩容
bm.bits = bm.bits[:i+1]
}
} else {
return
}
}
Check
检查第num位是否存在于位图种,不存在返回false,存在返回true,超出范围也返回false。
func (bm *bitmap) Check(num uint) (b bool) {
//bm不存在时直接返回false并结束
if bm == nil {
return false
}
//num超出范围,直接返回false并结束
if num/64+1 > uint(len(bm.bits)) {
return false
}
//判断第num是否为1,为1返回true,否则为false
if bm.bits[num/64]&(1<<(num%64)) > 0 {
return true
}
return false
}
All
返回所有在位图种存在的元素的下标,同时保证其为单调递增序列。
func (bm *bitmap) All() (nums []uint) {
//对要返回的集合进行初始化,以避免返回nil
nums=make([]uint,0,0)
//bm不存在时直接返回并结束
if bm == nil {
return nums
}
//分组遍历判断某下标的元素是否存在于位图中,即其值是否为1
for j := 0; j < len(bm.bits); j++ {
for i := 0; i < 64; i++ {
if bm.bits[j]&(1<<i) > 0 {
//该元素存在,添加入结果集合内
nums = append(nums, uint(j*64+i))
}
}
}
return nums
}
Clear
清空初始化位图。
func (bm *bitmap) Clear() {
if bm == nil {
return
}
bm.bits = make([]uint64, 0, 0)
}
使用示例
package main
import (
"fmt"
"github.com/hlccd/goSTL/data_structure/bitmap"
)
func main() {
var nums []uint
bm:=bitmap.New()
bm.Insert(1)
bm.Insert(2)
bm.Insert(3)
bm.Insert(64)
bm.Insert(128)
bm.Insert(256)
bm.Insert(320)
nums=bm.All()
for i:=0;i< len(nums);i++{
fmt.Println(nums[i])
}
bm.Delete(320)
fmt.Println()
nums=bm.All()
for i:=0;i< len(nums);i++{
fmt.Println(nums[i])
}
bm.Delete(256)
fmt.Println()
nums=bm.All()
for i:=0;i< len(nums);i++{
fmt.Println(nums[i])
}
bm.Delete(128)
fmt.Println()
nums=bm.All()
for i:=0;i< len(nums);i++{
fmt.Println(nums[i])
}
bm.Clear()
fmt.Println()
nums=bm.All()
for i:=0;i< len(nums);i++{
fmt.Println(nums[i])
}
}
结果
1 2 3 64 128 256 320
1 2 3 64 128 256
1 2 3 64 128
1 2 3 64