解決這個問題的第一想法是一位一位的觀察,判斷是否爲1,是則計數器加一,否則跳到下一位,於是很容易有這樣的程序。
int test(int n)
{
int count=0;
while(n != 0)
{
if(n%2 ==1)
count++;
n /= 2;
}
return count;
}
或者和其等價的位運算版本:
int test(int n)
{
int count=0;
while(n != 0){
count += n&1;
n >>= 1;
}
return count;
}
這樣的方法複雜度爲二進制的位數,即[tex]\log_2n[/tex],於是可是想一下,有沒有隻與二進制中1的位數相關的算法呢。
可以考慮每次找到從最低位開始遇到的第一個1,計數,再把它清零,清零的位運算操作是與一個零,但是在有1的這一位與零的操作要同時不影響未統計過的位數和已經統計過的位數,於是可以有這樣一個操作 n&(n-1) ,這個操作對比當前操作位高的位沒有影響,對低位則完全清零。拿6(110)來做例子,第一次 110&101=100,這次操作成功的把從低位起第一個1消掉了,同時計數器加1,第二次100&011=000,同理又統計了高位的一個1,此時n已變爲0,不需要再繼續了,於是110中有2個1。
代碼如下:
int test(int n)
{
int count=0;
while(n != 0)
{
n &= n-1;
count ++;
}
return count;
}
這幾個方法雖然也用到了位運算,但是並沒有體現其神奇之處,下面這個版本則彰顯位運算的強大能力,若不告訴這個函數的功能,一般一眼看上去是想不到這是做什麼的,這也是wikipedia上給出的計算hamming_weight方法:
int test(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;
}
沒有循環,5個位運算語句,一次搞定。
比如這個例子,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。