今天遇到一个面试题:
如何统计一个二进制整数num中1的个数.这里参考了
https://blog.youkuaiyun.com/peiyao456/article/details/51724099
的第4种思路,非常巧妙,这里写一下心得笔记
我们以8位整数为例,
首先输入num可以看成一个二进制序列
num= a1 b1 a2 b2 a3 b3 a4 b4,
可以认为他们自动分成8组,每组长度为1,即
num = {a1},{b1},{a2},{b2},{a3},{b3},{a4},{b4},
每组中的值正好就是本组中1的个数.
然后我们把每2组合并成新的一组(现在每组的长度为2),可以得到
num= {a1,b1}{a2,b2}{a3,b3}{a4,b4}
我们的目的是希望计算新的每一组的和,那么具体步骤是
首先构造一个单组掩码g_mask,长度等于每组的长度,左半部分都为0,右半部分都为1,
也就是g_mask=01,(这个掩码的作用就是每一组的值和它进行与运算之后,只保留第2个元素,左半部分为0!)
然后把每组的g_mask合并起来得到一个总的掩码mask,即
mask=01010101=0x55
然后我们计算num&mask,这一步的目的是把每组左半部分的值为0,
即
num&mask = {0,b1}{0,b2}{0,b3}{0,b4}
然后我们要得到每一组的左半部分,并且要和右半部分对齐,以便相加,实现这一步的技巧为
首先计算 num>>s,其中s为组长的一半,即
num>>s = {a1,a1}{b1,a2}{b2,a3}{b3,a4}
然后再和mask进行与运算,以只保留右半部分,即
(num>>s)&mask = {0,a1}{0,a2}{0,a3}{0,a4}
然后我们计算
num&mask+(num>>s)&mask,这就相当于计算
{0,b1}{0,b2}{0,b3}{0,b4}
+{0,a1}{0,a2}{0,a3}{0,a4}
={a1+b1},{a2+b2},{a3+b3},{a4+b4}
然后对新的数据继续进行两两合并,
每次重新计算
n*=2; //两两合并就意味着每次分组长度扩大一倍,同时分组数目减少一半
s=n/2; //s始终为当前分组长度的一半,这样才能保证每组右移s位之后,左元素和右元素对齐,以便相加
g_mask=1<<s-1; g_mask左边是s个0,右边是s个1
mask=n个g_mask连接起来,总长度为整数位长L
right = num>>mask; //每组只保留右半部分,左半部分为0
left = (num>>s)&mask; //把每组的左半部分移动到右部
num = left+right; //计算每组的和,现在每组中的值都等于原始数据中对应那一组中的1的个数
直到最后只剩一组为止,循环结束.最后得到的结果即为所求
下面是一个具体的计算例子,
输入数据:
a为输入的二进制整数
a=1111 1110 0001 0010 1001 1010 1011 1100
L=整数位数=32
一共18个1,预期输出18.
计算步骤(n和s每次都乘2)
原始分组
num = {1} {1} {1} {1} {1} {1} {1} {0} {0} {0} {0} {1} {0} {0} {1} {0} {1} {0} {0} {1} {1} {0} {1} {0} {1} {0} {1} {1} {1} {1} {0} {0}
step1
将num两两合并得到
{1,1} {1,1} {1,1} {1,0} {0,0} {0,1} {0,0} {1,0} {1,0} {0,1} {1,0} {1,0} {1,0} {1,1} {1,1} {0,0}
现在分组长度n=2,移位长度为s=n/2=1,分组数为16,单组mask为01,
总mask为 16个二进制01即0x5555 5555
right=num&mask的结果为
{0,1} {0,1} {0,1} {0,0} {0,0} {0,1} {0,0} {0,0} {0,0} {0,1} {0,0} {0,0} {0,0} {0,1} {0,1} {0,0}
num>>s的结果为
{0,1} {1,1} {1,1} {1,1} {0,0} {0,0} {1,0} {0,1} {0,1} {0,0} {1,1} {0,1} {0,1} {0,1} {1,1} {1,0}
left=(num>>s) &mask的结果为
{0,1} {0,1} {0,1} {0,1} {0,0} {0,0} {0,0} {0,1} {0,1} {0,0} {0,1} {0,1} {0,1} {0,1} {0,1} {0,0}
left+right的结果为
b={1,0} {1,0} {1,0} {0,1} {0,0} {0,1} {0,0} {0,1} {0,1} {0,1} {0,1} {0,1} {0,1}{1,0} {1,0} {0,0}
step2
继续将b两两合并
得到
b={1,0,1,0} {1,0,0,1} {0,0,0,1} {0,0,0,1} {0,1,0,1} {0,1,0,1} {0,1,1,0} {1,0,0,0}
现在分组长度n=4,移位长度为s=n/2=2,分组数为8,单组mask为0011,
总mask为 8个二进制0011即0x33333333
然后计算 c= b&mask+(b>>s) &mask,得到
c=0100 0011 0001 0001 0010 0010 0011 0010
step3,
现在分组长度n=8,移位长度为s=n/2=4,分组数为4,单组mask为00001111,
总mask为 0x0f0f0f0f
计算d= c&mask+(c>>s)&mask
得到
d=0000 0111 0000 0010 0000 0100 0000 0101
step4.
现在分组长度n=16,移位长度为s=n/2=8,分组数为2,单组mask为0x00ff
总mask为 0x00ff00ff
计算e= d&mask+(d>>s)&mask
得到
e=0000 0000 0000 1001 0000 0000 0000 1001
step 5
现在分组长度n=32,移位长度为s=n/2=16,分组数为1,单组mask为0x0000ffff
总mask为 0x0000ffff
计算f= e&mask+(e>>s)&mask
得到
f=0000 0000 0000 0000 0000 0000 0001 0010
即10进制的18,现在分组数目为1,算法结束,f即为所求结果
下面是上面计算过程的C程序,供读者参考,并自行修改让其适应64位的情况
#include <stdio.h>
void print_bin(int value) {
const int L = sizeof(int) * 8; //得到整数的位数
unsigned int mask =1<<(L-1);
for (int i = 0;i < L;++i, mask >>= 1) {
if (i % 4 == 0 && i != 0)
printf(" ");
printf("%d", value&mask ? 1 : 0);
}
printf("\n");
}
void print_bin(int value,int offset) {
const int L = sizeof(int) * 8; //得到整数的位数
unsigned int mask = 1 << (L - 1);
printf("{");
for (int i = 0;i < L;++i, mask >>= 1) {
if (i%offset==0 && i)
{
printf("} {");
}
if (i % offset )
printf(",");
printf("%d", value&mask ? 1 : 0);
}
printf("}");
printf("\n");
}
int count_ones(unsigned int num)
{
unsigned int m_2 = 0x55555555;
unsigned int m_4 = 0x33333333;
unsigned int m_8 = 0x0f0f0f0f;
unsigned int m_16 = 0x00ff00ff;
unsigned int m_32 = 0x0000ffff;
unsigned int b = (m_2&num) + ((num >> 1)&m_2);
printf("b=");
print_bin(b);
unsigned int c = (m_4&b) + ((b >> 2)&m_4);
printf("c=");
print_bin(c);
unsigned int d = (m_8&c) + ((c >> 4)&m_8);
printf("d=");
print_bin(d);
unsigned int e = (m_16&d) + ((d >> 8)&m_16);
printf("e=");
print_bin(e);
printf("f=");
unsigned int f = (m_32& e) + ((e>>16)&m_32);
print_bin(f);
return f;
}
int main()
{
int a = 0xfe129abc;
printf("a=");
print_bin(a);
int n = count_ones(a);
printf("0x%x has %d ones \n", a,n);
return 0;
}
程序运行结果为
a=1111 1110 0001 0010 1001 1010 1011 1100
b=1010 1001 0001 0001 0101 0101 0110 1000
c=0100 0011 0001 0001 0010 0010 0011 0010
d=0000 0111 0000 0010 0000 0100 0000 0101
e=0000 0000 0000 1001 0000 0000 0000 1001
f=0000 0000 0000 0000 0000 0000 0001 0010
0xfe129abc has 18 ones
显然本算法的复杂度为log(2,L),L为机器的整数位长.