1. 程式人生 > >c語言:統計整數二進位制表示中1的個數(漢明重量)

c語言:統計整數二進位制表示中1的個數(漢明重量)

問題描述:對於一個位元組的無符號整型變數,求其二進位制表示中1的個數。

第一次見到這個問題應該是icephone第一次例會的時候,問題雖然簡單,但也值得深思。

後來查閱資料的時候才知道這個問題有個正式的名字叫Hamming_weight,也被一些公司當做面試題。

下面通過幾個不同階段的演算法,談談這個問題。

一、逐個數

剛剛接觸這個問題的時候是上學期吧,大一,還剛接觸軟體工程,接觸c語言,對一些問題的看法也比較單純。

那時候,就想著純粹的一個個數來著,宣告一個計數變數,滿足條件(尾數是1),就加一,然後 / 2(二進位制),直到該數為0為止。

當然,就可行性來說,這樣的演算法完全沒有問題。簡單,明瞭。

下面給出具體程式碼:

#include<stdio.h>
//Hamming_weight演算法1---逐個數
int Hamming_weight_1( int n )
{
    int count_ = 0; //宣告計數變數

    while ( n != 0 )  //遍歷
    {
        if( n%2 == 1 ) //滿足尾數是1.
            count_++;
        n /= 2;        //除2,右移一位。(二進位制)
    }
    return count_;
}

int main()
{
    int n;
    while ( scanf("%d", &n) != EOF )  //讀入整數和列印1的個數
    {
        printf("%d \n", Hamming_weight_1( n ));
    }
    return 0;
}

上面的這種演算法很常規,也很簡單,就不多做說明。

因為今天的主角是位運算,所以相應的,給出位運算的版本,雖然比較簡單,但是還是寫出來,便於做比較。

#include<stdio.h>
//Hamming_weight演算法1---逐個數(位運算)
int Hamming_weight_1( int n )
{
    int count_ = 0; //宣告計數變數

    while ( n != 0 )  //遍歷
    {
        count_ += n & 1; //尾數按位與1
        n >>= 1;   // 右移一位
    }
    return count_;
}

int main()
{
    int n;
    while ( scanf("%d", &n) != EOF )  //讀入整數和列印1的個數
    {
        printf("%d \n", Hamming_weight_1( n ));
    }
    return 0;
}
好了,現在分析下逐個數演算法的效能。不難看出,對於任何一個整數n(對應的二進位制數有),它都要進行次判斷。可以說,演算法效率比較低,每一位都進行了判斷。

當然,我們不能排斥效率低的演算法,任何演算法,沒有絕對的優越,都是在比較中體現。

下面談談另外一種位運算,也比較易懂。

二、number&= number-1 -----只與二進位制中1的位數相關的演算法

逐個數的方法效率是比較低下的,因為它把每一位都考慮進去了,沒有進行篩選,一個勁的蠻幹。

現在,我們可以考慮每次找到從最低位開始遇到的第一個1,計數,再把它清零,清零的位運算操作是與一個零(任何數與零都等於零)。

但是在有1的這一位與零的操作要同時不影響未統計過的位數和已經統計過的位數,於是可以有這樣一個操作number&= number-1。

這個操作對比當前操作位高的位沒有影響,對低位則完全清零。

拿6(110)來做例子,

第一次 110&101=100,這次操作成功的把從低位起第一個1消掉了,同時計數器加1。

第二次100&011=000,同理又統計了高位的一個1,此時n已變為0,不需要再繼續了,於是110中有2個1。

下面先看程式碼,一會再舉例說明。

#include<stdio.h>
//Hamming_weight演算法二---只考慮1的位數
int Hamming_weight_2( int number )
{
    int count_ = 0; //宣告計數變數

    while ( number != 0 )  //遍歷
    {
        number &= number-1;
        count_ ++;
    }
    return count_;
}

int main()
{
    int n;
    while ( scanf("%d", &n) != EOF )  //讀入整數和列印1的個數
    {
        printf("%d \n", Hamming_weight_2( n ));
    }
    return 0;
}

這裡,關鍵是:number &=(number-1),也是巧妙所在。

精髓就是:這個操作對比當前操作位高的位沒有影響,對低位則完全清零。

[ 2. 判斷一個數是否是2的方冪
n > 0 && ((n & (n - 1)) == 0 ) ]

看完程式碼,再舉一例、

拿7(111)來做例子,

