引入
今天在使用Integer類的時候點進去看它的源碼,發現了bitCount()這麼一個方法,看它的介紹是用來獲取一個int中二進制位爲1的個數。然後看了它的實現,完全看不懂的節奏啊。
public static int bitCount(int i) {
// HD, Figure 5-2
i = i - ((i >>> 1) & 0x55555555);
i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);
i = (i + (i >>> 4)) & 0x0f0f0f0f;
i = i + (i >>> 8);
i = i + (i >>> 16);
return i & 0x3f;
}
這個方法如果我自己來實現的的話,會是下面這個樣子:
public static int bitCount(int i) {
int num = 0;
for (int j = 0; j < 32; i++) {
num += (i) & 1;
i >>>= 1;
}
return num;
}
爲了搞清楚這個方法背後的原理,查了一些資料,終於搞明白了。
分析
算法思路
這個算法的主要思路是這樣的:
- 求每兩位二進制數中1的個數
- 求每四位二進制數中1的個數
- 依次類推,求到三十二位二進制數中1的個數,就是我們需要的答案
兩位
只有兩位
首先來分析求兩位怎麼求:
兩位二進制數有四種可能,它們所對應的1的個數(在這裏個數也使用二進制來表示)如下所示,
二進制數 | 二進制位爲1的個數 |
---|---|
00 | 00 |
01 | 01 |
10 | 01 |
11 | 10 |
也就是說我們要將這個int值每兩位左列所示的二進制位,變成右列所示的二進制位。
將左列標記爲s,右列標記爲t,則t = s - x,觀察上述表格可以發現x = s >>> 1,即
t = s - (s >>> 1);
s | t | x |
---|---|---|
00 | 00 | 00 |
01 | 01 | 00 |
10 | 01 | 01 |
11 | 10 | 01 |
不止兩位
上述我們討論了只有兩個二進制位的情況,如果對於整個int32位來說,轉化關係是怎樣的呢?
在這之前先來討論4位的情況,舉個例子說吧:
- 現在有0111這個數,我們要求每兩位的1的個數,目標值是0110,如果直接代入上述公式,我們得到的是0100。
上述結果明顯不對,但是哪裏錯了呢?來分析一下:
- 代入公式的運算過程
- 0111 >>> 1 = 0011 ①
- 0111 - 0011 = 0100 ②
- 理想運算過程
- 0111 - 0001 = 0110 ③
對比可知道上面第一步求得的0011是不對的,也就是說我們不能直接右移一位得到減數,那麼應該怎麼做?
仔細分析可以發現,減數之間的區別在於右移時從高位傳到低位的那個1,根據表格我們知道x(x代表減數)的高位不可能是1,而1又會因爲右移從高位傳過來,所以在右移之後,我們需要與上0101來消除這個高位影響。將這個擴展到32位二進制數上,我們就得到公式i = i - ((i >>> 1) & 0x55555555);
。
第一步搞定了。
四位
再來看四位的求法。
這裏我們需要注意一點,此時的二進制位已經不再僅僅代表二進制位,我們應該把兩位二進制位看成一個整體,當成個數來看。也就是說,我們需要對這四位數進行拆分,拆成兩組,每組分別代表這兩位包含的1二進制位的個數。那麼這4個二進制位上面所包含的所有爲1的二進制位的個數爲兩組的和,也就是高位所代表的二進制數與低位所代表的二進制數的和。比如有這樣一個4位的二進制數,abcd,它所包含的1位的個數爲,00ab+00cd,注意前面的兩個0,根據這個原理我們可以得到下表,可以對照着這個表格加深理解,
s | t |
---|---|
0000 | 0000 |
0001 | 0001 |
0010 | 0010 |
0100 | 0001 |
0101 | 0010 |
0110 | 0011 |
1000 | 0010 |
1001 | 0011 |
1010 | 0100 |
我們最終的到的是個四位數,但是我們只需要兩位數進行求和運算,所以爲了消除高位的影響,我們需要與上0011,那麼就得到第二個公式i = (i & 0x33333333) + ((i>>>2) & 0x33333333;
。這裏要講一下,爲什麼0x33333333要&兩次,而不是放在外面一起&。這是因爲00ab&00cd可能會產生進位,這樣的話,如果放在外面與就會消除這個進位,使結果不正確。
這一步理解之後,後面就很快了。
八位、十六位、三十二位
這幾個位數與四位時的原理是一樣的。
- 八位:向右移四位,與原數相加,因爲這裏四位中最大的可能數爲0100,產生進位也只是1000,不會影響到前面的4位數,所以八位的時候可以在外面&,得到公式
i = (i + (i >>> 4)) & 0x0f0f0f0f;
- 十六位:
i = (i + (i >>> 8)) & 0x00ff00ff;
- 三十二位:
i = (i + (i >>> 16)) & 0x0000ffff;
我這裏表達形式稍微與源碼有點不一樣,源碼是把消除部分放到了最後,即i = i & 0x3f;
,而省略了十六位和三十二位那裏的與操作,因爲最後面二進制位爲1的個數不可能超過100000這個二進制數,所以這兩步與操作可以直接放到最後用一步來代替,最終我們就得到所有的公式:
i = i - ((i >>> 1) & 0x55555555);
i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);
i = (i + (i >>> 4)) & 0x0f0f0f0f;
i = i + (i >>> 8);
i = i + (i >>> 16);
i = i & 0x3f;
總結
要理解這個算法,主要是要理解兩位數和四位數這裏,後面的幾位數與四位數那個道理是一樣的,其中重點理解與上一個數來消除高位影響。