在位操作(Bit Manipulation)的领域里,有一些技巧因其简洁和高效,常常被称作「魔法」。Kernighan’s Algorithm 就是其中最令人印象深刻的一个。
它旨在解决一个经典问题:Population Count(又称 Hamming Weight),即:
如何高效地计算一个整数的二进制表示中,有多少个
1?
0x01 朴素解法的局限
最直观的解法是逐位检查。我们通过循环和移位,依次判断每一位是否为 1。
| |
这种方法的缺点显而易见:它的执行时间固定为 $O(n)$($n$ 为整数的位数,如 32)。无论这个数是 0 还是 0xFFFFFFFF,它都必须跑完所有循环。即便数字里只有一个 1,我们也浪费了 31 次循环在检查 0 上。
0x02 核心洞察:精准消除
Kernighan’s Algorithm 的核心思想非常性感:跳过所有的 0,每次循环只处理(并消除)一个 1。
实现这一目标的「魔法指令」只有一行:
| |
这一行代码的作用是:将 n 的二进制表示中,最右侧的那个 1 (The Rightmost Set Bit) 变为 0,而保持其他位不变。
为什么是 n & (n - 1)?
我们可以将任意整数 $n$ 的二进制结构拆解为三部分:
- 高位部分:最右侧
1左边的所有位。 - 关键位:最右侧的那个
1。 - 低位部分:关键位右边的所有
0。
即:$n = (\dots\text{High}\dots) \ \mathbf{1} \ (\dots000\dots)$
当我们执行 n - 1 时,借位操作会从最低位一直向左传导,直到遇到那个关键位 1:
- 关键位:被「借位」变成了
0。 - 低位部分:因为借位,原来的
000...全部翻转成了111...。 - 高位部分:不受借位影响,保持不变。
现在,我们将 n 与 n - 1 进行 按位与 (&) 操作:
$$ \begin{array}{rcccc} n & = & \text{High} & \mathbf{1} & 00\dots0 \\ n - 1 & = & \text{High} & \mathbf{0} & 11\dots1 \\ \hline \text{Result} & = & \text{High} & \mathbf{0} & 00\dots0 \end{array} $$
结论:高位保持不变,低位全部归零,唯独最右侧的那个 1 被消除了。
0x03 算法演示
在下方输入任意数字(0-255),观察每次迭代如何精准地摘除一个 set bit。
0x04 复杂度分析
这是 Kernighan’s Algorithm 能够成为经典的原因:
| 特性 | 朴素解法 | Kernighan’s Algorithm |
|---|---|---|
| 循环次数 | 固定 $N$ 次 (32/64) | $k$ 次 ($k$ 为 1 的个数) |
| 时间复杂度 | $O(N)$ | $O(k)$ |
| 优势场景 | 密集型 (全是 1) | 稀疏型 (1 很少) |
在实际应用中,绝大多数整数的 1 都是稀疏分布的。例如对于一个32位整数,如果它只有 3 个 1,该算法只需要运行 3 次循环,性能提升了 10 倍以上。
0x05 代码实现 (Go)
利用这一特性,我们可以写出极简的计数代码。注意循环条件是 n != 0,这意味着一旦所有的 1 被消除,循环立即终止。
| |
其逻辑流程如下图所示:
graph TD
Start((开始)) --> Cond{n != 0 ?}
Cond -- Yes --> Step1["n = n & (n - 1)<br>(消除最右侧 1)"]
Step1 --> Step2[count++]
Step2 --> Cond
Cond -- No --> End((返回 count))
style Step1 fill:#f9f,stroke:#333,stroke-width:2px
0x06 举一反三:更多应用场景
理解了 n & (n - 1) 的本质,我们还可以用它解决更多问题:
1. 判断 2 的幂 (Power of Two)
2 的幂有一个特征:二进制表示中有且仅有一个 1。
这意味着,如果我们消除掉这唯一的 1,结果应该变为 0。
| |
2. 汉明距离 (Hamming Distance)
汉明距离定义为两个数二进制位不同的个数。
我们只需要先做异或 xor = x ^ y(不同的位变为 1),然后对 xor 应用 Kernighan’s Algorithm 即可。
3. 稀疏位图迭代
在处理 BitMap 或 FlagSet 时,如果我们需要遍历所有被置位的标志,使用此算法可以直接跳跃到下一个置位点,而不需要逐个检查每一位,在大规模稀疏数据处理中效率极高。
0x07 总结
虽然在现代 CPU 指令集(如 x86 的 POPCNT)支持下,硬件级的位计数可以达到单指令周期完成,但 Kernighan’s Algorithm 依然是算法面试和跨平台软件实现中的最优解。它展示了位操作最迷人的一面:通过对二进制微观结构的深刻理解,换取宏观性能的极致优化。
