問題需求:計算32位整型數中的’1’的個數
思路分析:
1. 整型數 i 的數值,實際上就是各位乘以權重——也就是一個以2爲底的多項式:
因此,要求1的位數,實際上只要將各位消權:i = 所得的係數和就是’1’的個數。
2.對任何自然數n的N次冪,用n-1取模得數爲1,
證明如下:
若:
成立
則:
也成立
故對任意非負整數N,
3.因此,對一個係數爲 的以n爲底的多項式P(N), P(N)%(n-1) = (sum(A_i)) % (n-1)
註釋: (a + b) % p = (a % p + b % p) %p (a * b) % p = (a % p * b % p) % p 如果能保證sum(Ai) |
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);
return (tmp%63);
}
010101010101爲8進制 |
但MIT HAKMEM最終的算法要比上面的代碼更加簡單一些。
爲什麼說上面的式子中不能先把(ti>>k)都先提取出來相加,然後再進行&運算呢?
因爲用&(000001)進行MASK後,產生的有效位只有1位,只要6位數中的’1’個數超過1位,那麼在”先加”的過程中,得數就會從最低位中向上溢出。
但是我們注意到,6位數中最多隻有6個’1’,也就是000110,只需要3位有效位。上面的式子實際上是以1位爲單位提取出’1’的個數再相加求和求出6位中’1’的總個數的,所以用的是&(000001)。如果以3位爲單位算出’1’的個數再進行相加的話,那麼就完全可以先加後MASK。算法如下:
tmp = ( >>2)&(001001) + ( >>1)&(001001) + &(001001)
(tmp + tmp>>3)&(000111)
C代碼:
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);
}
注:代碼中是使用8進制數進行MASK的,11位8進制數爲33位2進制數,多出一位,因此第一位八進制數會把最高位捨去(7->3)以免超出int長度。
從第一個版本到第二個實際上是一個“提取公因式”的過程。用1組+, >>, &運算代替了3組。並且已經提取了”最大公因式”。然而這仍然不是最終的MIT HAKMEM算法,不過已經非常接近了,看看代碼吧。
MIT HAKMEM算法:
C代碼:
int bitcount(unsigned int n)
{
unsigned int tmp;
tmp = n
- ((n >> 1) & 033333333333)
- ((n >> 2) & 011111111111);
tmp = (tmp + (tmp >> 3)) & 030707070707
return (tmp%63);
}
又減少了一組+, >>, &運算。被優化的是3位2進制數“組”內的計算。再回到多項式,一個3位2進制數是4a+2b+c,我們想要求的是a
+b+c,n>>1的結果是2a+b,n>>2的結果是a。
於是: (4a+2b+c) - (2a+b) - (a) = a + b + c
中間的MASK是爲了屏蔽”組間”“串擾”,即屏蔽掉從左邊組的低位移動過來的數。
總結
第一次自己寫算法分析,感覺收穫還是蠻大的。
看算法的時候都是看這個算法是如何工作的,寫的過程則是去模擬發現者的思路,看這個算法是如何被推導出來的,把思路反過來又重新梳理了一遍。
最大的感慨是數學對於算法的重要性。這個算法最開始的思路就是多項式消權,並且貫穿了整個算法推導和優化的過程。而第二步的必要條件則是對取模和冪運算關係的瞭解。優化的第一步用到了“提取公因式”思想,第二步則迴歸到了多項式的基本運算。
其中除了取模和冪運算的知識不算太基礎以外,其他無一例外都是初中以內的數學知識與思想。特別是對2進制數本質的理解,所有人都清楚如何求得一個二進制整型數的數值,但是卻很少有人對其多項式本質始終保持着清晰的認識。
就是這些小得不能再小,基礎得不能再基礎的地方,決定了我們的水平。