位操作實(shí)現(xiàn)四則運(yùn)算與大數(shù)相乘

先來(lái)掌握一些常用的位運(yùn)算操作:

  1. 等式:-n = ~(n - 1) = ~n + 1(-n等于其各位取反加1);

  2. 獲取整數(shù)n的二進(jìn)制中最后一個(gè)1:-n&n 或(~n+1)&n或 ~(n - 1)&n;
    如:n = 010100,則 -n = 101100, n&(n - 1)=000100;

  3. 去掉整數(shù)n的二進(jìn)制中最后一個(gè)1:n&(n - 1)。如:n = 010100, n -1 = 010011, n&(n - 1) = 010000。

加法 a + b

思考:
a^b可得到對(duì)應(yīng)位沒(méi)有進(jìn)位時(shí)的和,如果該位進(jìn)位了,其值為0,該位沒(méi)有進(jìn)位,其值為a+b對(duì)應(yīng)位的結(jié)果(不考慮低位進(jìn)位)
a&b可得到各位產(chǎn)生的進(jìn)位值,如果該位沒(méi)有進(jìn)位,其值為0,該位進(jìn)位了,其值為1
(a&b)<<1正好代表a+b有進(jìn)位的位進(jìn)位后的結(jié)果
如果使用a^b+(a&b)<<1來(lái)表示a+b的值有問(wèn)題的地方在于不能保證這兩個(gè)子表達(dá)式相加過(guò)程不產(chǎn)生進(jìn)位
所以迭代也好,遞歸也好,一定要讓(a&b)也就是進(jìn)位的情況不存在,直接可以用a^b表示出不產(chǎn)生進(jìn)位的加法
注意在多次循環(huán)中,a,b的值也要相應(yīng)變化,當(dāng)然數(shù)據(jù)溢出的問(wèn)題本篇博客就不考慮了

解答如下:
用 ^,&和<<即可實(shí)現(xiàn),,a&b可得到各位產(chǎn)生的進(jìn)位值。

如:a=010010, b=100111,計(jì)算過(guò)程如下:

  1. 每輪首先判斷b是否為0,為0則結(jié)束,把a(bǔ)的值作為結(jié)果返回
  2. 第一輪:通過(guò)(a&b)=000010 得出a+b結(jié)果中需要進(jìn)位的位置
    (a&b)<<1 將進(jìn)位的值全體左移一位后,表示進(jìn)位部分進(jìn)位后的值
    a^b=110101,表示a+b不進(jìn)位部分相加的結(jié)果
    由于進(jìn)位大于0,把a(bǔ)^b的值賦給a,此時(shí)a=110101,(a&b)<<1的值賦給b,此時(shí)b=000100;
    進(jìn)入下一輪中,新的a+b=老的a+b的值=循環(huán)下去b值為0時(shí)a的值(此時(shí)代表不產(chǎn)生進(jìn)位)
  3. 第二輪:a^b=110001,(a&b)<<1=001000(進(jìn)位)。進(jìn)位大于0,進(jìn)入下一輪:a=110001,b=001000;
  4. 第三輪:a^b=111001,(a&b)<<1=000000(進(jìn)位)。進(jìn)位等于0,終止;

結(jié)果為:111001。

非遞歸
int add1(int a, int b)
{
    int carry; // 進(jìn)位
    while (b!=0)
    {
        carry = (a & b) << 1; // a & b取各位的進(jìn)位,進(jìn)位作用于對(duì)應(yīng)位的高1位
        a = a ^ b;
        b = carry;
    }
    return a;
}
遞歸
int add2(int a, int b)
{
    if (0 == b) return a;
    else
    {
        int carry = (a & b) << 1;
        a = a ^ b;
        return add2(a, carry);
    }
// 簡(jiǎn)化版
//  return b ? add2(a^b,(a&b)<<1) : a;
}

減法:a-b

由a-b=a+(-b),~(b-1)=-b可得a-b=a+(-b)=a+((b-1))=a+b + 1。把減法轉(zhuǎn)化為加法即可。

