题目要求
给定一个正整数 n,返回它的二进制表示中 1 的个数。这个数量也叫 Hamming weight。
LeetCode 提供的方法接口是:
hammingWeight(n: int) -> int
示例 1
输入:n = 11
二进制:1011
输出:3
示例 2
输入:n = 128
二进制:10000000
输出:1
约束
1 <= n <= 2^31 - 1- 输入处于题目给定的非负整数范围内。
虽然当前约束从 1 开始,后面的实现也会自然处理 n = 0,并返回 0。
Step 1:先明确到底在数什么
先看一个不使用十进制表示的小任务:
n = 101100₂
从左到右可以看到三个 1:
1 0 1 1 0 0
^ ^ ^
所以答案是 3。
当前 baseline 是:
先看出整数的二进制表示,再人工统计其中的 1。
这个 baseline 的 break 是:
题目给我们的是整数,不是一串已经展开的二进制字符。人工观察不能直接变成处理任意输入的函数。
现在这一版能做到:
- 知道统计对象是二进制表示中的
1,不是十进制数字中的1。 - 知道
11的答案是3,128的答案是1。 - 知道后续算法必须直接操作整数状态。
它还缺:
- 一个可以运行的整数位检查过程。
Step 2:先从最低位逐位检查
当前 baseline 需要读取一个整数的二进制位。
最低位只有两种可能:
n & 1 == 1:最低位是1。n & 1 == 0:最低位是0。
检查完最低位后,可以执行:
n >>= 1
右移会删除已经检查过的最低位,让下一位成为新的最低位。
先写一个正确版本:
def hamming_weight_by_shift(n: int) -> int:
count = 0
while n:
count += n & 1
n >>= 1
return count
用 101100₂ 检查:
当前 n | n & 1 | count 更新后 | 右移后的 n |
|---|---|---|---|
101100 | 0 | 0 | 10110 |
10110 | 0 | 0 | 1011 |
1011 | 1 | 1 | 101 |
101 | 1 | 2 | 10 |
10 | 0 | 2 | 1 |
1 | 1 | 3 | 0 |
循环 invariant 是:
count是已经从右侧移除的二进制位中1的数量,n保存尚未检查的高位部分。
运行检查:
assert hamming_weight_by_shift(11) == 3
assert hamming_weight_by_shift(128) == 1
assert hamming_weight_by_shift(0) == 0
现在这一版能做到:
- 直接检查整数的每个二进制位。
- 正确统计所有
1。 - 在
n变成0时结束循环。
它还缺:
- 即使某一位是
0,循环仍然要经过它。
Step 3:不要经过零位,直接删除一个 1
看第二个示例:
n = 10000000₂
它只有一个 1,但逐位右移需要执行八轮。
当前 baseline 的 break 是:
工作量取决于二进制总位数,而不是我们真正需要统计的
1的数量。
考虑一个正整数减一时发生的变化。对 101100₂ 来说:
n = 101100
n - 1 = 101011
最低位的那个 1 变成了 0,它右边原来的 0 全部变成了 1。
把两者进行按位与:
101100
& 101011
--------
101000
结果恰好删除了 n 中最低位的一个 1,其他更高位保持不变。因此:
n & (n - 1)
每执行一次,就会减少一个 1。
继续对 101100₂ 实际执行:
101100 -> 101000
101000 -> 100000
100000 -> 000000
一共执行三次,正好等于原数中 1 的数量。
把前一版的“读取最低位再右移”替换为:
n &= n - 1
count += 1
现在这一版能做到:
- 每轮必定删除一个
1。 - 不为两个
1之间的零位单独执行循环。 - 在只含一个
1的128上只执行一轮。
它还缺:
- 把“删除次数就是答案”写成完整 invariant 和 LeetCode 实现。
Step 4:删除多少次,答案就是多少
完整实现是:
class Solution:
def hammingWeight(self, n: int) -> int:
count = 0
while n:
n &= n - 1
count += 1
return count
循环 invariant 是:
每轮结束后,
count等于已经删除的1的数量,n保存原数中尚未删除的1。
初始时没有删除任何 1,所以 count = 0。每轮中 n &= n - 1 恰好删除一个 1,随后 count += 1,invariant 继续成立。
循环结束时 n = 0,说明已经没有未删除的 1。此时 count 就是原数中所有 1 的数量。
运行检查
solution = Solution()
assert solution.hammingWeight(0) == 0
assert solution.hammingWeight(11) == 3
assert solution.hammingWeight(128) == 1
assert solution.hammingWeight(2**31 - 1) == 31
复杂度
设 k 是 n 的二进制表示中 1 的数量:
- 时间复杂度:O(k)。每轮删除一个
1。 - 额外空间复杂度:O(1)。
为什么这里要强调输入范围
Python 的负整数使用概念上的无限符号扩展。对负数不断执行这个循环,不会像有限宽度无符号整数一样自然变成 0。
本题输入是正整数,因此最终代码不需要额外的位宽掩码。若工程输入允许负数,必须先明确 32 位还是 64 位表示,再在边界处进行相应归一化。
常见错误
1. 只背公式,不知道删掉了什么
n & (n - 1) 的关键不是“结果更小”,而是它恰好删除最低位的一个 1。
2. 忘记更新 count
修改 n 只是在删除位;必须同时记录删除次数。
3. 对负数直接套用 Python 循环
这超出了题目输入域,并可能导致循环无法按预期结束。
4. 把逐位右移说成错误方法
右移版本是正确 baseline,只是它的循环次数取决于总位数。优化来自跳过零位,不是修复错误答案。
小结
这道题的推导路线是:
人工数二进制中的 1
-> 用 n & 1 读取最低位并右移
-> 发现零位也会消耗循环
-> 用 n & (n - 1) 每次删除一个 1
-> 删除次数就是答案
下一题 LeetCode 338 Counting Bits 会把这个单个整数的过程扩展到 0..n,并进一步消除不同数字之间的重复计算。