问题:如何计算32位整数中’1‘的个数。
算法理论
思路分析,当我们计算多项式的各个系数的权重和时,例如:
i = A 0 0 0*20 + A 1 1 1*21 + A 2 2 2*22 …A n n n*2n
实际上就是把各个位消权,最后得出
i = A 0 0 0 + A 1 1 1 + A 2 2 2 + A 3 3 3 + … A n n n
而实现的原理就是:对任何n的N次幂,用n-1取模得数为1;证明如下:
设:n(k-1) % (n-1) = 1 成立
则: nk % (n-1) = ( (n-1)*n(k-1) + n(k-1) )%(n-1) = 0 + n(k-1)%(n-1) = 1 也成立
又:n(1-1) % (n-1) = 1 成立
所以,对任意非负整数N,n(k-1) % (n-1) = 1 成立。
因此,对于一个系数为{A i i i}的以n为底的多项式P(N),P(N)%(n-1) = {sumA{i}}%(n-1);如果能保证sum{A{i}} < n-1,则只要用n-1对多项式进行取模即可完成消权,得到系数和。
问题探讨
所以问题就转换为:以2为底的多项式转换为以n为底的多项式,其中只要n足够大,使得n-1 > sum{A{i}成立即可。32位整数中sum{A{i}}位1或者0,其和为32。所以n-1 > 32 ==> n > 33,在二进制中 26=64 > 33,所以我们取6位二进制即可,如下:
i = t 0 0 0*640 + t 1 1 1*641 + t 2 2 2*642 + …t n n n*64n
应用于此问题只需要取如下6个位
000001 (2进制)
然后将我们所需要求的二进制位移位后与其相与并相加。
例如2进制6位数a
a = 110010
(a>>5)&(000001) + (a>>4)&(000001) + (a>>3)&(000001) + (a>>2)&(000001) + (a>>1)&(000001) + a&(000001) = 3
即可求出一个6位,即模为64的数的二进制数中’1‘的个数,以此类推,对于32位数t,求其’1‘的位数的函数如下所示
int bitcount(unsigned int n)
{
unsigned int tmp;
tmp = (n &010101010101)
+((n>>1)&010101010101)
+((n>>2)&010101010101)
+((n>>3)&010101010101)
+((n>>4)&010101010101)
+((n>>5)&010101010101); //其中010101010101等为八进制
return (tmp%63);
}
在上面函数中,求tmp的步骤中,应用的就是上面求6位二进制数a的方法,不过这里求出来的是
a 1 1 1-a 2 2 2-a 3 3 3-a 4 4 4-a 5 5 5-a 6 6 6
上面即64进制数中的第1-6位(中间的‘-’是连接符)这样每个a i i i(i=1,2,3,4,5,6)代表的就是64为底的多项式的系数,结合上面求出的[#算法理论],此时求和只需要对底数64取模,即tmp%63。
算法优化
但是我们注意到,MIT HAKMEM最终的算法要比上面的代码更加简单一些。在上面的计算中,6位数中最多只有6个’1’,也就是000110(二进制),只需要3位有效位即可表示。上面的式子实际上是以1位为单位提取出’1’的个数再相加求和求出6位中’1’的总个数的,所以用的是&(000001)。如果以3位为单位算出’1’的个数再进行相加的话,那么就完全可以先加后与。首先还是以6位进行分析,因为实际只使用了3位存储结果的和:
tmp = (a>>2)&(001001) + (a>>1)&(001001) + a&(001001)
(tmp + tmp>>3)&(000111)
优化代码如下:
int bitcount(unsigned int n)
{
unsigned int tmp;
tmp = (n &011111111111)
+((n>>1)&011111111111)
+((n>>2)&011111111111);
tmp = (tmp + (tmp>>3)) &030707070707;
return (tmp%63);
}
这样的目的是减少计算量,可以看到运算符的数量明显减少了。在上面的式子中,主要解释第二条语句,因为和最初的算法一样,对a进行移位并取求和,但在这次求和中,6位二进制中的低三位和高三位是分开求得的,所以必须将其整合成一个完整的64进制数。使用此优化的代码如下:
int bitcount(unsigned int n)
{
unsigned int tmp;
tmp = n
- ((n >> 1) & 033333333333)
- ((n >> 2) & 011111111111); //其中011111111111等为8进制
tmp = (tmp + (tmp >> 3)) & 030707070707 ;
return (tmp%63);
}
因为11位8进制数为33位2进制数,比32位整型多出一位,因此第一个八进制数会把最高位舍去(7->3)以免超出int长度,即07 => 03。
这次被优化的是3位2进制数“组”内的计算。回到多项式,我们知道一个3位2进制数是4a+2b+c(1<<2 = 4 ; 1<<1 = 2),我们想要求的是a+b+c,n>>1的结果是2a+b,n>>2的结果是a。于是可以得出如下多项式:
(4a+2b+c) - (2a+b) - (a) = a + b + c
上面多项式即是代码中实现的原理。
计算机系统实验datalab-handout中的具体实现
前面一系列的理论推导证明了我们可以得到32位整数中’1’的个数,但在具体完成bitCount实验时发现了一个问题,即上面的代码不能在32位数为负数时生效。且在实验环境中,不能直接用unsigned进行强制类型转换。
- 原理探讨
在上面的所有代码示例中,变量定义为unsigned int类型,即永远为正数,所以代码能正确运行。但是,如果测试用例为负数时,会出现什么问题呢?
这里我们就必须了解C语言中的算术移位了,算术右移是指右移时是最高位补符号位,即为负数时,非符号的最高位补符号位:
例如对一个8位数:10001000>>2
其结果为:11100010;而非00100010
上面的算术右移操作,会增加’1’的数量,因为右移过程中最高位会补‘1’。但如果我们能将算术位移改为逻辑位移,即最高位补0。那么上面的式子还是成立的。逻辑位移实现代码比较简单,就不解释了,代码如下:
int logicalshift(int x, int n)
{
int y = ((~0)>>(32+~n))<<1; //其中 ~n+1 = -1
return (x>>n)&(~y);
}
- 具体代码实现
上面代码中x为32位整数,n为移动的位数。
了解了逻辑补0操作,上面代码可以改为如下形式(可在实验环境中直接运行):
/*
* bitCount - returns count of number of 1's in word
* Examples: bitCount(5) = 2, bitCount(7) = 3
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 40
* Rating: 4
*/
//上面为代码中必须满足的需求
int bitCount(int x) {
int y,n,m,k;
k = ~((~0)<<30); //定义操作数,减少操作符数量
n = (0x49<<24) | (0x24<<16) | (0x92<<8) | 0x49; //八进制011111111111
m = (0xC7<<24) | (0x1C<<16) | (0x71<<8) | 0xC7; //八进制030707070707
y = (x&n) + ((x>>1)&n) + ( (x>>2) & (k) &n);
y = (y + ( (y>>3)&(k>>1) )) & m;
return (y + (y>>6) + (y>>12) + (y>>18) + (y>>24) + ((y>>30)&0x03))&0x3f; //取余操作等价于 y%63
}
为了通过实验测试,代码必须满足许多要求,其中主要是合法操作符类型(例如%用不了)、操作符数量和数据类型等等。但其原理和上面的第二个例子一样,可结合上面第二个代码例子理解。
总结:反正上面的一系列限制导致其代码实现过程非常令人头疼,但其实只要抓住了算术右移这一问题点,将可能导致错误的算术位移点改为逻辑右移,然后再适当减少操作符数量,就可以通过测试
参考文献:
MIT HAKMEM算法分析