Bitmap快速实现排序、查询、去重
记得很久之前看到的一个看似逼格很高的面试题:
一个存满32位整数的200G磁盘,怎样用4G内存找到所有出现过恰好3次的整数。
所有32位整数共2 ^ 32个每个又占4 byte (32 bit),所以一共需要2^32*4/1024/1024/1024 = 16G,如果单纯遍历并存储所有出现的数字,4G内存肯定是远远不够的,所以有没有什么更好更省空间的办法呢?答案是肯定的。
我们可以创建一个2 ^ 32长度的byte数组a ([2 ^ 32]byte),大小正好位2^32/1024/1024/1024 = 4G。数组的每一个元素代表一个数字及其出现次数,比如a[0]代表0出现的次数,当数字i每出现一次,a[i]就加1,如果a[i] > 3,a[i]就无需再增加,因为我们只要找出刚好出现三次的整数,超过三次的我们就可以不再关注了。
数字1出现两次
a[1]
数字18出现三次
a[18]
其实,还可以更省空间,每个数字并不需要一个byte来存储其出现次数,只要有3个bit位就够了,000未出现,001出现一次,010出现两次,011出现三次,100出现大于三次,这样总共需要2^32*3/1024/1024/1024 = 1.5G就够了。
下面介绍一下bitmap(位图)。
所谓的bitmap就是用一个bit位来标记某个元素所对应的value,而key就是该元素,由于bitmap使用bit来存储数据,因此可以大大节省存储空间。在位图中,每个元素位1或0,表示其对应的元素是否存在。
来看一个具体例子,假设我们要对0~7之中的5个数字进行排序,比如(4, 7, 2, 5, 3)。要表示8个数字,我们只需要开辟8 bit (1 byte)的空间,并将所有bit位设为0;然后遍历这5个数字,第一个是4,所以把下标为4(也就是第五个)的元素设为1,以此类推;最后我们遍历这个8 bit,把值为1的输出,即得到排好序的元素(2, 3, 4, 5, 7)。
bitmap的golang实现:
// bitmap数据结构
type Bitmap struct {
data []byte
bitsize uint64
}
// 置位和清位
func (this *Bitmap) SetBit(offset uint64, value uint8) bool {
index, pos := offset/8, offset%8
if this.bitsize < offset {
return false
}
if value == 0 {
this.data[index] &^= 0x01 << pos
} else {
this.data[index] |= 0x01 << pos
}
return true
}
典型应用场景:
- 快速排序:上例
- 快速去重:00表示不存在,01表示存在一个,11表示存在两个或更多
- 快速查询:1表示存在,0表示不存在
优点:
- 运算效率高,不需要进行比较和移位;
- 占用内存少。
缺点:
- 不可对重复的数据进行排序和查找。