比特位计数 图解题解
这道题到底在问什么
- 输入
- n = 10
- 输出
- [0,1,1,2,1,2,2,3,1,2,2]
最优解:为什么这么做
一句话答案:LeetCode 338 比特位计数的最优解是一维动态规划:dp[i] = dp[i >> 1] + (i & 1)。i 右移一位(去掉最低位)后的 1 的个数早已算好,补上最低位是否为 1 即可,每个数 O(1) 转移,整体时间 O(n);除必须输出的答案数组外只用常数空间。
比特位计数在问什么
给一个整数 n,要求返回长度为 n+1 的数组,第 i 个元素是 i 的二进制表示里 1 的个数(0 ≤ i ≤ n)。比如 n = 10 时答案是 [0,1,1,2,1,2,2,3,1,2,2]。单独数一个数的 1 谁都会,这道题的考点是:要一口气数 0 到 n 全部的数,能不能让后面的数「白拿」前面已经算好的结果。
为什么逐个数各自数一遍还不够好
直觉解是对每个 i 独立做一次位计数(逐位右移或 Kernighan 消 1),每个数要 O(log i) 步,总共 O(n log n)。慢的根源是重复劳动:数 6(二进制 110)的时候,其实把 3(二进制 11)的 1 又数了一遍——6 的高位部分就是 3。
关键观察由此浮出来:任何数 i 右移一位(i >> 1,即抹掉最低位)得到的那个数比 i 小,它的答案在从小到大填表时早就算好了。i 和 i >> 1 的二进制只差最低那一位,这一位是不是 1,用 i & 1 一步就能测出来。既然大问题的答案等于「已解决的小问题 + 常数补充」,动态规划的结构就齐了。
dp 数组的定义和转移方程为什么成立
定义 dp[i] = 数 i 的二进制中 1 的个数,下标直接就是数值本身,dp[0] = 0 是地基。转移方程 dp[i] = dp[i >> 1] + (i & 1):i 的全部 1 分成两部分——高位部分(就是 i >> 1 的二进制)的 1,加上最低位可能贡献的那一个。偶数最低位是 0,答案与 dp[i >> 1] 相同;奇数最低位是 1,再加一。
正确性的依据是无后效性:i >> 1 严格小于 i(i ≥ 1 时),按 i 从 1 到 n 顺序填表,转移用到的那格永远已经填好,绝无循环依赖。拿 10 验证:10 是 1010,10 >> 1 = 5(101),dp[5] = 2,10 是偶数不加,dp[10] = 2,正确。
另一种转移 dp[i & (i-1)] + 1 是什么原理
同样 O(n) 的还有一种写法:dp[i] = dp[i & (i - 1)] + 1。i & (i - 1) 会精确抹掉 i 最低位的那个 1,所以它的 1 比 i 恰好少一个,且这个数也比 i 小、已经算好。两种转移都体现同一个思想——从 i 身上剥掉一小块位结构,剩下的部分复用已有答案。面试里能随手写出任意一种并解释另一种,位运算功底就立住了。
复杂度与两个容易踩的坑
时间 O(n):n+1 个数每个只做一次右移、一次按位与、一次加法,全是 O(1)。空间上答案数组本身 O(n) 是题目要求的输出,额外空间只有循环变量,O(1)。
两个高频错误:一是把转移里的 i >> 1 顺手写成 i - 1——这道题依赖的是「去掉最低位」的位结构关系,不是「前一个数」的算术关系,i - 1 的 1 的个数和 i 没有固定加减规律;二是数组长度开成 n,忘了答案要覆盖 0 到 n 共 n+1 个数,最后一格越界或缺失。
▶ 动画逐步走查(共 24 步)——想跟着动画一帧帧对照就展开
- 3记住这条转移式:去掉最低位(右移一位)的答案早算好了,补上最低位是不是 1 就行。下面每格都在套它。
- 4列头是下标 i(0 到 10),下行 dp[i] 待填。从最左 dp[0] 开始,维度全程不变。
- 50 的二进制就是 0,一个 1 都没有:dp[0]=0。这是地基。
- 6算 dp[1]:1 的二进制是 1,去掉最低位变成 0(=0,就是 1>>1)。蓝格 dp[0]=0 早填好了。
- 71 是奇数,最低位是 1,给 dp[0] 再 +1,得 1 填进 dp[1]。
- 8算 dp[2]:2 的二进制是 10,去掉最低位变成 1(=1,就是 2>>1)。蓝格 dp[1]=1 早填好了。
- 92 是偶数,最低位是 0,跟 dp[1] 一样多,得 1 填进 dp[2]。
- 10算 dp[3]:3 的二进制是 11,去掉最低位变成 1(=1,就是 3>>1)。蓝格 dp[1]=1 早填好了。
- 113 是奇数,最低位是 1,给 dp[1] 再 +1,得 2 填进 dp[3]。
- 12算 dp[4]:4 的二进制是 100,去掉最低位变成 10(=2,就是 4>>1)。蓝格 dp[2]=1 早填好了。
- 134 是偶数,最低位是 0,跟 dp[2] 一样多,得 1 填进 dp[4]。
- 14算 dp[5]:5 的二进制是 101,去掉最低位变成 10(=2,就是 5>>1)。蓝格 dp[2]=1 早填好了。
- 155 是奇数,最低位是 1,给 dp[2] 再 +1,得 2 填进 dp[5]。
- 16算 dp[6]:6 的二进制是 110,去掉最低位变成 11(=3,就是 6>>1)。蓝格 dp[3]=2 早填好了。
- 176 是偶数,最低位是 0,跟 dp[3] 一样多,得 2 填进 dp[6]。
- 18算 dp[7]:7 的二进制是 111,去掉最低位变成 11(=3,就是 7>>1)。蓝格 dp[3]=2 早填好了。
- 197 是奇数,最低位是 1,给 dp[3] 再 +1,得 3 填进 dp[7]。
- 20算 dp[8]:8 的二进制是 1000,去掉最低位变成 100(=4,就是 8>>1)。蓝格 dp[4]=1 早填好了。
- 218 是偶数,最低位是 0,跟 dp[4] 一样多,得 1 填进 dp[8]。
- 22算 dp[9]:9 的二进制是 1001,去掉最低位变成 100(=4,就是 9>>1)。蓝格 dp[4]=1 早填好了。
- 239 是奇数,最低位是 1,给 dp[4] 再 +1,得 2 填进 dp[9]。
- 24算 dp[10]:10 的二进制是 1010,去掉最低位变成 101(=5,就是 10>>1)。蓝格 dp[5]=2 早填好了。
- 2510 是偶数,最低位是 0,跟 dp[5] 一样多,得 2 填进 dp[10]。
- 26整行 dp 就是答案:[0,1,1,2,1,2,2,3,1,2,2]。每格只看了它「右移一位」那格,所以总共只扫一遍。
⚠️ 容易写错的地方
✗ 错:把 i>>1 写成 i-1
✓ 对:来路是 i>>1(右移),不是前一个数
靠的是去掉最低位的关系
✗ 错:漏了 (i&1)
✓ 对:奇数要 +1、偶数 +0
最低位决定差几个 1
✗ 错:数组长度写成 n
✓ 对:要 n+1(含 0~n)
ans 下标到 n
完整代码(Python / C++ / Java)
Python
def countBits(n):
dp = [0] * (n + 1)
for i in range(1, n + 1):
dp[i] = dp[i >> 1] + (i & 1)
return dpC++
vector<int> countBits(int n){
vector<int> dp(n + 1, 0);
for(int i = 1; i <= n; ++i)
dp[i] = dp[i >> 1] + (i & 1);
return dp;
}Java
int[] countBits(int n){
int[] dp = new int[n + 1];
for(int i = 1; i <= n; i++)
dp[i] = dp[i >> 1] + (i & 1);
return dp;
}复杂度
时间
O(n)
每个数 O(1) 转移,一遍扫完
空间
O(1)
除答案数组外只用常数;答案本身 O(n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 比特位计数 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么用 i>>1 而不是 i 的其他子结构?+
i>>1 把 i 的最低位去掉,剩下的高位 1 的个数已经算过;只差最低位一个 bit,所以 +(i&1)即可。
还有别的 O(n) 写法吗?+
可用 dp[i] = dp[i & (i-1)] + 1:i&(i-1) 抹掉最低位的 1,比它多 1 个。两种都 O(n)。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 比特位计数 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。