Java Integer.bitCount()方法解析

引入

今天在使用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;

總結

要理解這個算法,主要是要理解兩位數和四位數這裏,後面的幾位數與四位數那個道理是一樣的,其中重點理解與上一個數來消除高位影響。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章