int sub(int a, int b)
{
    return add(a, add(~b, 1));
}

乘法:a*b

基礎(chǔ)版:觀察如下

   1011  
*  1010  
--------  
  10110 (1011<<1,相當(dāng)于乘以0010)  
1011000 (1011<<3,相當(dāng)于乘以1000)  
--------
1101110 

可以看到,二進(jìn)制乘法的原理是:從乘數(shù)的低位到高位,遇到1并且這個(gè)1在乘數(shù)的右起第i(i從0開(kāi)始數(shù))位,那么就把被乘數(shù)左移i位得到 temp_i 。直到乘數(shù)中的1遍歷完后,把根據(jù)各位1而得到的被乘數(shù)的左移值們 temp_i 相加起來(lái)即得乘法結(jié)果。那么根據(jù)這個(gè)原理,可以得到實(shí)現(xiàn)代碼:這里要點(diǎn)為:用i記錄當(dāng)前遍歷的乘數(shù)位,當(dāng)前位為1則被乘數(shù)左移i位并加到和中,同時(shí)i++處理下一位;為0則乘數(shù)右移,i++,處理下一位......直到乘數(shù)==0說(shuō)明乘數(shù)中的1遍歷完了。此時(shí)把和返回即可。
需要注意的是,補(bǔ)碼乘法符號(hào)位也是參與運(yùn)算的,具體參見(jiàn)深入理解計(jì)算機(jī)系統(tǒng)第二章內(nèi)容(無(wú)符號(hào)乘法和補(bǔ)碼乘法的位級(jí)等價(jià)性)

// 值得注意的是網(wǎng)上的程序片段中使用的>>代表算術(shù)右移,而在考慮到補(bǔ)碼
// 符號(hào)位為1的可能性的時(shí)候必須采用邏輯右移>>>,不然會(huì)死循環(huán)
int multi(int a,int b){
        int i=0;
        int res=0;
        while(b!=0){//乘數(shù)為0則結(jié)束
            //處理乘數(shù)當(dāng)前位
            if((b&1)==1){
                res+=(a<<i);
                b=b>>>1;
                ++i;//i記錄當(dāng)前位是第幾位
            }else{
                b=b>>>1;
                ++i;
            }
        }
        return res;
}

進(jìn)階思考:在基礎(chǔ)版之上,如果遇到兩個(gè)大數(shù)相乘,采用什么方式呢?
采用帶有分治法思想的乘法來(lái)計(jì)算


image.png
image.png

那么我們看一下java8里BigInteger類如何實(shí)現(xiàn)的

