题目:
Given a non negative integer number num. For every numbers i in the range 0 ≤ i ≤ num calculate the number of 1's in their binary representation and return them as an array.
Example:
For num = 5
you should return [0,1,1,2,1,2]
.
Follow up:
- It is very easy to come up with a solution with run time O(n*sizeof(integer)). But can you do it in linear timeO(n) /possibly in a single pass?
- Space complexity should be O(n).
- Can you do it like a boss? Do it without using any builtin function like __builtin_popcount in c++ or in any other language.
题目分析:
1、很容易从题干发现,题目要求对每个给出的非负整数num,要求算出从0-num中所有数转化为2进制数后所含数字1的个数;
2、解法一:正如题中所说,利用暴力解法很容易在
O(n*sizeof(integer))内求出答案,即对0 - num这num+1个数,每个数都转化为2进制数后统计其中1的个数就可以了。当然也可以在转化过程中边转化边统计以提高效率,代码将在下面给出;
3、解法二:从解法一的过程中可以看到,对于相邻两个数的二进制转化以及二进制码中1的统计过程,可能只存在一两位的差别,特别是对于大型的数字而言,往往绝大部分的步骤和结果是一模一样的,因此这里肯定存在一定的途径去寻找相邻数变化之中的规律所在。我们可以通过例举一些连续数字对应的二进制码和其中的1的个数统计:
i BinaryCode count
0 0 0
1 1 1
2 10 1
3 11 2
4 100 1
5 101 2
6 110 2
7 111 3
8 1000 1
9 1001 2
.. .. ..
通过观察上面的二进制码序列我们不难发现,序列 [2, 3] 是在序列 [0, 1] 的基础上多了一位1【即 [0, 1] 只有一位二进制码,而 [2, 3] 则是 [0, 1] 在第二位上添加一个1】。同样序列 [4, 5, 6, 7] 也是在序列 [0, 1, 2, 3]的基础上多了一位1【第三位】……以此类推下去便得到了自然数系上从0开始的推导关系。简单来理解,对于count这个参数来说,随着num的增加,count = 0、1、1、2、1、2、2、3、1、2……便可以化为0 | 1 | 1 2 | 1 2 2 3 | 1 2……相信规律已经非常明显了:每段 “ | ” 所夹成的区域当中的序列大小都等于该区域前所有数行程的序列长度,且每个对应的数都增加了1,这也就为解法二的算法提供了理论支撑。
到了具体实现的过程中,仍然有几个细节需要注意:
1)算法实现:由于前0 - num中的num+1个数并不能保证刚好能分成若干个上述区域,因此考虑把这num+1个数分割为能用区域表示的前部分A和剩下的后部分B,A的大小直接用指数参数X = (int)(log(num)/log(2))来表示,即2的指数部分;剩余部分B的直接用num - X来标志;
2)对于后半部分B,处理方法很简单,利用上面分析的方法直接取出从0开始相应个数的计数值+1再push_back即可;
3)对于前半部分A,由于采用2的指数X来表示,因此X同时可看作A已经被分为了A个区域,利用X进行循环即可,对于每一次循环取出已有vector的size在进行循环填充,即每次循环vector容量翻倍。
可以看见算法二对于每个要填充的数的填充值都只进行了常数项的运算,同时也没有申明vector<int> res以外的空间,因此成功地把运算的时间和空间复杂度控制在了理想范围内,满足了题干要求。
解法一代码:
class Solution {
public:
vector<int> countBits(int num) {
vector<int> res;
for(int i = 0; i <= num; i++){
int count = 0;
while(1){
if(i%2 != 0)
count ++;
i /= 2;
if(i == 0)
break;
}
res.push_back(count);
}
return res;
}
};
解法二代码:
#include <math.h>
class Solution {
public:
vector<int> countBits(int num) {
vector<int> res;
res.push_back(0);
if(num == 0)
return res;
int X = (int)(log(num)/log(2));
int rest = num - pow(2, X);
for(int i = 0; i < X; i++){
int size = res.size();
for(int j = 0; j < size; j++)
res.push_back(res[j] + 1);
}
for(int i = 0; i <= rest; i++){
res.push_back(res[i] + 1);
}
return res;
}
};
总结:虽然这道题目的解法二成功地满足了题干和时间和空间复杂度地要求,且空间复杂度基本上被控制到了最小,即只保留了答案容器,但是在时间复杂度地考量上,可以发现对于序列依次的+1过程实际上是有大量重复存在的,比如对于第一个元素0的值,在2、4、8、16、……等位置都被执行了+1的操作并填充,这个+1填充后的序列又被后面的序列再次+1填充;第二个元素的值也同样紧随其后反复被+1计算,实际上前面的很多序列都被执行了此类操作很多次,这是一个改进的途径。但是目前为止我并没有想到如何去解决这个问题,如果日后有更好的方法再来补充说明~