java不使用“+”,“-”,“×”,“÷”實現四則運算

想了解更多數據結構以及算法題,可以關注微信公衆號“數據結構和算法”,每天一題爲你精彩解答。也可以掃描下面的二維碼關注
在這裏插入圖片描述

從我們開始上學的時候就知道,如果要實現加法運算就要使用“+”符號,如果要實現減法運算就要使用“-”符號……,甚至在今天的計算機中也是一樣的,我們只知道怎麼使用,但很少去關注他的底層是怎麼實現的。如果突然哪天給你一道面試題,讓你不使用"+"來實現兩個數相加,你該怎麼做呢,今天我們就來看一下該怎麼實現。

一:不使用“+”實現兩個數相加

我們先來看一道非常簡單的題,在計算機中數字是由二進制位表示的,也就是說是由0和1組成的,如果我們要實現0和1之間的加法該怎麼實現呢,他會有4種組合方式

1,0+0=00
2,0+1=01
3,1+0=01
4,1+1=10

我們發現一個很重要的規律,就是隻有1+1有進位,其他的都沒進位。所以我們判斷有沒有進位只需要判斷a&b是否等於1即可,而a+b的值(不考慮進位)只需要計算a|b即可,看明白了這點,代碼就呼之欲出了

private static int add1(int a, int b) {
    int c = (a & b) << 1;//進位的值
    int d = a ^ b;//不考慮進位,相加的值
    return c | d;//或者 return c ^ d;
}

a和b要麼是1要麼是0,所以這裏最多也只有一個進位,很好理解。但我們計算二進制的加減法不光只有1個0或1,可能會有好多個1或0,那我們該怎麼實現呢。比如a=13(1101),b=9(1001),我們該怎麼計算a+b的結果。首先如果我們不考慮進位問題,那麼a+b的運算會是下面這樣
在這裏插入圖片描述
但實際上最前面和最後面的1都有了進位。

1, 我們看到如果不考慮進位,那麼a+b的結果其實就是a^b的結果,我們該怎麼把進位問題也考慮在內呢,實際上只有1+1的時候纔會出現進位,1+0或者0+0都不會出現進位,所以我們首先想到的是&運算

2, 這裏我們計算一下a&b的結果是1001,我們知道當&運算的結果爲1的時候,說明參與&運算的兩個都是1,既然兩個都是1,那麼相加的時候就肯定會有進位,所以他們進位的值實際上是10010((a&b)<<1),然後在和0100相加就是我們要求的結果,10010+00100=10110,10110實際上就是22,也就是13+9的結果

3, 但我們好像忽略了一個問題,就是這道題要求不能使用加減乘除符號,而上面我們分析的時候使用了加號,所以明顯不行。通過上面的分析實際上我們已經發現了一個規律,就是a+b通過^和&運算之後又再執行相加操作,所以我們首先想到的是遞歸,我們來看下代碼

private static int add2(int a, int b) {
    if (a == 0 || b == 0)
        return a ^ b;
    return add2(a ^ b, (a & b) << 1);
}

第3行表示的是如果a== 0就返回b,如果b==0就返回a,這種寫法少了一個if語句的判斷會更簡潔。爲了驗證代碼的準確性我們隨便找幾個數據測試一下

    int[] array = {1, 1, 1, 0, 0, 1, 0, 0, 13, 9, 1, -1, -Integer.MAX_VALUE, Integer.MAX_VALUE, -8, -9};
    for (int i = 0; i < array.length / 2; i++) {
        System.out.println(array[i << 1] + "+" + array[(i << 1) + 1] + "=" + add2(array[i << 1], array[(i << 1) + 1]));
    }

上面的代碼可以不用看,我們來看一下運行結果
在這裏插入圖片描述
經過測試,發現我們的代碼完全正確,沒有使用“+”實現了兩個數相加。上面的遞歸我們還可以改爲非遞歸的方式

private static int add3(int a, int b) {
    while (b != 0) {
        int temp = a ^ b;
        b = (a & b) << 1;
        a = temp;
    }
    return a;
}

