用归并法统计二进制序列中1的个数

今天遇到一个面试题:

 如何统计一个二进制整数num中1的个数.这里参考了

https://blog.csdn.net/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<

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  

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为机器的整数位长.

 

 

你可能感兴趣的:(算法,面试题目,位运算,统计1的个数)