第一次 111&110=110,這次操作成功的把從低位起第一個1消掉了,同時計數器加1。

第二次110&101=100,同理又統計了高位的一個1,同時計數器加1。

第三次100&011=000,同理又統計了高位的一個1,同時計數器加1。

此時n已變為0,不需要再繼續了,於是111中有3個1。

相信看完程式碼和例子不難理解了。

以我目前水平,我覺得這個演算法已經很巧妙了。不過,,,看了wikipedia上解的Hamming_weight問題,才知道什麼叫大神...

三、wiki上高效解法。

先給程式碼,,

#include<stdio.h>
//Hamming_weight
int Hamming_weight_3( 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;
}

int main()
{
    int n;
    while ( scanf("%d", &n) != EOF )  //讀入整數和列印1的個數
    {
        printf("%d \n", Hamming_weight_3( n ));
    }
    return 0;
}

說實在,以我個人的能力。

我看這個跟看天書一樣,完全不懂。

下面通過學習過程,附帶一些大牛的講解,來解釋下。

說簡單點,就是一個 錯位分段相加,然後遞迴合併的過程

下面是細節分析:

首先先看看那些詭異的數字都有什麼特點:
0x5555……這個換成二進位制之後就是0101010101010101……
0x3333……這個換成二進位制之後就是0011001100110011……
0x0f0f……...這個換成二進位制之後就是0000111100001111……
看出來點什麼了嗎?
如果把這些二進位制序列看作一個迴圈的週期序列的話,

那麼第一個序列的週期是2,每個週期是01,第二個序列的週期是4,每個週期是0011,第三個的週期是8,每個是00001111……

這樣的話,我們可以看看如果一個數和這些玩意相與之後的結果:

整個數按照上述的週期被分成了n段,每段裡面的前半截都被清零,後半截保留了資料。不同在於這些數分段的長度是2倍增長的。於是我們可以姑且命名它們為“分段擷取常數”。

這樣,如果我們按照分段的思想,每個週期分成一段的話,你或許就可以感覺到這個分段是二分法的倒過來——類似二段合併一樣的東西!


現在回頭來看問題,我們要求的是1的個數。這就要有一個清點並相加的過程(查表法除外)。使用&運算和移位運算可以幫我們找到1,但是卻無法計算1的個數,需要由加法來完成。最傳統的逐位查詢並相加,每次只加了1位,顯然比較浪費,我們能否一次用加法來計算多次的位數呢?

再考慮問題,找到了1的位置,如何把這個位置變成數量。最簡單的情況,一個2位的數,比如11,只要把它的第二位和第一位相加,不就得到了1的個數了嗎?!所以對於2位的x,有x中1的個數=(x>>1)+(x&1)。是不是和上面的式子有點像?

再考慮稍複雜的,一個位元組內的情況。
一個位元組的x,顯然不能用(x>>1)+(x&1)的方法來完成,但是我們受到了啟發,如果把x分段相加呢?把x分成4個2位的段,然後相加,就會產生4個2位的數,每個都代表了x對應2位地方的1的個數。

所以,該解法的核心如下:

對於n位二進位制數,最多有n個1,而n必定能由n位二進位制數來表示,因此我們在求出某k位中1的個數後,可以將結果直接儲存在這k位中,不需要額外的空間。
以4位二進位制數abcd為例,最終結果是a+b+c+d,迴圈的話需要4步加法

那麼我們讓abcd相鄰的兩個數相加,也就是 a+b+c+d=[a+b]+[c+d]

[0 b 0 d]

[0 a 0 c]

------------

[e f] [ g h]

ef=a+b   gh=c+d

 而 0b0d=(abcd)&0101,0a0c=(abcd)>>1 &0101

[ef]  [gh]再相鄰的兩組相加

[00 ef]

    [gh]

----------

i j k l

ijkl=ef+gh  gh=(efgh)& 0011 ,ef=(efgh)>>2 & 0011

依次入次遞推。需要log(N)次


下面通過具體的例子再說明一下。
例子,若求156中1的個數,156二進位制是10011100
最終:

[1][0][0][1][1][1][0][0] //初始,每一位是一組
---
|0  0 |0  1 |0  1 |0  0|  //與01010101相與的結果,同時2個一組分組
+
|0  1 |0  0 |0  1 |0  0|  //右移一位後與01010101相與的結果
=
[0  1][0  1][1  0][0  0]  //相加完畢後,現在每2位是一組,每一組儲存的都是最初在這2位的1的個數
----
|0  0  0  1 |0  0  0  0|  //與00110011相與的結果,4個一組分組
+
|0  0  0  1 |0  0  1  0|  //右移兩位後與00110011相與的結果
=
[0  0  1  0][0  0  1  0] //相加完畢後,現在每4位是一組,並且每組儲存的都是最初這4位的1的個數
----
|0  0  0  0  0  0  1  0|
+
|0  0  0  0  0  0  1  0|
=
[0  0  0  0  0  1  0  0] //最終合併為8位1組,儲存的是整個數中1的個數,即4。


比如這個例子,143的二進位制表示是10001111,這裡只有8位,高位的0怎麼進行與的位運算也是0,所以只考慮低位的運算,按照這個演算法走一次

+---+---+---+---+---+---+---+---+
| 1 | 0 | 0 | 0 | 1 | 1 | 1 | 1 |   <---143
+---+---+---+---+---+---+---+---+
|  0 1  |  0 0  |  1 0  |  1 0  |   <---第一次運算後
+-------+-------+-------+-------+
|    0 0 0 1    |    0 1 0 0    |   <---第二次運算後
+---------------+---------------+
|        0 0 0 0 0 1 0 1        |   <---第三次運算後,得數為5
+-------------------------------+

這裡運用了分治的思想,先計算每對相鄰的2位中有幾個1,再計算每相鄰的4位中有幾個1,下來8位,16位,32位,因為2^5=32,所以對於32位的機器,5條位運算語句就夠了。

像這裡第二行第一個格子中,01就表示前兩位有1個1,00表示下來的兩位中沒有1,其實同理。再下來01+00=0001表示前四位中有1個1,同樣的10+10=0100表示低四位中有4個1,最後一步0001+0100=00000101表示整個8位中有5個1。



再舉一個例子:(來源:維基百科)

例如,要計算二進位制數 A=0110110010111010 中 1 的個數,這些運算可以表示為:

符號二進位制十進位制註釋
A0110110010111010原始資料
B = A & 01 01 01 01 01 01 01 0101 00 01 00 00 01 00 001,0,1,0,0,1,0,0A 隔一位檢驗
C = (A >> 1) & 01 01 01 01 01 01 01 0100 01 01 00 01 01 01 010,1,1,0,1,1,1,1A 中剩餘的資料位
D = B + C01 01 10 00 01 10 01 011,1,2,0,1,2,1,1A 中每個雙位段中 1 的個數列表
E = D & 0011 0011 0011 00110001 0000 0010 00011,0,2,1D 中資料隔一位檢驗
F = (D >> 2) & 0011 0011 0011 00110001 0010 0001 00011,2,1,1D 中剩餘資料的計算
G = E + F0010 0010 0011 00102,2,3,2A 中 4 位資料段中 1 的個數列表
H = G & 00001111 0000111100000010 000000102,2G 中資料隔一位檢驗
I = (G >> 4) & 00001111 0000111100000010 000000112,3G 中剩餘資料的計算
J = H + I00000100 000001014,5A 中 8 位資料段中 1 的個數列表
K = J & 000000001111111100000000000001015J 中隔一位檢驗
L = (J >> 8) & 000000001111111100000000000001004J 中剩餘資料的檢驗
M = K + L00000000000010019最終答案

最後,給出維基百科上面該問題的逐步優化過程..反正我是看不懂了。

做個標記,哪天有興趣,有能力了再來瞅瞅

//types and constants used in the functions below
 
typedef unsigned __int64 uint64;  //assume this gives 64-bits
const uint64 m1  = 0x5555555555555555; //binary: 0101...
const uint64 m2  = 0x3333333333333333; //binary: 00110011..
const uint64 m4  = 0x0f0f0f0f0f0f0f0f; //binary:  4 zeros,  4 ones ...
const uint64 m8  = 0x00ff00ff00ff00ff; //binary:  8 zeros,  8 ones ...
const uint64 m16 = 0x0000ffff0000ffff; //binary: 16 zeros, 16 ones ...
const uint64 m32 = 0x00000000ffffffff; //binary: 32 zeros, 32 ones ...
const uint64 hff = 0xffffffffffffffff; //binary: all ones
const uint64 h01 = 0x0101010101010101; //the sum of 256 to the power of 0,1,2,3...
 
