算法-求二進制數中1的個數

算法-求二進制數中1的個數 問題描述面試

任意給定一個32位無符號整數n,求n的二進制表示中1的個數,好比n = 5(0101)時,返回2,n = 15(1111)時,返回4算法

這也是一道比較經典的題目了,相信很多人面試的時候可能遇到過這道題吧,下面介紹了幾種方法來實現這道題,相信不少人可能見過下面的算法,但我相信不多有人見到本文中全部的算法。若是您上頭上有更好的算法,或者本文沒有提到的算法,請不要吝惜您的代碼,分享的時候,也是學習和交流的時候。wordpress

普通法學習

我老是習慣叫普通法,由於我實在找不到一個合適的名字來描述它,其實就是最簡單的方法,有點程序基礎的人都能想獲得,那就是移位+計數,很簡單,很少說了,直接上代碼,這種方法的運算次數與輸入n最高位1的位置有關,最多循環32次。指針

複製代碼 int BitCount(unsigned int n) { unsigned int c =0 ; // 計數器 while (n >0) { if((n &1) ==1) // 當前位是1 ++c ; // 計數器加1 n >>=1 ; // 移位 } return c ; } 複製代碼 一個更精簡的版本以下rest

複製代碼 int BitCount1(unsigned int n) { unsigned int c =0 ; // 計數器 for (c =0; n; n >>=1) // 循環移位 c += n &1 ; // 若是當前位是1,則計數器加1 return c ; } 複製代碼 快速法code

這種方法速度比較快,其運算次數與輸入n的大小無關,只與n中1的個數有關。若是n的二進制表示中有k個1,那麼這個方法只須要循環k次便可。其原理是不斷清除n的二進制表示中最右邊的1,同時累加計數器,直至n爲0,代碼以下blog

複製代碼 int BitCount2(unsigned int n) { unsigned int c =0 ; for (c =0; n; ++c) { n &= (n -1) ; // 清除最低位的1 } return c ; } 複製代碼 爲何n &= (n – 1)能清除最右邊的1呢?由於從二進制的角度講,n至關於在n - 1的最低位加上1。舉個例子,8(1000)= 7(0111)+ 1(0001),因此8 & 7 = (1000)&(0111)= 0(0000),清除了8最右邊的1(其實就是最高位的1,由於8的二進制中只有一個1)。再好比7(0111)= 6(0110)+ 1(0001),因此7 & 6 = (0111)&(0110)= 6(0110),清除了7的二進制表示中最右邊的1(也就是最低位的1)。ip

查表法get

動態建表

因爲表示在程序運行時動態建立的,因此速度上確定會慢一些,把這個版本放在這裏,有兩個緣由

  1. 介紹填表的方法,由於這個方法的確很巧妙。

  2. 類型轉換,這裏不能使用傳統的強制轉換,而是先取地址再轉換成對應的指針類型。也是經常使用的類型轉換方法。

複製代碼 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

1.若是它是偶數,那麼n的二進制中1的個數與n/2中1的個數是相同的,好比4和2的二進制中都有一個1,6和3的二進制中都有兩個1。爲啥?由於n是由n/2左移一位而來,而移位並不會增長1的個數。

2.若是n是奇數,那麼n的二進制中1的個數是n/2中1的個數+1,好比7的二進制中有三個1,7/2 = 3的二進制中有兩個1。爲啥?由於當n是奇數時,n至關於n/2左移一位再加1。

再說一下查表的原理

對於任意一個32位無符號整數,將其分割爲4部分,每部分8bit,對於這四個部分分別求出1的個數,再累加起來便可。而8bit對應2^8 = 256種01組合方式,這也是爲何表的大小爲256的緣由。

注意類型轉換的時候,先取到n的地址,而後轉換爲unsigned char*,這樣一個unsigned int(4 bytes)對應四個unsigned char(1 bytes),分別取出來計算便可。舉個例子吧,以87654321(十六進制)爲例,先寫成二進制形式-8bit一組,共四組,以不一樣顏色區分,這四組中1的個數分別爲4,4,3,2,因此一共是13個1,以下面所示。

10000111 01100101 01000011 00100001 = 4 + 4 + 3 + 2 = 13

靜態表-4bit

原理和8-bit表相同,詳見8-bit表的解釋

複製代碼 int BitCount4(unsigned int n) { unsigned int table[16] = { 0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4 } ;

unsigned int count =0 ;
while (n)
{
    count += table[n &0xf] ;
    n >>=4 ;
}
return count ;

} 複製代碼 靜態表-8bit

首先構造一個包含256個元素的表table,table[i]即i中1的個數,這裏的i是[0-255]之間任意一個值。而後對於任意一個32bit無符號整數n,咱們將其拆分紅四個8bit,而後分別求出每一個8bit中1的個數,再累加求和便可,這裏用移位的方法,每次右移8位,並與0xff相與,取得最低位的8bit,累加後繼續移位,如此往復,直到n爲0。因此對於任意一個32位整數,須要查表4次。以十進制數2882400018爲例,其對應的二進制數爲10101011110011011110111100010010,對應的四次查表過程以下:紅色表示當前8bit,綠色表示右移後高位補零。

第一次(n & 0xff) 10101011110011011110111100010010

第二次((n >> 8) & 0xff) 00000000101010111100110111101111

第三次((n >> 16) & 0xff)00000000000000001010101111001101

第四次((n >> 24) & 0xff)00000000000000000000000010101011

複製代碼 int BitCount7(unsigned int n) { unsigned int table[256] = { 0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8, };

return table[n &0xff] +
    table[(n >>8) &0xff] +
    table[(n >>16) &0xff] +
    table[(n >>24) &0xff] ;

} 複製代碼

固然也能夠搞一個16bit的表,或者更極端一點32bit的表,速度將會更快。

平行算法

網上都這麼叫,我也這麼叫吧,不過話說回來,的確有平行的意味在裏面,先看代碼,稍後解釋

複製代碼 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 ;

} 複製代碼 速度不必定最快,可是想法絕對巧妙。 說一下其中奧妙,其實很簡單,先將n寫成二進制形式,而後相鄰位相加,重複這個過程,直到只剩下一位。

以217(11011001)爲例,有圖有真相,下面的圖足以說明一切了。217的二進制表示中有5個1

完美法

int BitCount5(unsigned int n) { unsigned int tmp = n - ((n >>1) &033333333333) - ((n >>2) &011111111111); return ((tmp + (tmp >>3)) &030707070707) %63; } 最喜歡這個,代碼太簡潔啦,只是有個取模運算,可能速度上慢一些。區區兩行代碼,就能計算出1的個數,到底有何奧妙呢?爲了解釋的清楚一點,我儘可能多說幾句。

第一行代碼的做用

先說明一點,以0開頭的是8進制數,以0x開頭的是十六進制數,上面代碼中使用了三個8進制數。

將n的二進制表示寫出來,而後每3bit分紅一組,求出每一組中1的個數,再表示成二進制的形式。好比n = 50,其二進制表示爲110010,分組後是110和010,這兩組中1的個數本別是2和3。2對應010,3對應011,因此第一行代碼結束後,tmp = 010011,具體是怎麼實現的呢?因爲每組3bit,因此這3bit對應的十進制數都能表示爲2^2 * a + 2^1 * b + c的形式,也就是4a + 2b + c的形式,這裏a,b,c的值爲0或1,若是爲0表示對應的二進制位上是0,若是爲1表示對應的二進制位上是1,因此a + b + c的值也就是4a + 2b + c的二進制數中1的個數了。舉個例子,十進制數6(0110)= 4 * 1 + 2 * 1 + 0,這裏a = 1, b = 1, c = 0, a + b + c = 2,因此6的二進制表示中有兩個1。如今的問題是,如何獲得a + b + c呢?注意位運算中,右移一位至關於除2,就利用這個性質!

4a + 2b + c 右移一位等於2a + b

4a + 2b + c 右移量位等於a

而後作減法

4a + 2b + c –(2a + b) – a = a + b + c,這就是第一行代碼所做的事,明白了吧。

第二行代碼的做用

在第一行的基礎上,將tmp中相鄰的兩組中1的個數累加,因爲累加到過程當中有些組被重複加了一次,因此要捨棄這些多加的部分,這就是&030707070707的做用,又因爲最終結果可能大於63,因此要取模。

須要注意的是,通過第一行代碼後,從右側起,每相鄰的3bit只有四種可能,即000, 001, 010, 011,爲啥呢?由於每3bit中1的個數最多爲3。因此下面的加法中不存在進位的問題,由於3 + 3 = 6,不足8,不會產生進位。

tmp + (tmp >> 3)-這句就是是相鄰組相加,注意會產生重複相加的部分,好比tmp = 659 = 001 010 010 011時,tmp >> 3 = 000 001 010 010,相加得

001 010 010 011

000 001 010 010


001 011 100 101

011 + 101 = 3 + 5 = 8。(感謝網友Di哈指正。)注意,659只是箇中間變量,這個結果不表明659這個數的二進制形式中有8個1。

注意咱們想要的只是第二組和最後一組(綠色部分),而第一組和第三組(紅色部分)屬於重複相加的部分,要消除掉,這就是&030707070707所完成的任務(每隔三位刪除三位),最後爲何還要%63呢?由於上面至關於每次計算相連的6bit中1的個數,最可能是111111 = 77(八進制)= 63(十進制),因此最後要對63取模。

位標誌法

感謝網友 gussing提供

複製代碼 struct _byte { unsigned a:1; unsigned b:1; unsigned c:1; unsigned d:1; unsigned e:1; unsigned f:1; unsigned g:1; unsigned h:1; };

long get_bit_count( unsigned char b ) { struct _byte by = (struct _byte)&b; return (by->a+by->b+by->c+by->d+by->e+by->f+by->g+by->h); } 複製代碼 指令法

感謝網友 Milo Yip提供

使用微軟提供的指令,首先要確保你的CPU支持SSE4指令,用Everest和CPU-Z能夠查看是否支持。

unsigned int n =127 ; unsigned int bitCount = _mm_popcnt_u32(n) ; References

http://gurmeetsingh.wordpress.com/2008/08/05/fast-bit-counting-routines/

做者:zdd 出處:http://www.cnblogs.com/graphics/ 本文版權歸做者和博客園共有,歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接,不然保留追究法律責任的權利.

相關文章
相關標籤/搜索