求二進制數中 1 的個數
大多數的讀者都會有這樣的反應:這個題目也太簡單了吧,解法似乎也相當地單一,不會有太多的曲折分析或者峰回路轉之處。 那么這個題目考察我們什么呢?事實上,在編寫程序的過程中,根據實際應用的不同,對存儲空間或效率的要求也不一樣。比如在 PC 上的程序編寫與在嵌入式設備上的程序編寫就有很大的差別。我們可以仔細思索一下如何才能使效率盡可能地“高”。
【解法一】 除法、余數
可以舉一個八位的二進制例子來進行分析。對於二進制操作,我們知道,除以一個 2,原來的數字將會減少一個 0。如果除的過程中有余,那么就表示當前位置有一個 1。 以 10 100 010 為例; 第一次除以 2 時,商為1 010 001,余為 0。 第二次除以 2 時,商為101 000,余為 1。 因此,可以考慮利用整型數據除法的特點,通過相除和判斷余數的值來進行分析。於是有了如下的代碼。
{
int num = 0;
while(v)
{
if(v % 2 == 1)
{
num++;
}
v = v/ 2;
}
return num;
}
【解法二】位操作
前面的代碼看起來比較復雜。我們知道,向右移位操作同樣也可以達到相除的目的。唯一不同之處在於,移位之后如何來判斷是否有 1 存在。對於這個問題,再來看看一個八位的數字:10 100 001。 在向右移位的過程中,我們會把最后一位直接丟棄。因此,需要判斷最后一位是否為1,而“與”操作可以達到目的。可以把這個八位的數字與00000001進行“與”操作。如果結果為1,則表示當前八位數的最后一位為1,否則為0。代碼如下:
{
int num = 0;
While(v)
{
num += v & 0x01;
v >>= 1;
}
return num;
}
【解法三】 位操作(修改)
位操作比除、余操作的效率高了很多。但是,即使采用位操作,時間復雜度仍為 O(log2v),log2v 為二進制數的位數。那么,還能不能再降低一些復雜度呢?如果有辦法讓算法的復雜度只與“1”的個數有關,復雜度不就能進一步降低了嗎? 同樣用 10 100 001 來舉例。如果只考慮和 1 的個數相關,那么,我們是否能夠在每次判斷中,僅與 1 來進行判斷呢? 為了簡化這個問題,我們考慮只有一個 1 的情況。例如:01 000 000。 如何判斷給定的二進制數里面有且僅有一個1呢?可以通過判斷這個數是否是2的整數次冪來實現。另外,如果只和這一個“1”進行判斷,如何設計操作呢?我們知道的是,如果進行這個操作,結果為 0 或為1,就可以得到結論。 如果希望操作后的結果為 0,01 000 000 可以和00 111 111 進行“與”操作。 這樣,要進行的操作就是 01 000 000 &(01 000 000 – 00 000 001)= 01 000 000 & 00 111 111 = 0。 因此就有了解法三的代碼:
{
int num = 0;
while(v)
{
v &= (v- 1);
num++;
}
return num;
}
【解法四】分支操作
解法三的復雜度降低到 O(M),其中 M 是v 中1 的個數,可能會有人已經很滿足了,只用計算 1的位數,這樣應該夠快了吧。然而我們說既然只有八位數據,索性直接把 0~255的情況都羅列出來,並使用分支操作,可以得到答案,代碼如下:
{
int num = 0;
switch (v)
{
case 0x0:
num = 0;
break;
case 0x1:
case 0x2:
case 0x4:
case 0x8:
case 0x10:
case 0x20:
case 0x40:
case 0x80:
num = 1;
break;
case 0x3:
case 0x6:
case 0xc:
case 0x18:
case 0x30:
case 0x60:
case 0xc0:
num = 2;
break;
// ...
}
return num;
}
解法四看似很直接,但實際執行效率可能會低於解法二和解法三,因為分支語句的執行情況要看具體字節的值,如果 a =0,那自然在第 1 個case就得出了答案,但是如果 a =255,則要在最后一個 case 才得出答案,即在進行了 255 次比較操作之后! 看來,解法四不可取!但是解法四提供了一個思路,就是采用空間換時間的方法,羅列並直接給出值。如果需要快速地得到結果,可以利用空間或利用已知結論。這就好比已經知道計算 1+2+ … +N 的公式,在程序實現中就可以利用公式得到結論。 最后,得到解法五:算法中不需要進行任何的比較便可直接返回答案,這個解法在時間復雜度上應該能夠讓人高山仰止了。
【解法五】查表法
int countTable[ 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
};
int Count( int v)
{
// check parameter
return countTable[v];
}
這是個典型的空間換時間的算法,把 0~255 中“1”的個數直接存儲在數組中,v 作為數組的下標,countTable[v]就是 v 中“1”的個數。算法的時間復雜度僅為 O(1)。 在一個需要頻繁使用這個算法的應用中,通過“空間換時間”來獲取高的時間效率是一個常用的方法,具體的算法還應針對不同應用進行優化。