1. 程式人生 > >演算法:只用位操作實現+、-、*、/、冪次運算

演算法:只用位操作實現+、-、*、/、冪次運算

最近零星看到一些位操作的演算法題,看題目都有類似的套路,不用常規算術運算,只用位操作和判斷,實現+、-、*、/和冪次操作。之前都是看到了就做一做,今天寫這篇文章對此種類型的演算法題做統一整理。

加法

不用+號做加法?首先回顧一下我們小學時候學的加法運算是怎麼做的。從低位到高位,逐位做加法,得到進位,迴圈下一位。對這道題我們也可以用同樣的方法來實現加法。怎麼做呢?我們可以把兩個數字都表示為二進位制數,然後分別對二進位制數用小學的加法運算逐位計算和與進位,從低位到高位迴圈,得到最終結果。對二進位制數來說,當前bit位的和的計算可以表示為:0 + 0 = 0,0 + 1 = 1,1 + 0 = 1,1 + 1 = 2

,這不就是XOR操作嘛!下一位bit位的進位,只有噹噹前兩個bit位和進位中有兩個1時,下一位的進位才為1,所以可以用與操作實現:(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)2y = (10)2, k = 2, 因為2 * (2^2) < 112 * (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 * xx^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為資料型別寬度。

總結

本文整理了不用常規算術運算,只用位操作和判斷,實現+、-、*、/和冪次操作的演算法題的解題思路,基本想法都是通過把一個運算數看作二進位制數,利用不同運算固有的運算率減少計算次數,仔細觀察會發現它們都有基本一致的套路,掌握了以後就可以很輕鬆地舉一反三。