演算法:只用位操作實現+、-、*、/、冪次運算
最近零星看到一些位操作的演算法題,看題目都有類似的套路,不用常規算術運算,只用位操作和判斷,實現+、-、*、/
和冪次操作。之前都是看到了就做一做,今天寫這篇文章對此種類型的演算法題做統一整理。
加法
不用+號做加法?首先回顧一下我們小學時候學的加法運算是怎麼做的。從低位到高位,逐位做加法,得到進位,迴圈下一位。對這道題我們也可以用同樣的方法來實現加法。怎麼做呢?我們可以把兩個數字都表示為二進位制數,然後分別對二進位制數用小學的加法運算逐位計算和與進位,從低位到高位迴圈,得到最終結果。對二進位制數來說,當前bit位的和的計算可以表示為:0 + 0 = 0,0 + 1 = 1,1 + 0 = 1,1 + 1 = 2
(bit1 & bit2) | (bit1 & carry) | (bit2 & carry)
。這樣一來,當前bit位的值和下一位的進位計算方法我們都有了,由低位到高位逐位計算即可得到無+號的加法演算法:
public static long add(long a, long b){
long sum = 0, carryin = 0, tempA = 0, tempB = 0, k = 1;
while(tempA != 0 || tempB != 0 ){
long ak = a & k;
long bk = b & k;
sum |= ak ^ bk ^ carryin;
carryin = ((ak & bk) | (ak & carryin) | (bk & carryin)) << 1;
k <<= 1;
tempA >>>= 1;
tempB >>>= 1;
}
return sum;
}
時間複雜度為O(n),其中n為long資料型別的寬度。
乘法
最先想到的brute-force方法就是重複做加法實現乘法。比如x * y
可以通過重複加y次x得到,時間複雜度是O(2^n)其中n為資料型別寬度。
再想想我們在學校學過的乘法運算,其實是通過移位運算,逐位相加各bit位的方法得到結果。舉個例子,111 * 111
,我們在草稿上通過111 * 100 + 111 * 10 + 111 * 1
得到結果。在此我們可以借鑑這種方法,只不過在學校我們學的是十進位制,在計算機中我們用二進位制。計算x * y
,假如x的第k位是1,那我們就在結果中加上2^k * y
,這樣逐位遍歷x的所有bit位,即可得到結果。加法運算可以用前文描述的加法演算法。程式碼如下:
public static long multiply(long x, long y){
long sum = 0;
while(x != 0){
if((x & 1) != 0){
sum = add(sum, y);
}
x >>>= 1;
y <<= 1;
}
return sum;
}
逐位遍歷的時間複雜度為O(n),其中n為資料型別寬度。每次加法的時間複雜度如前文所述,為O(n),所以乘法運算總的時間複雜度為O(n^2)。
除法
除法運算可以用乘法運算的逆運算來考慮。最簡單的brute-force方法就是對除數逐次做減法,每減一次商加1,直到減到不夠減為止,時間複雜度如乘法運算所述為O(2^n)。
參考前文所述的乘法運算,除法運算也可以用逐位減的方式來計算。計算x / y
,如果2^k * y <= x
,那麼計算結果商就加2^k
。舉個例子,如果x = (1011)2
,y = (10)2
, k = 2
, 因為2 * (2^2) < 11
且2 * (2^3) > 11
, 那我們就從(1011)2
中減去(1000)2
得到(11)2
, 並在商中加上2^k = 2^2 = (100)2
,然後更新x
為(11)2
。
2^k
可以很方便地通過移位操作實現,而k我們也可以從高到低位逐位遞減。程式碼如下:
public static long divide(long x, long y){
long result = 0;
int power = 32;
long ypower = y << power;
while(x >= y){
while(ypower > x){
ypower >>>= 1;
power--;
}
x -= ypower;
result |= (1L << power);
}
return result;
}
時間複雜度為O(n),其中n為資料型別寬度。
冪次
冪次運算的brute-force演算法也很簡單,x^2 = x * x
,x^3 = (x^2) * x
,依次類推。時間複雜度為O(2^n),其中n為資料型別寬度。
我們知道,對冪次運算有運算率:x^(y+z) = (x^y) * (x^z)
,這樣我們可以把乘數看作多個數的和,然後再用上面的公式進行計算,可以大大減少運算次數。而每個整數的二進位制表示恰好可以看作2的次方數相加的結果,這樣,我們可以把乘數當作二進位制數,逐位計算,當前第k位為1時,就在結果中乘上x^(2^k)
,直到y為0為止。而x^(2^k)
可以很輕鬆地通過x的移位運算得到,也可以逐次計算x^(2^k) = x^(2^(k-1)) * x^(2^(k-1))
。針對y為負數的情況,有x^y = (1/x)^(-y)
。完整程式碼如下:
public static double power(double x, int y){
long power = y;
if(power < 0){
power = -power;
x = 1/x;
}
double result = 1;
while(power > 0){
if((power & 1) == 1){
result *= x;
}
x <<= 1;
power >>>= 1;
}
return result;
}
時間複雜度為O(n),其中n為資料型別寬度。
總結
本文整理了不用常規算術運算,只用位操作和判斷,實現+、-、*、/
和冪次操作的演算法題的解題思路,基本想法都是通過把一個運算數看作二進位制數,利用不同運算固有的運算率減少計算次數,仔細觀察會發現它們都有基本一致的套路,掌握了以後就可以很輕鬆地舉一反三。