Skip to content

Day 35: std.bit_set模块:位集操作

欢迎来到第三十五天!在处理大量布尔值(true/false)或开关状态时,使用一个 []bool 数组虽然直观,但内存效率很低,因为每个 bool 值通常会占用一整个字节(8位)。std.bit_set 模块提供了一个**位集(Bit Set)**数据结构,它将这些布尔值紧凑地打包到一块连续的内存中,每个值只占用一位(1 bit)。这使得位集成为了在需要管理大量开关状态(如权限、集合成员资格、状态标志等)时,一种极其节省内存且高效的选择。

std.bit_set 提供了两种主要的位集类型:

  • std.bit_set.StaticBitSet(N: usize): 一个静态位集,其大小 N 在编译时就已确定。它的内存是直接内联在结构体中的,不需要任何堆分配。

  • std.bit_set.DynamicBitSet: 一个动态位集,其大小可以在运行时确定和改变。它需要一个分配器来管理其内存。

今天我们主要关注更常用的 StaticBitSet。

const std = @import("std");
// 创建一个可以存储256个位的静态位集
const MyBitSet = std.bit_set.StaticBitSet(256);
var permissions = MyBitSet.initEmpty(); // 初始化为空集

initEmpty() 创建一个所有位都为0(false)的位集。initFull() 则创建一个所有位都为1(true)的位集。

位集的核心操作是设置(set)、清除(clear)和测试(test)某一位的状态。

  • set(index: usize): 将指定索引的位设置为1 (true)。
  • unset(index: usize): 将指定索引的位设置为0 (false)。
  • isSet(index: usize) bool: 检查指定索引的位是否为1 (true)。
permissions.set(10); // 将第10位置为 true
permissions.set(42);
if (permissions.isSet(10)) { // true
// ...
}
permissions.unset(10); // 将第10位置为 false
if (permissions.isSet(10)) { // false
// ...
}

BitSet 支持标准的集合运算:

  • union(other: BitSet): 并集。a.union(b)
  • intersect(other: BitSet): 交集。a.intersect(b)
  • diff(other: BitSet): 差集。a.diff(b) (a 中有但 b 中没有的)
  • toggle(index: usize): 翻转某一位。

迭代 (Iteration)

你可以迭代一个位集中所有被设置为 true 的位的索引。

var it = permissions.iterator();
while (it.next()) |index| {
// index 是一个被设置为 true 的位的索引
std.debug.print("Bit {d} is set.\n", .{index});
}

位集非常适合用来表示一组独立的权限标志。

const std = @import("std");
const Permission = enum(u4) {
Read = 0,
Write = 1,
Execute = 2,
Admin = 3,
};
const PermissionSet = std.bit_set.StaticBitSet(4);
pub fn main() !void {
var user_perms = PermissionSet.initEmpty();
user_perms.set(@enumToInt(Permission.Read));
user_perms.set(@enumToInt(Permission.Write));
var admin_perms = PermissionSet.initFull();
if (user_perms.isSet(@enumToInt(Permission.Write))) {
std.debug.print("User can write.\n", .{});
}
if (!user_perms.isSet(@enumToInt(Permission.Admin))) {
std.debug.print("User is not an admin.\n", .{});
}
}

6. 实践练习:使用位集实现一个素数筛

Section titled “6. 实践练习:使用位集实现一个素数筛”

这是一个经典的算法练习,非常适合用位集来优化。

  1. 创建一个 StaticBitSet(1000) 来表示数字0到999。
  2. 初始化时,假设所有数字都是素数(将所有位设置为 true),但0和1除外。
  3. 实现埃拉托斯特尼筛法(Sieve of Eratosthenes):
    • 从第一个素数 p=2 开始。
    • 将所有 p 的倍数(2p, 3p, …)在位集中标记为非素数(unset)。
    • 找到下一个未被标记的数字,它就是下一个素数,重复此过程。
  4. 最后,使用迭代器打印出所有仍然被标记为 true 的索引,它们就是1000以内的素数。

问:如果 set 或 isSet 的索引超出了位集的大小会怎样?

答:在安全编译模式下(Debug, ReleaseSafe),这会触发一个panic,因为索引越界了。这与数组和切片的边界检查行为一致。

问:StaticBitSet 和 DynamicBitSet 该如何选择?

答:选择的依据是你的使用场景中,所需位的数量是否在编译时就已知。

  • 如果位的数量是固定的、编译时已知的(例如,表示ASCII字符集中的字符、一个枚举的所有成员等),StaticBitSet 是最佳选择,因为它没有堆分配,性能最高。
  • 如果位的数量需要在运行时根据输入动态确定,那么你应该使用 DynamicBitSet,并为其提供一个分配器。

今天,我们学习了 std.bit_set,一个用于高效处理大量布尔状态的强大工具。我们了解了静态位集和动态位集的区别,掌握了设置、清除、测试位的基本操作,以及如何进行集合运算和迭代。相比于 []bool,位集在内存使用上有着巨大的优势,是实现诸如权限系统、状态机、集合算法等功能的理想选择。它完美地体现了Zig作为一门系统编程语言,在提供高级抽象的同时,也赋予开发者进行底层优化的能力。

明天,我们将接触Zig最强大的元编程工具:std.meta模块。