/**
     * Returns a BigInteger whose value is {@code (this * val)}.
     *
     * @implNote An implementation may offer better algorithmic
     * performance when {@code val == this}.
     *
     * @param  val value to be multiplied by this BigInteger.
     * @return {@code this * val}
     */
    public BigInteger multiply(BigInteger val) {
        if (val.signum == 0 || signum == 0)
            return ZERO;

        int xlen = mag.length;

        if (val == this && xlen > MULTIPLY_SQUARE_THRESHOLD) {
            return square();
        }

        int ylen = val.mag.length;

        if ((xlen < KARATSUBA_THRESHOLD) || (ylen < KARATSUBA_THRESHOLD)) {
            int resultSign = signum == val.signum ? 1 : -1;
            if (val.mag.length == 1) {
                return multiplyByInt(mag,val.mag[0], resultSign);
            }
            if (mag.length == 1) {
                return multiplyByInt(val.mag,mag[0], resultSign);
            }
            int[] result = multiplyToLen(mag, xlen,
                                         val.mag, ylen, null);
            result = trustedStripLeadingZeroInts(result);
            return new BigInteger(result, resultSign);
        } else {
            if ((xlen < TOOM_COOK_THRESHOLD) && (ylen < TOOM_COOK_THRESHOLD)) {
                return multiplyKaratsuba(this, val);
            } else {
                return multiplyToomCook3(this, val);
            }
        }
    }
 /**
     * Multiplies two BigIntegers using the Karatsuba multiplication
     * algorithm.  This is a recursive divide-and-conquer algorithm which is
     * more efficient for large numbers than what is commonly called the
     * "grade-school" algorithm used in multiplyToLen.  If the numbers to be
     * multiplied have length n, the "grade-school" algorithm has an
     * asymptotic complexity of O(n^2).  In contrast, the Karatsuba algorithm
     * has complexity of O(n^(log2(3))), or O(n^1.585).  It achieves this
     * increased performance by doing 3 multiplies instead of 4 when
     * evaluating the product.  As it has some overhead, should be used when
     * both numbers are larger than a certain threshold (found
     * experimentally).
     *
     * See:  http://en.wikipedia.org/wiki/Karatsuba_algorithm
     */
    private static BigInteger multiplyKaratsuba(BigInteger x, BigInteger y) {
        int xlen = x.mag.length;
        int ylen = y.mag.length;

        // The number of ints in each half of the number.
        int half = (Math.max(xlen, ylen)+1) / 2;

        // xl and yl are the lower halves of x and y respectively,
        // xh and yh are the upper halves.
        BigInteger xl = x.getLower(half);
        BigInteger xh = x.getUpper(half);
        BigInteger yl = y.getLower(half);
        BigInteger yh = y.getUpper(half);

        BigInteger p1 = xh.multiply(yh);  // p1 = xh*yh
        BigInteger p2 = xl.multiply(yl);  // p2 = xl*yl

        // p3=(xh+xl)*(yh+yl)
        BigInteger p3 = xh.add(xl).multiply(yh.add(yl));

        // result = p1 * 2^(32*2*half) + (p3 - p1 - p2) * 2^(32*half) + p2
        BigInteger result = p1.shiftLeft(32*half).add(p3.subtract(p1).subtract(p2)).shiftLeft(32*half).add(p2);

        if (x.signum != y.signum) {
            return result.negate();
        } else {
            return result;
        }
    }

