Ziglings 笔记 98: 位运算实战 - 全字母句判断

26 个开关

在 Ziglings 的第 98 个练习中,我们面对的是一个经典的算法题:Pangram (全字母句)。 全字母句是指包含字母表中所有字母(a-z)至少一次的句子。例如:“The quick brown fox jumps over the lazy dog.”。

通常的解法是开一个 bool seen[26] 数组。但作为系统程序员,我们追求极致的效率。Zig 向我们展示了如何用位运算来优雅地解决这个问题。

挑战:压缩状态

我们需要遍历字符串,记录每个字母是否出现过。 最后,检查是否所有 26 个字母都出现了。

解决方案

我们使用一个 u32 整数,将其看作是 32 个开关的集合。我们只需要用到低 26 个开关。

const std = @import("std");
const ascii = std.ascii;
const print = std.debug.print;

pub fn main() !void {
    print("Is this a pangram? {}!\n", .{isPangram("The quick brown fox jumps over the lazy dog.")});
}

fn isPangram(str: []const u8) bool {
    // 优化:如果长度小于 26,绝对不可能是全字母句
    if (str.len < 26) return false;

    // 状态容器:32位整数,初始化为全 0
    var bits: u32 = 0;

    for (str) |c| {
        // 过滤:只处理字母
        if (ascii.isAscii(c) and ascii.isAlphabetic(c)) {
            // 核心逻辑:计算偏移量
            // 'a' -> 0, 'b' -> 1 ... 'z' -> 25
            const index = ascii.toLower(c) - 'a';
            
            // 设置位:使用位移和按位或
            // 1 << index 生成掩码,|= 将对应位置 1
            bits |= @as(u32, 1) << @truncate(index);
        }
    }

    // 验证:是否所有 26 个位都为 1?
    // 2^26 - 1 = 0x3FFFFFF
    return bits == 0x3FFFFFF;
}

核心知识点总结

1. 空间效率

  • Bool 数组方案:26 字节(甚至更多,取决于对齐)。
  • 位掩码方案:4 字节(u32)。 在 CPU 缓存极其宝贵的今天,数据越紧凑,缓存命中率越高,程序跑得越快。

2. 位操作的原子性

虽然本题没有涉及并发,但位操作的一个巨大优势是它通常对应单条 CPU 指令。在底层驱动开发中,寄存器的每一位都控制着硬件的不同功能,这种 |= (Set) 和 &= ~ (Clear) 的操作是基本功。

3. 十六进制计算技巧

如何快速写出 0x3FFFFFF? 记住 F 就是 1111 (4 个 bit)。 26 个 bit = 6 个 F (24 bit) + 2 个 bit。 剩下的 2 个 bit 是 11,即 3。 组合起来就是 3 后面跟 6 个 F


后续预告:我们已经通过位运算深入了数据的微观世界。接下来的练习可能会带我们回到宏观世界,探讨 Zig 的 Errors (错误处理) 进阶用法,或者深入标准库的 Sorting (排序) 功能。