前置知识:二进制和位运算对数器

特别提醒:Python 的同学实现位运算的题目需要特别注意,需要自己去手动处理溢出和符号扩展等问题,比如:(n << shift_amount) & 0xFFFFFFFF(Python 溢出会自动升位:

位图原理

其实就是用 bit 组成的数组来存放值,用 bit 状态 1、0 代表存在、不存在,取值和存值操作都用位运算

  • 限制是必须为连续范围且不能过大(一开始就会申请这么大的空间)
  • 好处是极大的节省空间,因为 1 个数字只占用 1 个 bit 的空间

位图的实现

package main
 
import (
  "fmt"
  "math/rand"
  "unsafe"
)
 
const (
  INT_BIT_COUNT = int(unsafe.Sizeof(int(0))) * 8 // int 占的位数
  MAXN          = 10000
  TEST_TIMES    = 100000
)
 
var emptyStruct = struct{}{}
 
func main() {
  bitSet := NewBitset(MAXN)
  hashSet := make(map[int]struct{}, MAXN)
 
  for i := 0; i < TEST_TIMES; i++ {
    num := rand.Intn(MAXN)
    decide := rand.Intn(3)
    switch decide {
    case 0:
      bitSet.Add(num)
      hashSet[num] = emptyStruct
    case 1:
      bitSet.Remove(num)
      delete(hashSet, num)
    case 2:
      bitSet.Reverse(num)
      if _, has := hashSet[num]; has {
        delete(hashSet, num)
      } else {
        hashSet[num] = emptyStruct
      }
    }
  }
 
  for i := 0; i < MAXN; i++ {
    if _, has := hashSet[i]; has != bitSet.Contains(i) {
      panic("error")
    }
  }
  fmt.Println("success")
}
 
// 位图
// 使用时 num 不要超过初始化的大小
type Bitset []int
 
// n个数字: [0, n)
func NewBitset(n int) Bitset {
  // a / b 想向上取整,可以写成: (a+b-1) / b
  // 前提是 a 和 b 都是非负数
  return make(Bitset, (n+INT_BIT_COUNT-1)/INT_BIT_COUNT)
}
 
// 把 num 加入位图
func (b Bitset) Add(num int) {
  b[num/INT_BIT_COUNT] |= 1 << (num % INT_BIT_COUNT)
}
 
// 把 num 从位图中删除
func (b Bitset) Remove(num int) {
  b[num/INT_BIT_COUNT] &^= 1 << (num % INT_BIT_COUNT)
}
 
// 如果位图里没有 num,就加入
// 如果位图里有 num,就删除
func (b Bitset) Reverse(num int) {
  b[num/INT_BIT_COUNT] ^= 1 << (num % INT_BIT_COUNT)
}
 
// 查询 num 是否在位图中
func (b Bitset) Contains(num int) bool {
  return b[num/INT_BIT_COUNT]&(1<<(num%INT_BIT_COUNT)) != 0
}

采用对数器验证,当你找不到测试链接的时候就用对数器验证,而且对数器验证更稳妥、更能练习 debug 能力

找到了一个相关测试:2166.设计位集

type Bitset struct {
	data    []uint32
	size    int  // 位图大小
	count   int  // 1 的数量
	flipped bool // 是否翻转
}
 
// 用 size 个位初始化 Bitset ,所有位都是 0
func Constructor(size int) Bitset {
	return Bitset{
		data:    make([]uint32, (size+31)/32),
		size:    size,
		count:   0,
		flipped: false,
	}
}
 
// 将下标为 idx 的位上的值更新为 1 。如果值已经是 1 ,则不会发生任何改变
func (this *Bitset) Fix(idx int) {
	if !this.Has(idx) {
		this.count++
		this.Reverse(idx)
	}
}
 
// 将下标为 idx 的位上的值更新为 0 。如果值已经是 0 ,则不会发生任何改变
func (this *Bitset) Unfix(idx int) {
	if this.Has(idx) {
		this.count--
		this.Reverse(idx)
	}
}
 
// 翻转 Bitset 中每一位上的值。换句话说,所有值为 0 的位将会变成 1 ,反之亦然
func (this *Bitset) Flip() {
	this.flipped = !this.flipped
	this.count = this.size - this.count
}
 
// 第 idx 位是否为 1
func (this *Bitset) Has(idx int) bool {
	bit := this.data[idx/32] & (1 << (idx % 32))
	if this.flipped {
		return bit == 0
	}
	return bit != 0
}
 
// 翻转 idx 位上的值。换句话说,所有值为 0 的位将会变成 1 ,反之亦然
func (this *Bitset) Reverse(idx int) {
	this.data[idx/32] ^= 1 << (idx % 32)
}
 
// 检查 Bitset 中每一位的值是否都是 1
func (this *Bitset) All() bool {
	return this.count == this.size
}
 
// 检查 Bitset 中是否至少一位的值是 1
func (this *Bitset) One() bool {
	return this.count > 0
}
 
// 返回 Bitset 中值为 1 的位的总数
func (this *Bitset) Count() int {
	return this.count
}
 
// 返回 Bitset 的当前组成情况
// 注意,在结果字符串中,第 i 个下标处的字符应该与 Bitset 中的第 i 位一致
func (this *Bitset) ToString() string {
	var (
		builder strings.Builder
		status byte
	)
	for i := 0; i < this.size; i++ {
		status = byte((this.data[i/32] >> (i % 32)) & 1)
		if this.flipped {
			status ^= 1
		}
		builder.WriteByte('0' + status)
	}
	return builder.String()
}

添加 flipped 变量使得 Flip() 时间复杂度降为 O(1),而不用遍历整个位图;添加 count 变量使得 All()One()Count() 时间复杂度降为 O(1),而不用遍历整个位图,非常巧妙

备注

位图的后续扩展(如:布隆过滤器、资源限制类题目),将在【扩展】课程里进一步讲述