题目描述:
给你一个整数
n,对于0 <= i <= n中的每个i,计算其二进制表示中1的个数 ,返回一个长度为n + 1的数组ans作为答案。不要使用内置函数来解决(例如,C++ 中的
__builtin_popcount)。示例 1:
输入:n = 2 输出:[0,1,1] 解释: 0 --> 0 1 --> 1 2 --> 10示例 2:
输入:n = 5 输出:[0,1,1,2,1,2] 解释: 0 --> 0 1 --> 1 2 --> 10 3 --> 11 4 --> 100 5 --> 101进阶:
- 很容易就能实现时间复杂度为
O(n log n)的解决方案,你可以在线性时间复杂度O(n)内用一趟扫描解决此问题吗?
解题思路:
方法一:动态规划(最低有效位)
核心思路:
对于任意整数 i:
i 的二进制中 1 的个数 = (i >> 1) 的二进制中 1 的个数 + (i & 1)
因为 i >> 1 是 i 去掉最低位,i & 1 是最低位是否为 1。
代码实现:
cpp
class Solution {
public:
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;
}
};
具体过程示例:
n = 5
| i | 二进制 | i>>1 | i&1 | dpi |
|---|---|---|---|---|
| 0 | 0 | - | - | 0 |
| 1 | 1 | 0 | 1 | dp0+1=1 |
| 2 | 10 | 1 | 0 | dp1+0=1 |
| 3 | 11 | 1 | 1 | dp1+1=2 |
| 4 | 100 | 2 | 0 | dp2+0=1 |
| 5 | 101 | 2 | 1 | dp2+1=2 |
结果:[0,1,1,2,1,2] ✅
复杂度分析:
| 维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(n) | 一次遍历 |
| 空间复杂度 | O(n) | dp 数组(不算输出的话是 O(1)) |
方法二:动态规划(最高有效位)
核心思路:
找到小于等于 i 的最大 2 的幂 highBit:
dp[i] = dp[i - highBit] + 1
因为 i 比 i - highBit 多了一个最高位的 1。
代码实现:
cpp
class Solution {
public:
vector<int> countBits(int n) {
vector<int> dp(n + 1, 0);
int highBit = 0;
for (int i = 1; i <= n; i++) {
if ((i & (i - 1)) == 0) {
highBit = i; // i 是 2 的幂
}
dp[i] = dp[i - highBit] + 1;
}
return dp;
}
};
具体过程示例:
n = 5
| i | highBit | dpi |
|---|---|---|
| 0 | - | 0 |
| 1 | 1 | dp0+1=1 |
| 2 | 2 | dp0+1=1 |
| 3 | 2 | dp1+1=2 |
| 4 | 4 | dp0+1=1 |
| 5 | 4 | dp1+1=2 |
结果:[0,1,1,2,1,2] ✅
复杂度分析:
| 维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(n) | 一次遍历 |
| 空间复杂度 | O(n) | dp 数组 |
方法三:动态规划(最低设置位)
核心思路:
dp[i] = dp[i & (i - 1)] + 1
因为 i & (i - 1) 是 i 去掉最低位的 1,所以 dp[i] 比它多 1。
代码实现:
cpp
class Solution {
public:
vector<int> countBits(int n) {
vector<int> dp(n + 1, 0);
for (int i = 1; i <= n; i++) {
dp[i] = dp[i & (i - 1)] + 1;
}
return dp;
}
};
具体过程示例:
n = 5
| i | i&(i-1) | dpi |
|---|---|---|
| 0 | - | 0 |
| 1 | 0 | dp0+1=1 |
| 2 | 0 | dp0+1=1 |
| 3 | 2 | dp2+1=2 |
| 4 | 0 | dp0+1=1 |
| 5 | 4 | dp4+1=2 |
结果:[0,1,1,2,1,2] ✅
复杂度分析:
| 维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(n) | 一次遍历 |
| 空间复杂度 | O(n) | dp 数组 |
方法四:暴力法(O(n log n))
核心思路:
对 0 到 n 中的每一个数 ,单独统计它二进制表示中 1 的个数。
统计单个数的 1 的个数:
对于一个整数 num:
-
看它的最低位是不是
1:num & 1 -
把
num右移一位:num >>= 1 -
重复上述过程,直到
num == 0
例子 :num = 5(二进制 101)
| 步骤 | num | num & 1 | count |
|---|---|---|---|
| 1 | 101 | 1 | 1 |
| 2 | 10 | 0 | 1 |
| 3 | 1 | 1 | 2 |
| 4 | 0 | - | 结束 |
结果:count = 2
代码实现:
cpp
class Solution {
public:
vector<int> countBits(int n) {
vector<int> result(n + 1, 0);
for (int i = 1; i <= n; i++) {
int count = 0;
int num = i;
while (num > 0) {
count += num & 1;
num >>= 1;
}
result[i] = count;
}
return result;
}
};
具体过程示例:
n = 5
| i | 二进制 | 统计过程 | count |
|---|---|---|---|
| 0 | 0 | - | 0 |
| 1 | 1 | 1&1=1 → 0 | 1 |
| 2 | 10 | 10&1=0 → 1&1=1 → 0 | 1 |
| 3 | 11 | 11&1=1 → 1&1=1 → 0 | 2 |
| 4 | 100 | 100&1=0 → 10&1=0 → 1&1=1 → 0 | 1 |
| 5 | 101 | 101&1=1 → 10&1=0 → 1&1=1 → 0 | 2 |
结果:[0,1,1,2,1,2] ✅
复杂度分析:
| 维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(n log n) | 每个数最多遍历 log n 位 |
| 空间复杂度 | O(n) | 结果数组 |
缺点:不是最优,但容易理解。
四种方法对比:
| 方法 | 时间复杂度 | 空间复杂度 | 推荐度 |
|---|---|---|---|
| 最低有效位 | O(n) | O(n) | ⭐⭐⭐⭐⭐ |
| 最高有效位 | O(n) | O(n) | ⭐⭐⭐⭐ |
| 最低设置位 | O(n) | O(n) | ⭐⭐⭐⭐⭐ |
| 暴力法 | O(n log n) | O(n) | ⭐⭐ |
总结:
| 要点 | 说明 |
|---|---|
| 核心思想 | 利用 i 和 i>>1 或 i&(i-1) 的关系递推 |
| 最优解法 | 最低有效位 / 最低设置位,时间 O(n) |
| 关键公式 | dp[i] = dp[i>>1] + (i&1) |