Brian Kernighan 算法:利用 x & (x - 1) 逐步清除最低位的 1
1. 算法原理
x & (x - 1)这个操作的作用是每次清除x的最低位的1。- 由于 二进制的减法 会影响最低的
1,我们可以利用这一特性不断去除1,直到x变为0,从而统计1的个数。
2. x & (x - 1) 操作的数学推导
对于任意正整数 x,可以用二进制表示:
x = ... 1000 ... 000 x = \text{...} 1000\text{...}000 x=...1000...000
(假设 x 的最低位的 1 出现在某个位置)
当 x - 1 计算时,二进制的减法规则会导致:
- 最低位的
1变为0。 - 它右边的所有
0变为1(因为-1相当于借位操作)。
举例:
x = 101000 // 40
x - 1 = 100111 // 39
x & (x - 1) = 100000 // 结果 32,最低的 1 被清除
可以看到:
x - 1把x最低的1变成0,并把右侧所有0变成1。x & (x - 1)就只保留了x中比最低1更高的位,消除了最低位1。
一般性证明:
设 x 的二进制最低 1 位于 k 位置(从右向左,从 0 开始编号),则:
x = ...1000...000(最低位1在k处)x - 1 = ...0111...111(k位置变成0,右侧全变1)
执行 x & (x - 1):
x & ( x − 1 ) = ( . . . 1000...000 ) & ( . . . 0111...111 ) = ( . . . 0000...000 ) x \& (x - 1) = (...1000...000) \& (...0111...111) = (...0000...000) x&(x−1)=(...1000...000)&(...0111...111)=(...0000...000)
可见,最低的 1 被消除。
3. 代码实现
#include <stdio.h>int count_bits(uint32_t x) {int count = 0;while (x) {x &= (x - 1); // 每次清除最低位的1count++;}return count;
}int main() {uint32_t x = 0b10110101101100011011000101101010; // 示例数据printf("Number of 1s: %d\n", count_bits(x));return 0;
}
4. 代码解析
- 初始化
count = 0,用于存储1的个数。 - 循环执行
x &= (x - 1):- 每次清除
x的最低位1。 count++统计已经清除的1的数量。
- 每次清除
- 循环终止条件是
x == 0,即x所有1都被清除。
5. 运行示例
假设 x = 0b10110101101100011011000101101010(等于 3073836458),二进制如下:
10110101101100011011000101101010
逐步消除最低位的 1:
10110101101100011011000101101010 // x
10110101101100011011000101101001 // x - 1
--------------------------------
10110101101100011011000101101000 // x & (x - 1)10110101101100011011000101101000
10110101101100011011000101100111
--------------------------------
1011010110110001101100010110000010110101101100011011000101100000
10110101101100011011000101011111
--------------------------------
10110101101100011011000101000000...
不断重复,直到 x = 0。
最终 循环运行 19 次,输出:
Number of 1s: 19
6. 时间复杂度分析
假设 x 的位数是 n(32 位),设 x 里有 k 个 1,则:
- 每次操作都减少一个
1,因此总共执行k次操作。 - 最坏情况下
O(n)(x的 32 位全是1,如0xFFFFFFFF)。 - 最好情况下
O(1)(x = 0b10000000000000000000000000000000,只执行一次)。 - 均摊复杂度
O(1),因为实际数据往往1的个数远少于32。
相比逐位检查:
- 逐位检查的复杂度始终是
O(n),即最多执行 32 次。 x & (x - 1)只执行O(k),k通常远小于n,因此速度更快。
7. 适用场景
| 方案 | 复杂度 | 适用情况 |
|---|---|---|
逐位检查(x >> 1) | O(n) | 适用于所有情况,但效率较低 |
| Brian Kernighan | O(k) | 适用于 1 数量较少的情况 |
| POPCNT 指令 | O(1) | 最优解,适用于现代 CPU |
| 查表法 | O(1) | 适用于大批量数据 |
结论:
- 单个整数计算 →
x & (x - 1)是 CPU 无特殊指令时的最优选择。 - 处理大量整数 →
__builtin_popcount(x)或查表法更快。
8. 拓展:其他 x & (x - 1) 的应用
- 判断
x是否是 2 的幂(x只有一个1)
int is_power_of_2(uint32_t x) {return x && !(x & (x - 1));
}
示例:
8 (0b1000) -> x & (x - 1) = 0
10 (0b1010) -> x & (x - 1) = 8 (!= 0)
- 计算
x的最低位1的位置
int lowest_bit_position(uint32_t x) {return x & -x;
}
示例:
x = 40 (0b101000) -> x & -x = 8 (0b1000)
9. 结论
x & (x - 1)是一个强大的二进制操作,可以用来快速统计 1 的个数,适用于 CPU 没有POPCNT指令的情况。- 时间复杂度 O(k),其中
k是1的个数,比逐位检查更快。 - 还能用于判断
x是否是 2 的幂、提取最低位的1等多种用途。