//This is a naive implementation, shown for comparison,
//and to help in understanding the better functions.
//It uses 24 arithmetic operations (shift, add, and).
int popcount_1(uint64 x) {
    x = (x & m1 ) + ((x >>  1) & m1 ); //put count of each  2 bits into those  2 bits 
    x = (x & m2 ) + ((x >>  2) & m2 ); //put count of each  4 bits into those  4 bits 
    x = (x & m4 ) + ((x >>  4) & m4 ); //put count of each  8 bits into those  8 bits 
    x = (x & m8 ) + ((x >>  8) & m8 ); //put count of each 16 bits into those 16 bits 
    x = (x & m16) + ((x >> 16) & m16); //put count of each 32 bits into those 32 bits 
    x = (x & m32) + ((x >> 32) & m32); //put count of each 64 bits into those 64 bits 
    return x;
}
 
//This uses fewer arithmetic operations than any other known  
//implementation on machines with slow multiplication.
//It uses 17 arithmetic operations.
int popcount_2(uint64 x) {
    x -= (x >> 1) & m1;             //put count of each 2 bits into those 2 bits
    x = (x & m2) + ((x >> 2) & m2); //put count of each 4 bits into those 4 bits 
    x = (x + (x >> 4)) & m4;        //put count of each 8 bits into those 8 bits 
    x += x >>  8;  //put count of each 16 bits into their lowest 8 bits
    x += x >> 16;  //put count of each 32 bits into their lowest 8 bits
    x += x >> 32;  //put count of each 64 bits into their lowest 8 bits
    return x &0xff;
}
 
//This uses fewer arithmetic operations than any other known  
//implementation on machines with fast multiplication.
//It uses 12 arithmetic operations, one of which is a multiply.
int popcount_3(uint64 x) {
    x -= (x >> 1) & m1;             //put count of each 2 bits into those 2 bits
    x = (x & m2) + ((x >> 2) & m2); //put count of each 4 bits into those 4 bits 
    x = (x + (x >> 4)) & m4;        //put count of each 8 bits into those 8 bits 
    return (x * h01)>>56;  //returns left 8 bits of x + (x<<8) + (x<<16) + (x<<24) + ... 
}

在最壞的情況下,上面的實現是所有已知演算法中表現最好的。但是,如果已知大多數資料位是 0 的話,那麼還有更快的演算法。這些更快的演算法是基於這樣一種事實即 X 與 X-1 相與得到的最低位永遠是 0。例如:

ExpressionValue
X0 1 0 0 0 1 0 0 0 1 0 0 0 0
X-10 1 0 0 0 1 0 0 0 0 1 1 1 1
X & (X-1)0 1 0 0 0 1 0 0 0 0 0 0 0 0

減 1 操作將最右邊的符號從 0 變到 1,從 1 變到 0,與操作將會移除最右端的 1。如果最初 X 有 N 個 1,那麼經過 N 次這樣的迭代運算,X 將減到 0。下面的演算法就是根據這個原理實現的。

//This is better when most bits in x are 0
//It uses 3 arithmetic operations and one comparison/branch per "1" bit in x.
int popcount_4(uint64 x) {
    uint64 count;
    for (count=0; x; count++)
        x &= x-1;
    return count;
}
 
//This is better if most bits in x are 0.
//It uses 2 arithmetic operations and one comparison/branch  per "1" bit in x.
//It is the same as the previous function, but with the loop unrolled.
#define f(y) if ((x &= x-1) == 0) return y;
int popcount_5(uint64 x) {
    if (x == 0) return 0;
    f( 1) f( 2) f( 3) f( 4) f( 5) f( 6) f( 7) f( 8)
    f( 9) f(10) f(11) f(12) f(13) f(14) f(15) f(16)
    f(17) f(18) f(19) f(20) f(21) f(22) f(23) f(24)
    f(25) f(26) f(27) f(28) f(29) f(30) f(31) f(32)
    f(33) f(34) f(35) f(36) f(37) f(38) f(39) f(40)
    f(41) f(42) f(43) f(44) f(45) f(46) f(47) f(48)
    f(49) f(50) f(51) f(52) f(53) f(54) f(55) f(56)
    f(57) f(58) f(59) f(60) f(61) f(62) f(63)
    return 64;
}
 
//Use this instead if most bits in x are 1 instead of 0
#define f(y) if ((x |= x+1) == hff) return 64-y;