题目概述:
题目链接:点我做题
题解
一、暴力算法
老实说,笨比的我第一个想的方法还是这个,对每个数,让它与1按位取与,如果结果是1,那么cnt++,否则cnt不变,然后让这个数向右一一位,循环直到这个数为0为止。
代码:
class Solution {
public:
vector<int> countBits(int n)
{
vector<int> vec(n + 1);
vec[0] = 0;
if (n == 0)
{
return vec;
}
vec[1] = 1;
if (n == 1)
{
return vec;
}
for (int i = 2; i <= n; i++)
{
vec[i] = getbit1cnt(i);
}
return vec;
}
int getbit1cnt(int x)
{
int i = 1;
int cnt = 0;
while (x > 0)
{
if (x & i == i)
{
cnt++;
}
x >>= 1;
}
return cnt;
}
};
时间复杂度:对每个数x,让它向右移一位就相当于x/2,所以得到x的1的个数用的循环,循环到0需要的次数是
l
o
g
x
logx
logx级别的,所以时间复杂度为
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
空间复杂度:
O
(
1
)
O(1)
O(1)
二、动态规划——最高有效位法(未优化的笨比法)
这是我想的第二个方法,我注意到如果x不是偶数,那么x - 1 + 1不用考虑进位的问题,x的1的个数等于x-1的1的个数+1;如果x是偶数,我想的是首先得到它的最高有效位closenum,所谓最高有效位的含义是closenum是2的幂且
c
l
o
s
e
n
u
m
<
=
x
closenum<=x
closenum<=x,那么x的1的个数就等于1+去掉最高有效位后剩的值
(
x
−
c
l
o
s
e
n
u
m
)
(x-closenum)
(x−closenum)的1的个数,由于
x
−
c
l
o
s
e
n
u
m
<
x
x-closenum<x
x−closenum<x,它的1的个数在我们循环过程中已经在前面的循环中算过了,所以可以利用动态规划计算。
&emsp 至于如何得到closenum,从ret = 1开始枚举,每轮ret *= 2,当
r
e
t
>
x
ret > x
ret>x,返回
r
e
t
/
2
ret/2
ret/2.
代码:
class Solution {
public:
vector<int> countBits(int n)
{
vector<int> vec(n + 1);
vec[0] = 0;
if (n == 0)
{
return vec;
}
vec[1] = 1;
if (n == 1)
{
return vec;
}
for (int i = 2; i <= n; i++)
{
if (i % 2 != 0)
{
vec[i] = vec[i - 1] + 1;
}
else
{
//这里的closenum相当于我前面描述的x - closenum
int closestnum = getclosenum(i);
vec[i] = vec[closestnum] + 1;
}
}
return vec;
}
int getclosenum(int i)
{
int tmp = 1;
while (i >= tmp)
{
tmp *= 2;
}
//这里返回的相当于我前面描述的i - closenum
return i - tmp / 2;
}
};
时间复杂度:我为什么说这个是笨比方法呢,首先,我对每个偶数都计算了 c l o s e n u m closenum closenum,徒增时间复杂度,实际上完全没必要啊,只要维护一个当前最大的2的幂次,当i等于这个数时,说明i是2的幂次,其二进制位数为1,并且我们更新这个数,让乘2就行。由于我的笨比,这个时间复杂度显然也是 O ( n l o g n ) O(nlogn) O(nlogn),所以我们改进如下:
class Solution {
public:
vector<int> countBits(int n)
{
vector<int> vec(n + 1);
vec[0] = 0;
if (n == 0)
{
return vec;
}
vec[1] = 1;
if (n == 1)
{
return vec;
}
int close2pow = 2;
for (int i = 2; i <= n; i++)
{
if (i % 2 != 0)
{
vec[i] = vec[i - 1] + 1;
}
else
{
if (i == close2pow)
{
close2pow *= 2;
vec[i] = 1;
}
else
{
//i不是2的幂次时,最高有效位显然是close2pow / 2
//i - close2pow得到剩余部分 已经在前面计算过了
vec[i] = vec[i - close2pow / 2] + 1;
}
}
}
return vec;
}
};
此时时间复杂度被优化到了
O
(
n
)
O(n)
O(n).
空间复杂度:
O
(
1
)
O(1)
O(1)
三、Brian Kernighan算法
注意到一个事实,如果我们把一个整数x的二进制展开写为A 1 B,其中B是N(N>=0)个0,那么x-1可以写为:A 0 C,其中C是N个1,也就是说,这个1是x从低位数的第一个1,由于x和x-1的形态,我们考虑 x & x − 1 = A 0 B x\&x-1 = A 0 B x&x−1=A0B,这样不就让x的最低位1变成0了嘛,不难想想如果一直对x进行这个运算,会把x的每个1都变成0,x变到0时运算的次数就是x的1的个数。
class Solution {
public:
vector<int> countBits(int n)
{
//Brian Kernighan算法
//利用x &= x -1可以让x的最低位的1变0
//x变0的用的次数就是x的1的个数
vector<int> bits(n + 1);
for (int i = 1; i <= n; i++)
{
bits[i] = count(i);
}
return bits;
}
int count(int x)
{
int ret = 0;
while (x > 0)
{
x &= (x - 1);
ret++;
}
return ret;
}
};
时间复杂度:显然最慢的情况就是x每一位都是1,这样就相当于右移x,右移又相当于/2,所以时间复杂度为
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
空间复杂度:
O
(
1
)
O(1)
O(1)
四、动态规划——最高有效位(优化版)
我们得知了
x
&
=
x
−
1
x \&= x-1
x&=x−1可以让x的最低位1变成0,仔细想想,如果x是2的幂次,那么x的二进制表达不就只有一个1嘛,因此可以通过
(
x
&
(
x
−
1
)
)
=
=
0
(x \&(x - 1)) == 0
(x&(x−1))==0来判断x是否为2的幂次(注意==优先级大于&),如果是,就更新最高有效位.然后x的1的个数等于1(最高有效位的1)+(x-最高有效位对应值的1)的个数,这个数显然小于x,肯定已经在前面的循环计算过了。
代码:
class Solution {
public:
vector<int> countBits(int n)
{
vector<int> vec(n + 1);
vec[0] = 0;
if (n == 0)
{
return vec;
}
vec[1] = 1;
if (n == 1)
{
return vec;
}
int highbit = 0;
for (int i = 2; i <= n; i++)
{
if ((i & (i - 1)) == 0)
{
//说明i是2的整数次幂
//更新highbit
highbit = i;
}
//i的1位数等于最大有效位的1加上剩余部分的1(在前面已经计算)
vec[i] = vec[i - highbit] + 1;
}
return vec;
}
};
时间复杂度:只遍历了一遍,显然是
O
(
n
)
O(n)
O(n)
空间复杂度:
O
(
1
)
O(1)
O(1)
五、动态规划——最低位
这个想法也很简单,x的1的个数就等于最低位的1的个数(0或1)+剩余位1的个数,剩余位的值可以用x>>1得到,这个数相当于x/2,我们循环前面一定也已经计算过了,至于最低位是否为1,可以用x & 1得到或者用x%2得到。
class Solution {
public:
vector<int> countBits(int n)
{
//动态规划——最低有效位
//x的1的个数等于x>>1的1的个数加上x的末尾是否为1
//x的末尾是否为1可以用x%2表示 更可以用x & 1计算
//x>>1 和x &1都算过了 因此可以用动态规划
vector<int> bits(n + 1);
for (int i = 0; i <= n; i++)
{
bits[i] = bits[i >> 1] + (i & 1);
}
return bits;
}
};
时间复杂度:
O
(
n
)
O(n)
O(n)
空间复杂度:
O
(
1
)
O(1)
O(1)
六、动态规划——最低1位置
我们假设一个整数x的二进制表示是 A 1 B
其中B 是N(N>=0)个0
这里里头的1就是A从右往左数第一个1
那么x - 1的二进制表达就是A 0 B’
B’ 是N个1
x & (x - 1)就等于A 0 B
就得到了让A的第一个1变成0的数——
x
&
(
x
−
1
)
x\&(x-1)
x&(x−1).
观察上面的
x
&
(
x
−
1
)
x\&(x-1)
x&(x−1)的表达式,不难发现x& (x - 1) < x,x的二进制1的个数等于
x
&
(
x
−
1
)
x \& (x-1)
x&(x−1)的二进制1的个数再+1,所以也可以从头开始动态规划.
代码:
class Solution {
public:
vector<int> countBits(int n)
{
//我们假设一个整数x的二进制表示是 A 1 B
//其中B 是N(N>=0)个0
//这里里头的1就是A从右往左数第一个1
//那么x - 1的二进制表达就是A 0 B'
//B' 是N个1
//x & (x - 1)就等于A 0 B
//就得到了让A的第一个1变成0的数
//不难发现x& (x - 1) < x
//x的二进制1的个数等于x & (x-1)的二进制1的个数再+1
//所以也可以从头开始动态规划
vector<int> bits(n + 1);
for (int i = 1; i <= n; i++)
{
bits[i] = bits[i & (i - 1)] + 1;
}
return bits;
}
};
时间复杂度:
O
(
n
)
O(n)
O(n)
空间复杂度:
O
(
1
)
O(1)
O(1)