這個也很好理解,每次計算的時候要對a和b進行重新賦值,然後再不斷的循環,直到b等於0的時候停止循環,我們知道這裏在運算的時候b表示的是進位的值,當b等於0的時候就表示沒有進位,沒有進位就退出循環,這就是使用位運算來實現加法。我們假設a=13,b=9來畫個圖加深一下理解
在這裏插入圖片描述
二:不使用“-”實現兩個數相減

既然加法都實現了,那麼減法就更容易了,a-b,直接改爲a+(-b)即可,那麼請等一下,我們不是說不能使用“-”嗎,這裏明顯有了“-”符號,肯定不符合規則,那麼彆着急,在計算機中一個數的相反數還可以用另一種方式來表示,那就是
a的相反數是~a+1
上面“+”我們已經實現了,“~”不屬於四則運算符,所以代碼也很容易寫出

private static int subtraction(int a, int b) {
    return add3(a, add3(~b, 1));
}

這種實現就更簡潔了,直接一行代碼搞定,代碼中add3(~b,1)表示的是-b。如果不使用加法是否能實現兩個數相減呢,其實也是可以的,我們這樣來思考一下,比如a-b

1, 如果b等於0,我們直接返回a即可。如果b不等於0,我們可以先把a和b上同爲1的數字給去掉,那麼怎麼去掉呢,其實很簡單,我們先要計算c=a&b,那麼c中爲1的位置在a和b中相對應的位置上也是1,然後再通過異或運算就可以把它給移除。

