TL;DR
直接看代码
🎯《编程珠玑》中有这样的一个问题:
在一个有 40 亿个随机排列的 32 位整数的顺序文件中,找出一个不在文件中的 32 位整数,在具有足够内存的情况下应该如何解决?如果可以借助几个临时文件,但是可用内存很小的情况,又该如何解决?
在有足够内存的情况下,可以先将 40 亿个整数读取出来后按大小排序,然后遍历这个列表找出不存在的数字。
但是,40 亿 个整数占用的内约为 4e10 * 32 / 8 / 1024 / 1024 / 1024 ≈ 149 GB
,这样的实现方式我们日常是根本不可能用的。而在内存很小的那种极端情况其实很少会遇到。
所以,作者提出了使用 bitmap 的解决方案
✨ Bitmap Intro#
在 bitmap 中,每个 bit 位都是一个标识位,可以表示某种意义。
👆 上面的问题,我们简化一下,假设有 200 个 8 位的正整数(0~255),我们可以声明一个 [32]uint8
的数组作为 bitmap
,在这个数组中,一共有 32 * 8 = 256
个 bit 位,刚好可以覆盖这 200 个整数。 第 0 个元素的第 0 个 bit 位表示数字 0,第 1 个 bit 表示数字 1,第 1 个元素的第 0 个 bit 位表示数字 8,第 1 个元素的第 1 个 bit 位表示数字 9,以此类推(都是从 0 开始数)
0 0 0 0 0 0 0 0 | 0 0 0 0 0 0 0 0 | ... | 0 0 0 0 0 0 0 0
0 1 2 3 4 5 6 7 8 9 ...
我们遍历这 200 个数字,分别将 bitmap 中对应下标位置的 bit 位的值修改为 1
假如前 4 个数字为 3, 5, 1, 2
那么,bitmap 应该会变成这样
0 1 1 1 0 1 0 0 | 0 0 0 0 0 0 0 0 | ... | 0 0 0 0 0 0 0 0
0 1 2 3 4 5 6 7 8 9 ...
当我们把 200 个元素遍历完成后,bit 位的值为 0 的下标就是不存在的数字了。
🌈 Bitmap 的优点#
常规方式我们把 200 个数字读取到一个 slice 中排序,这些数字占用的内存为 200 * 8 / 8 = 200 bytes
而是用 bitmap 的话,只需要 32 * 8 / 8 = 32 bytes
而且数据量越大,bitmap 的优势越明显
💻 Bitmap 的实现#
ok,讲重点,理解了 bitmap 后,其实实现并不难。首先我们需要先了解一下 位运算。
🧬 结构定义#
我们首先来定义一下接口和结构体这些骨架
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
package main
import (
"errors"
)
// BitMap go 目前泛型还没有上线,我们定义 bitmap 只能固定一种类型,
// 这里用 uint64 就只是展示。添加了泛型后会更加灵活
type BitMap interface {
// Add 添加一个元素到 bitmap 中
Add(uint64) error
// Del 从 bitmap 中删除一个元素
Del(uint64) error
// Contain bitmap 中是否包含这个数字
Contain(uint64) bool
}
type bitmap struct {
store []uint64
}
// Add 实现了 BitMap 接口的方法
func (b *bitmap) Add(u uint64) error {
panic("implement me")
}
// Del 实现了 BitMap 接口的方法
func (b *bitmap) Del(u uint64) error {
panic("implement me")
}
// Contain 实现了 BitMap 接口的方法
func (b *bitmap) Contain(u uint64) bool {
panic("implement me")
}
// ErrZeroCapacity 表示要创建的 bitmap 的容量小于等于 0,这样是没有意义的
var ErrZeroCapacity = errors.New("capacity should greater than 0")
// NewBitmap 创建一个 Bitmap,cap 表示 bitmap 的容量
func NewBitmap(cap int) (*bitmap, error) {
if cap <= 0 {
return nil, ErrZeroCapacity
}
return &bitmap{
store: make([]uint64, cap/64+1), // +1 防止数字太小整除后结果为 0
}, nil
}
// 类型检查
var _ BitMap = (*bitmap)(nil)
|
🧪 测试用例#
养成好习惯,写代码之前先写测试用例
tips: goland 和 vscode 都支持自动生成测试用例,文件中点击右键后选择 generate 就可以生成测试用例了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
|
package main
import (
"reflect"
"testing"
)
func TestNewBitmap(t *testing.T) {
type args struct {
cap int
}
tests := []struct {
name string
args args
want *bitmap
wantErr bool
}{
{
name: "negative capacity",
args: args{
cap: -1,
},
want: nil,
wantErr: true,
},
{
name: "ok",
args: args{
cap: 1,
},
want: &bitmap{
store: make([]uint64, 1),
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := NewBitmap(tt.args.cap)
if (err != nil) != tt.wantErr {
t.Errorf("NewBitmap() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("NewBitmap() got = %v, want %v", got, tt.want)
}
})
}
}
func Test_BitMap(t *testing.T) {
bm, _ := NewBitmap(10)
if err := bm.Add(10); err != nil {
t.Errorf("unexpected error: %v", err)
}
if !bm.Contain(10) {
t.Errorf("bitmap should contain number 10")
}
if err := bm.Del(10); err != nil {
t.Errorf("unexpect error: %s", err)
}
if bm.Contain(10) {
t.Errorf("bitmap should not contain number 10")
}
}
|
⚓ 计算坐标#
我们如何快速计算出一个数字应该存储在 bitmap 中的哪个位置?
我们还是以 uint8 的 bitmap 来计算…因为方便😂
0 0 0 0 0 0 0 1 | 0 1 0 0 0 0 0 0 | ... | 0 0 0 0 0 0 0 0
0 1 2 3 4 5 6 7 8 9
数字 7 存储在第 0 个元素的第 7 个 bit 位上(从 0 开始数)
数字 9 存储在第 1 个元素的第 1 个 bit 位上
计算公式如下
7 / 8 = 0
1 « (7 % 8) = 0b10000000
9 / 8 = 1
1 « (9 & 8) = 0b00000001
得到了这个规律,我们就可以编写代码了,因为每个方法都需要计算坐标,所以我定义了一个 coordinate
的方法
1
2
3
4
|
// 计算 u 在 bitmap 中的坐标
func (b *bitmap) coordinate(u uint64) (int, uint64) {
return int(u / bitSize), 1 << (u % bitSize)
}
|
📥 添加元素#
知道了如何计算坐标以后,剩下的就简单了。
1
2
3
4
5
6
7
8
9
10
11
12
|
// ErrOutOfRange 表示存储的元素超出了 bitmap 的最大容量
var ErrOutOfRange = errors.New("out of range")
// Add 实现了 BitMap 接口的方法
func (b *bitmap) Add(u uint64) error {
x, y := b.coordinate(u)
if x > len(b.store) {
return ErrOutOfRange
}
b.store[x] |= y
return nil
}
|
📤 删除元素#
1
2
3
4
5
6
7
8
9
|
// Del 实现了 BitMap 接口的方法
func (b *bitmap) Del(u uint64) error {
x, y := b.coordinate(u)
if x > len(b.store) {
return ErrOutOfRange
}
b.store[x] &= ^y
return nil
}
|
⁉️ 是否包含元素#
1
2
3
4
5
6
7
8
|
// Contain 实现了 BitMap 接口的方法
func (b *bitmap) Contain(u uint64) bool {
x, y := b.coordinate(u)
if x > len(b.store) {
return false
}
return b.store[x]&y == y
}
|
测试代码#
一个简易的 bitmap 就实现了,现在就可以用测试用例来验证一下我们的逻辑是否正确
1
2
|
~/src/bitmap » go test .
ok github.com/ningzio/bitmap 0.563s
|
✅ 测试通过~