今天在听 吴军的谷歌方法论的时候,吴军讲解了关于 "求二进制数中1的个数" 的一些内容。
于是搜索了下文章,发现有蛮多的方式,以下内容为对我看到的一篇文章的一些难点的补充(因为原文写的比较简洁,有些我一时也没能看懂):
算法-求二进制数中1的个数: http://www.cnblogs.com/graphics/archive/2010/06/21/1752421.html
以下的代码也都为引用该文章:
1、普通法
int BitCount(unsigned int n) { unsigned int c =0 ; // 计数器 while (n >0) { if((n &1) ==1) // 当前位是1 ++c ; // 计数器加1 n >>=1 ; // 移位 } return c ; }
普通法就不要说明了,估计是针对该问题头脑中蹦出的第一个算法。
2、快速法
int BitCount2(unsigned int n) { unsigned int c =0 ; for (c =0; n; ++c) { n &= (n -1) ; // 清除最低位的1 } return c ; }
快速法的特点是使用了一个清除最右侧1的技巧,该算法运算次数与n中一个个数直接相关,也就是一算一个准。这里主要描述“清除最右侧1”的原理。
假设:数字a= 0b10000(n) 和 数字 b= 0b01111 ,
这里 a是一个2^n的数,也就是1个1 后面接 X个0(X取值 自然数 0,1,2,3...)
这里 b = a-1 ,也就是X个1
a&b = 0b00000 ,也就是我们需要用到的 a&(a-1)=0 ,也就是 a的唯一的一个1被消除掉了。
那么任意的一个数字 n (n不等于0)我们都可以取出最后的那位1 和后面的所以0 作为a ,则a不为0,那么n-1的时候a其中的1左侧的值都不会变,只会有a变成a-1,
那么n&n-1的时候,也就是a中的1消除掉的时候。
假设 n=10101011101000= 10101011100000 + 1000
则 n-1 =10101011100000 + 1000 - 1 =10101011100000 + (1000 - 1)
所以n&(n-1)=10101011100000 & 10101011100000 + 1000&(111)=10101011100000 消除掉了最右侧的一个1。同理,当所以1消除完用的次数就是1的个数了。
3、查表法
int BitCount3(unsigned int n) { // 建表 unsigned char BitsSetTable256[256] = {0} ; // 初始化表 for (int i =0; i <256; i++) { BitsSetTable256[i] = (i &1) + BitsSetTable256[i /2]; } unsigned int c =0 ; // 查表 unsigned char* p = (unsigned char*) &n ; c = BitsSetTable256[p[0]] + BitsSetTable256[p[1]] + BitsSetTable256[p[2]] + BitsSetTable256[p[3]]; return c ; }
查表法是一种比较简单的方式,方便理解。而且在当前存储越来越便宜的情况下,是越来越适合使用。
上面例子中主要有两个点:
一个是初始化数据的适合,以为是从小到大开始初始化,合理利用了 n/2的结果来计算n的结果
另一个是因为初始化为255个结果即8bit的结果,所以把数据都拆成8bit来计算,然后再加起来。同理原文中还有一个是4bit计算的,我们甚至可以考虑16位初始化的,只要效益足够(这里吴军老师在后面分析关于缓存机制上是不太可行的,把一二级缓存的增益给抹掉了)。
4、平行算法
int BitCount4(unsigned int n) { n = (n &0x55555555) + ((n >>1) &0x55555555) ; n = (n &0x33333333) + ((n >>2) &0x33333333) ; n = (n &0x0f0f0f0f) + ((n >>4) &0x0f0f0f0f) ; n = (n &0x00ff00ff) + ((n >>8) &0x00ff00ff) ; n = (n &0x0000ffff) + ((n >>16) &0x0000ffff) ; return n ; }
这个算法也是比较有意思,核心思想如上图所示。
这里
0x55555555 = 0b01010101010101010101010101010101
0x33333333 = 0b00110011001100110011001100110011
0x0f0f0f0f = 0b00001111000011110000111100001111
0x00ff00ff = 0b00000000111111110000000011111111
0x0000ffff = 0b00000000000000001111111111111111
我们看到上面16进制数对应的2进制数,就比较了解了。
第一步的效果是通过移位和与操作,把每2位加起来
第二步是把第一步的结果每2个数加起来,为4位的结果
......
5、完美法(原文作者认为的完美)
int BitCount5(unsigned int n) { unsigned int tmp = n - ((n >>1) &033333333333) - ((n >>2) &011111111111); return ((tmp + (tmp >>3)) &030707070707) %63; }
这个算法主要做2步骤,第一步每3位计算1的个数,第二步,把第一步的结果加一起。
第一步的每三位加统计1个方式如下,假设三位分布位a、b、c (0,1)
则,对应位置的值大小为s=a*4 + b *2 + c ,
而 ((n >>1) &033333333333) 的左右为先右移一位再屏蔽最高wei 相当与上面 s1=a*2 + b
而((n >>2) &011111111111) 相当于 s2=a
所以 s-s1-s2=a*4 + b*2 + c - (a*2+b) - a =a + b + c;
也就是达到统计3位的1的个数的结果。
上面的计算看不出来算法是怎么来的,难道是巧合?
其实我们换个写法就看的出来了:
s-s1-s2=a*4 + b*2 + c - (a*2+b) - a =(a*4 - a*2 - a) +(b*2 - b) +c;
同理,假设我们要的是4个位的1的个数,我们可以构造为:
s-s1-s2-s3 =(a*8 - a*4 - a*2 - a) +(b*4- b*2 -b)+(c*2-c) + d;
所以它的关键点还是在于右移动上,把一个 数n (n为2^k)一直减去它所有的右移结果,最后为1 ,如:0b10000-0b1000-0b100-0b10-0b1=1
第二步的技巧有两个,
第一个原文说明很详细还有配图,可参考原文,(tmp + (tmp >>3)) 达到的效果是每两个3位结果相加。
第二个为取模63,这个达到的效果是把上一个的结果(每6位的结果存一个数)累加起来,得到最后的结果。
原理是: 每6位的偏移为2^6倍,也就是我们可以把前面的结果写成 n1 + 2^6 * n2 + 2^6 * 2^6 *n3 .....=n1 + 64 *n2 + 64*64*n3
我们对上面的数字取模
n1 + 64 *n2 + 64*64*n3... (mod 63)
= n1 + (63+1)*n2 +(63+1)(63+1)n3...(mod 63)
= n1 + 63*n2 + n2 + 63 *(63+1)n3 +(63+1)n3 ...(mod 63)
= n1 + n2 + (63+1)n3...(mod 63)
= n1 + n2 + 63*n3 + n3 ...(mod 63)
= n1 + n2 + n3...(mod 63)
这里的关键点还在于,我们输入的数字长度是有限的32位,所以再取模不会有什么影响,最后达到了把每6bit的结果累加的目的。
这其中用到和比较多的2进制上的一些技巧,值得我们好好学习。