2, 在經過第一步執行之後,a和b在相同的位置上要麼都是0,要麼一個0一個1,不可能全是1了,那麼下面就要會分爲3種情況了(我們先不考慮因不夠減而借位的問題

(1), 如果a和b對應的位置上都是0,那麼結果對應的位置上也是0。

(2), 如果a對應的位置是1,b對應的位置是0,結果對應的位置是1。

(3), 如果a對應的位置是0,b對應的位置是1,結果對應的位置是1。(向前借一位1)

所以在不考慮借位的情況下,對應位置上的結果其實就是a|b(對應位置都爲1的在第一步就已經被踢出了),那麼實際計算的時候我們不可能不考慮借位的問題,所以實際結果是(a|b)-(b<<1),但這裏又出現了“-”符號,所以不符合要求,這時我們可以使用遞歸的方式來解決,代碼如下

private static int subtraction2(int a, int b) {
    if (b == 0)
        return a;
    int c = a & b;
    //下面兩行是把a和b中相同位置爲1的都消去
    a ^= c;
    b ^= c;
    return subtraction2(a | b, b << 1);
}

當然我們還可以把它改爲非遞歸的方式,像下面這樣

 private static int subtraction3(int a, int b) {
     while (b != 0) {
         int c = a & b;
         a ^= c;
         b ^= c;
         a |= b;
         b <<= 1;
     }
     return a;
}

我們還是找幾組數據來測試一下吧

    int[] array = {1, 1, 1, 0, 0, 1, 0, 0, 13, 9, 1, -1, Integer.MAX_VALUE, Integer.MAX_VALUE, -8, -9, 100, Integer.MAX_VALUE};
    for (int i = 0; i < array.length / 2; i++) {
        System.out.println(array[i << 1] + "-" + array[(i << 1) + 1] + "=" + subtraction2(array[i << 1], array[(i << 1) + 1]));
    }

上面的我們可以不用看,直接看運行結果就行了
在這裏插入圖片描述
我們以a=13,b=10來畫個圖加深一下理解
在這裏插入圖片描述
在這裏插入圖片描述

三:不使用“×”實現兩個數相乘

我們先來看個例子,比如13*9,計算方式如下
在這裏插入圖片描述
由上面公式我們可以看出只有b的某一位是1的時候和a相乘纔有意義,如果b的某一位是0,那麼和a相乘則永遠都是0,所以我們計算的時候逐步遍歷b的每一位,只有當他爲1的時候才進行運算,我們來看下代碼

 //求一個數的相反數
 private static int negative(int a) {
     return add3(~a, 1);
 }
 
 private static int mult(int a, int b) {
     int x = a < 0 ? negative(a) : a;//如果是負數,先轉爲正數再參與計算
     int y = b < 0 ? negative(b) : b;
     int res = 0;
    while (y != 0) {
        if ((y & 1) == 1)
            res = add3(res, x);
        x <<= 1;
        y >>= 1;
    }
    return (a ^ b) >= 0 ? res : negative(res);
}

我們還是來找一組數據測試一下,驗證我們代碼的正確性

    int[] array = {1, 1, 1, 0, 0, 1, 0, 0, 13, 9, 1, -1, -8, -9, 100, 99};
    for (int i = 0; i < array.length / 2; i++) {
        System.out.println(array[i << 1] + "×" + array[(i << 1) + 1] + "=" + mult(array[i << 1], array[(i << 1) + 1]));
    }

上面代碼不用看,我們直接來看一下打印的數據,結果絲毫不差
在這裏插入圖片描述
四:不使用“÷”實現兩個數相除

a÷b的含義是a中包含多少個b,比如6÷3=2,7÷3=2,這裏我們實現的除法和計算機中兩個int類型相除結果是一樣的,只記錄商的值,餘數會被捨去,所以我們想到的一種解法是用a不斷的減去b,並記錄減了多少次,所以代碼很容易想到,我們來看下

private static int div1(int a, int b) {
    int x = a < 0 ? negative(a) : a;
    int y = b < 0 ? negative(b) : b;
    if (x < y)
        return 0;
    return (a ^ b) >= 0 ? div1(subtraction(a, b), b) + 1 : div1(add3(a, b), b) - 1;
}

上面的+1,-1直接改爲上面的加法和減法即可,這裏我爲了方便閱讀代碼就沒寫。這種遞歸的實現效率不是很高,如果a非常大,b又比較小,很容易出現堆棧溢出異常,所以我們還可以把它改爲非遞歸

 private static int div2(int a, int b) {
     int x = a < 0 ? negative(a) : a;
     int y = b < 0 ? negative(b) : b;
     int ocunt = 0;
     while (x >= y) {
         x = subtraction3(x, y);
         ocunt++;
     }
     return (a ^ b) >= 0 ? ocunt : -ocunt;
}

這種雖然不會出現堆棧溢出異常了,但如果b是1,a是一個非常非常大的數,這樣一直減下去也是非常慢的,我們還可以換種思路,每次減去的不是b,而是b的倍數,我們來看下代碼

 private static int div3(int a, int b) {
     if (a == 0 || b == 0)
         return 0;//b不能爲0,如果b是0我們應該拋異常的,這裏簡單處理就沒拋
     int x = a < 0 ? negative(a) : a;
     int y = b < 0 ? negative(b) : b;
     int result = 0;
     for (int i = 31; i >= 0; i--) {
         if ((x >> i) >= y) {
             result = add3(result, 1 << i);
            x = subtraction3(x, y << i);
        }
    }
    return (a ^ b) >= 0 ? result : -result;
}

我們找一組非常極端的數據來測一下上面兩種方法,看一下效率到底相差多少倍

    int a = Integer.MAX_VALUE;
    int b = 1;
    long time = System.nanoTime();
    System.out.println(a + "÷" + b + "=" + div2(a, b));
    System.out.println("優化之前的時間:" + (System.nanoTime() - time));
    time = System.nanoTime();
    System.out.println(a + "÷" + b + "=" + div3(a, b));
    System.out.println("優化之後的時間:" + (System.nanoTime() - time));

我們來看一下結果
在這裏插入圖片描述
這個時間相差還是非常大的,一個30多億納秒,一個兩萬多納秒,相差十幾萬倍。最後我們再找一組數據測試一下我們的代碼是否正確

    int[] array = {1, 1, 0, 1, 13, 9, 40, 3, 1, -1, -8, -9, 100, 99};
    for (int i = 0; i < array.length / 2; i++) {
        System.out.println("div2方法測試:" + array[i << 1] + "÷" + array[(i << 1) + 1] + "=" + div2(array[i << 1], array[(i << 1) + 1]));
    }
    System.out.println("----------------------------------------");
    for (int i = 0; i < array.length / 2; i++) {
        System.out.println("div3方法測試:" + array[i << 1] + "÷" + array[(i << 1) + 1] + "=" + div3(array[i << 1], array[(i << 1) + 1]));
    }

我們來看下運行結果
在這裏插入圖片描述
結果和我們預想的完全一致。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章