← 中文资源

简体中文版本

位集合成员判定

用紧凑的 64 位字存储整数标识,支持置位、清位、并集、交集、计数与有序遍历。

阅读英文原文

功能说明

当标识是连续的整数编号时,用每个比特表示一个元素比哈希集合更省内存,判定也只是一次位运算。这个位集合以 uint64 切片为底层,容量向上取整到 64 的倍数,适合权限位、活跃用户编号、布隆过滤器底层数组等场景。

除了单点读写,示例还提供并集、交集、计数和有序遍历。计数使用 math/bits 的 OnesCount64,遍历用 TrailingZeros64 逐个取出最低位的 1,因此结果天然按编号升序排列,不需要额外排序。

关键代码

Set 和 Clear 先用下标除以 64 定位字,再用对 64 取余得到位偏移;置位用按位或,清位用按位与非。Has 判断对应位是否为 1。Union 选择两个集合中较长的长度,缺失的字按零处理;Intersect 选择较短长度,逐字相与。Slice 只输出值为 1 的位置,并在循环中把已取出的位清零,避免死循环。

使用注意

容量在构造时确定,写入超出容量的下标会触发切片越界 panic,调用方需要自行校验编号范围。位集合不存储原始元素,只能表示非负整数。清位和置位是原地修改,多协程同时写入必须加锁。并集和交集返回新切片,不会修改输入。容量为零的集合在并集时会采用另一侧长度,结果仍然正确。序列化时注意字序,跨语言读取要约定端序。

验证方式

容量八十位时内部占用两个字,即一百二十八位。依次置位 1、5、64、79 后计数为四,查询 64 返回 true,查询 65 返回 false,遍历输出 1、5、64、79。与包含 5 和 9 的集合求并集得到五个元素,交集只包含 5。清除 64 后计数变为三,说明清位正确。

代码示例

package main

import (
	"fmt"
	"math/bits"
)

type Bitset []uint64

func NewBitset(size int) Bitset {
	if size < 0 {
		panic("negative bitset size")
	}
	return make(Bitset, (size+63)/64)
}

func (b Bitset) Set(index int) {
	b[index/64] |= uint64(1) << (uint(index) % 64)
}

func (b Bitset) Clear(index int) {
	b[index/64] &^= uint64(1) << (uint(index) % 64)
}

func (b Bitset) Has(index int) bool {
	return b[index/64]&(uint64(1)<<(uint(index)%64)) != 0
}

func (b Bitset) Union(other Bitset) Bitset {
	size := len(b)
	if len(other) > size {
		size = len(other)
	}
	out := make(Bitset, size)
	for i := range out {
		var left, right uint64
		if i < len(b) {
			left = b[i]
		}
		if i < len(other) {
			right = other[i]
		}
		out[i] = left | right
	}
	return out
}

func (b Bitset) Intersect(other Bitset) Bitset {
	size := len(b)
	if len(other) < size {
		size = len(other)
	}
	out := make(Bitset, size)
	for i := range out {
		out[i] = b[i] & other[i]
	}
	return out
}

func (b Bitset) Count() int {
	total := 0
	for _, word := range b {
		total += bits.OnesCount64(word)
	}
	return total
}

func (b Bitset) Slice() []int {
	out := make([]int, 0, b.Count())
	for wordIndex, word := range b {
		for word != 0 {
			bit := bits.TrailingZeros64(word)
			out = append(out, wordIndex*64+bit)
			word &^= uint64(1) << uint(bit)
		}
	}
	return out
}

func main() {
	active := NewBitset(80)
	for _, id := range []int{1, 5, 64, 79} {
		active.Set(id)
	}
	blocked := NewBitset(80)
	blocked.Set(5)
	blocked.Set(9)

	fmt.Println("count:", active.Count())
	fmt.Println("has 64:", active.Has(64), "has 65:", active.Has(65))
	fmt.Println("members:", active.Slice())
	fmt.Println("union count:", active.Union(blocked).Count())
	fmt.Println("intersect:", active.Intersect(blocked).Slice())

	active.Clear(64)
	fmt.Println("after clear:", active.Count())
}