在此之上還有更低復(fù)雜度的算法

 /**
     * Multiplies two BigIntegers using a 3-way Toom-Cook multiplication
     * algorithm.  This is a recursive divide-and-conquer algorithm which is
     * more efficient for large numbers than what is commonly called the
     * "grade-school" algorithm used in multiplyToLen.  If the numbers to be
     * multiplied have length n, the "grade-school" algorithm has an
     * asymptotic complexity of O(n^2).  In contrast, 3-way Toom-Cook has a
     * complexity of about O(n^1.465).  It achieves this increased asymptotic
     * performance by breaking each number into three parts and by doing 5
     * multiplies instead of 9 when evaluating the product.  Due to overhead
     * (additions, shifts, and one division) in the Toom-Cook algorithm, it
     * should only be used when both numbers are larger than a certain
     * threshold (found experimentally).  This threshold is generally larger
     * than that for Karatsuba multiplication, so this algorithm is generally
     * only used when numbers become significantly larger.
     *
     * The algorithm used is the "optimal" 3-way Toom-Cook algorithm outlined
     * by Marco Bodrato.
     *
     *  See: http://bodrato.it/toom-cook/
     *       http://bodrato.it/papers/#WAIFI2007
     *
     * "Towards Optimal Toom-Cook Multiplication for Univariate and
     * Multivariate Polynomials in Characteristic 2 and 0." by Marco BODRATO;
     * In C.Carlet and B.Sunar, Eds., "WAIFI'07 proceedings", p. 116-133,
     * LNCS #4547. Springer, Madrid, Spain, June 21-22, 2007.
     *
     */
    private static BigInteger multiplyToomCook3(BigInteger a, BigInteger b) {
        int alen = a.mag.length;
        int blen = b.mag.length;

        int largest = Math.max(alen, blen);

        // k is the size (in ints) of the lower-order slices.
        int k = (largest+2)/3;   // Equal to ceil(largest/3)

        // r is the size (in ints) of the highest-order slice.
        int r = largest - 2*k;

        // Obtain slices of the numbers. a2 and b2 are the most significant
        // bits of the numbers a and b, and a0 and b0 the least significant.
        BigInteger a0, a1, a2, b0, b1, b2;
        a2 = a.getToomSlice(k, r, 0, largest);
        a1 = a.getToomSlice(k, r, 1, largest);
        a0 = a.getToomSlice(k, r, 2, largest);
        b2 = b.getToomSlice(k, r, 0, largest);
        b1 = b.getToomSlice(k, r, 1, largest);
        b0 = b.getToomSlice(k, r, 2, largest);

        BigInteger v0, v1, v2, vm1, vinf, t1, t2, tm1, da1, db1;

        v0 = a0.multiply(b0);
        da1 = a2.add(a0);
        db1 = b2.add(b0);
        vm1 = da1.subtract(a1).multiply(db1.subtract(b1));
        da1 = da1.add(a1);
        db1 = db1.add(b1);
        v1 = da1.multiply(db1);
        v2 = da1.add(a2).shiftLeft(1).subtract(a0).multiply(
             db1.add(b2).shiftLeft(1).subtract(b0));
        vinf = a2.multiply(b2);

        // The algorithm requires two divisions by 2 and one by 3.
        // All divisions are known to be exact, that is, they do not produce
        // remainders, and all results are positive.  The divisions by 2 are
        // implemented as right shifts which are relatively efficient, leaving
        // only an exact division by 3, which is done by a specialized
        // linear-time algorithm.
        t2 = v2.subtract(vm1).exactDivideBy3();
        tm1 = v1.subtract(vm1).shiftRight(1);
        t1 = v1.subtract(v0);
        t2 = t2.subtract(t1).shiftRight(1);
        t1 = t1.subtract(tm1).subtract(vinf);
        t2 = t2.subtract(vinf.shiftLeft(1));
        tm1 = tm1.subtract(t2);

        // Number of bits to shift left.
        int ss = k*32;

        BigInteger result = vinf.shiftLeft(ss).add(t2).shiftLeft(ss).add(t1).shiftLeft(ss).add(tm1).shiftLeft(ss).add(v0);

        if (a.signum != b.signum) {
            return result.negate();
        } else {
            return result;
        }
    }

 /**
     * Returns a slice of a BigInteger for use in Toom-Cook multiplication.
     *
     * @param lowerSize The size of the lower-order bit slices.
     * @param upperSize The size of the higher-order bit slices.
     * @param slice The index of which slice is requested, which must be a
     * number from 0 to size-1. Slice 0 is the highest-order bits, and slice
     * size-1 are the lowest-order bits. Slice 0 may be of different size than
     * the other slices.
     * @param fullsize The size of the larger integer array, used to align
     * slices to the appropriate position when multiplying different-sized
     * numbers.
     */
    private BigInteger getToomSlice(int lowerSize, int upperSize, int slice,
                                    int fullsize) {
        int start, end, sliceSize, len, offset;

        len = mag.length;
        offset = fullsize - len;

        if (slice == 0) {
            start = 0 - offset;
            end = upperSize - 1 - offset;
        } else {
            start = upperSize + (slice-1)*lowerSize - offset;
            end = start + lowerSize - 1;
        }

        if (start < 0) {
            start = 0;
        }
        if (end < 0) {
           return ZERO;
        }

        sliceSize = (end-start) + 1;

        if (sliceSize <= 0) {
            return ZERO;
        }

        // While performing Toom-Cook, all slices are positive and
        // the sign is adjusted when the final number is composed.
        if (start == 0 && sliceSize >= len) {
            return this.abs();
        }

        int intSlice[] = new int[sliceSize];
        System.arraycopy(mag, start, intSlice, 0, sliceSize);

        return new BigInteger(trustedStripLeadingZeroInts(intSlice), 1);
    }

感覺(jué)分析完身體要被掏空。。以后再說(shuō)吧。。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容