拜託,面試別再讓我數1了!!!
面試中,除了TopK,是否被問過:求一個正整數的二進位制表示包含多少個1?
例如:
uint32_t i=58585858;
i的二進位制表示是:
0000 0011 0111 1101 1111 0011 0000 0010
於是,i的二進位制表示包含15個1。 到底有幾種方法,這些思路里蘊含的優化思路究竟是怎麼樣的,今天和大家聊一聊。
一、位移法
思路:既然輸入n是uint32,每次取n的最低位,判斷是不是1,位移32次,迴圈判斷即可。 虛擬碼:
do{
if ((n&1)==1){
result++;
}
n>>= 1;
i++;
} while (i<32);
分析:不管n的二進位制表示裡包含多少個1,都需要迴圈計算32次,比較耗時。有沒有可能,每次消除掉一個1,這樣來降低計算次數呢?
二、求與法
觀察一下n與n-1這兩個數的二進位制表示:
-
最末位一個1會變成0
-
最末位一個1之後的0會全部變成1
-
其他位相同
栗子:
x = 1011 0000
x-1= 1010 1111
x & (x-1) = 1010 0000
於是, n&(n-1) 這個操作,可以起到“消除最後一個1”的功效。
思路:逐步通過n&(n-1),來消除n末尾的1,消除了多少次,就有多少個1。 虛擬碼:
while (n){
result++;
n&=(n-1);
}
分析:這個方法,n的二進位制表示有多少個1,就會計算多少次。總的來說,n的長度是32bit,如果n的值選取完全隨機,平均期望由16個1構成,平均下來16次,節省一半的計算量。
畫外音:校招時,我問過這樣的面試題,“如何快速判斷一個正整數是不是2的x次冪”,巧妙解法是 return !(n&(n-1)); 即,如果n是2的x次冪,二進位制表示只有一個1。
三、查表法
空間換時間,是演算法優化中最常見的手段,如果有相對充裕的記憶體,可以有更快的演算法。 思路:一個uint32的正整數n,一旦n的值確定,n的二進位制表示中包含多少個1也就確定了,理論上無需重新計算:
1的二進位制表示中包含1個1
2的二進位制表示中包含1個1
3的二進位制表示中包含2個1
…
58585858的二進位制表示中包含15個1
…
提前計算好結果陣列:
result[1]=1;
result[2]=1;
result[3]=2;
…
result[58585858]=15;
…
虛擬碼:
return result[n];
查表法的好處是,時間複雜度為O(1),潛在的問題是,需要很大的記憶體。
記憶體分析:
假如被分析的整數是uint32,打表陣列需要記錄2^32個正整數的結果。
n的二進位制表示最多包含32個1,儲存結果的計數,使用5個bit即可。
故,共需要記憶體2^32 * 5bit = 2.5GB。
畫外音:5個bit,能表示00000-11111這32個數。幫忙看下,算錯了沒有,上一篇文章bit和Byte算錯了8倍。
四、二次查表法
查表法,非常快,只查詢一次,但消耗記憶體太大,在工程中幾乎不被使用。
演算法設計,本身是一個時間複雜度與空間複雜度的折衷,增加計算次數,往往能夠減少儲存空間。
思路:
(1)把uint32的正整數n,分解為低16位正整數n1,和高16正整數n2;
(2)n1查一次表,其二進位制表示包含a個1;
(3)n2查一次表,其二進位制表示包含b個1;
(4)則,n的二進位制表示包含a+b個1;
虛擬碼:
uint16 n1 = n & 0xFFFF;
uint16 n2 = (n>>16) & 0xFFFF;
return result[n1]+result[n2];
問題來了:增加了一倍的計算量(1次查表變2次查表),記憶體空間是不是對應減少一半呢?
記憶體分析:
被分析的整數變成uint16,打表陣列需要記錄2^16個正整數的結果。
n1和n2的二進位制表示最多包含16個1,儲存結果的計數,使用4個bit即可。
故,共需要記憶體2^16 * 4bit = 32KB。
畫外音:幫忙看下,算錯了沒有。
好神奇!!!
計算量多了1次(1倍),記憶體佔用量卻由2.5G降到了32K(1萬多倍),是不是很有意思?
五、總結
數1,不難;但其思路有優化過程,並不簡單:
(1)位移法,32次計算;
(2)n&(n-1),能消除一個1,平均16次計算;
(3)查表法,1次查表,2.5G記憶體;
(4)二次查表法,2次查表,32K記憶體;
知其然,知其所以然。
思路比結論重要。
希望大家對“數1”有新的認識,謝轉。