Brian Kernighan 算法是一种高效计算二进制数中 1 的个数 (即 "汉明重量 / Hamming Weight")的经典算法,由计算机科学家 Brian Kernighan(C 语言设计者之一)提出。其核心优势是循环次数仅等于二进制中 1 的个数,比 "逐位遍历所有二进制位" 的暴力法更高效。
算法核心原理
Brian Kernighan 算法的核心依赖一个关键的位运算性质:对对于任意整数 x,令 x=x & (x−1),该运算将 x 的二进制表示的最后一个 1 变成 0,并保持其他位不变。
为什么 n & (n - 1)
能消除最右边的 1?
从二进制减法的逻辑可解释:
- 若
n
的二进制末尾是k
个 0(例如n = 12
,二进制为1100
,末尾 2 个 0):n - 1
会将末尾的k
个 0 全部变为 1,且将最右边的那个 1 变为 0(例如12 - 1 = 11
,二进制为1011
,1011 & 1100 = 1000
)。
- 若
n
的二进制末尾是 1(例如n = 13
,二进制为1101
):n - 1
直接将末尾的 1 变为 0(例如13 - 1 = 12
,二进制为1100
,1100 & 1101 = 1100)。
此时对 n
和 n - 1
做 "按位与(&)" 运算,由于最右边的 1 已被 n - 1
翻转为 0,且其右侧的位全为 1(与 n
的右侧 0 对应),最终结果会精准消除 n
最右边的 1。
示例 :以 n = 13
(二进制 1101
)为例
- 第 1 次:
n = 13 (1101)
,n-1 = 12 (1100)
,n & (n-1) = 12 (1100)
→ 消除最右边的 1(末尾的 1),计数 + 1。 - 第 2 次:
n = 12 (1100)
,n-1 = 11 (1011)
,n & (n-1) = 8 (1000)
→ 消除最右边的 1(第 3 位的 1),计数 + 1。 - 第 3 次:
n = 8 (1000)
,n-1 = 7 (0111)
,n & (n-1) = 0 (0000)
→ 消除最右边的 1(第 4 位的 1),计数 + 1。 - 此时
n = 0
,循环结束,最终计数为 3(13
的二进制确实有 3 个 1)。
算法流程
-
初始化计数 :设置一个计数器
count = 0
,用于记录 1 的个数。 -
循环消除 1 :只要
n != 0
,就执行以下操作:- 计算
n = n & (n - 1)
,消除n
最右边的 1。 - 计数器
count += 1
(每消除一个 1,计数加 1)。
- 计算
-
返回结果 :当
n
变为 0 时,count
即为二进制中 1 的总个数。cppint countOneBits(int n) { int count = 0; while (n) { // 当 n 不为 0 时循环 n &= (n - 1); // 消除最右边的 1 count++; // 计数加 1 } return count; }
简单推理
对于(n = 1,2,3,......)都是最高位是1,其余位都是0的,所以如果 i
& (i - 1) = 0,
则i是2的整数次幂。
方法开拓
如果想要线性的求n个数的二进制1的个数,可以采用动态规划的方法。
比如对于二进制数1010,可以看做1010 = 1000 + 10,我设x=1010,y=1000,z=10,其中z=x-y。
而且不难发现z的二进制中1的个数=y的二进制中1的个数+z的二进制中1的个数,而y是2的n次幂的类型,所有bit1[x] = bit1[x - y] + 1 =bit1[z] + 1;//这里的bit1代表二进制中1的个数。
cpp
class Solution {
public:
vector<int> countBits(int n) {
int level = 0;
vector<int> ans(n+1);
ans[0] = 0;
for(int i = 1;i <= n;++i){
if(!(i & (i - 1))){
level = i;
}
ans[i] = ans[i - level] + 1;
}
return ans;
}
};