今天在聽 吳軍的谷歌方法論的時候,吳軍講解了關於 "求二進制數中1的個數" 的一些內容。html
因而搜索了下文章,發現有蠻多的方式,如下內容爲對我看到的一篇文章的一些難點的補充(由於原文寫的比較簡潔,有些我一時也沒能看懂): 算法
算法-求二進制數中1的個數: http://www.cnblogs.com/graphics/archive/2010/06/21/1752421.html緩存
如下的代碼也都爲引用該文章:學習
一、普通法spa
int BitCount(unsigned int n) { unsigned int c =0 ; // 計數器 while (n >0) { if((n &1) ==1) // 當前位是1 ++c ; // 計數器加1 n >>=1 ; // 移位 } return c ; }
普通法就不要說明了,估計是針對該問題頭腦中蹦出的第一個算法。3d
二、快速法code
int BitCount2(unsigned int n) { unsigned int c =0 ; for (c =0; n; ++c) { n &= (n -1) ; // 清除最低位的1 } return c ; }
快速法的特色是使用了一個清除最右側1的技巧,該算法運算次數與n中一個個數直接相關,也就是一算一個準。這裏主要描述「清除最右側1」的原理。htm
假設:數字a= 0b10000(n) 和 數字 b= 0b01111 ,blog
這裏 a是一個2^n的數,也就是1個1 後面接 X個0(X取值 天然數 0,1,2,3...)get
這裏 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的個數了。
三、查表法
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位初始化的,只要效益足夠(這裏吳軍老師在後面分析關於緩存機制上是不太可行的,把一二級緩存的增益給抹掉了)。
四、平行算法
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位的結果
......
五、完美法(原文做者認爲的完美)
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進制上的一些技巧,值得咱們好好學習。