想了解更多數據結構以及算法題,可以關注微信公衆號“數據結構和算法”,每天一題爲你精彩解答。也可以掃描下面的二維碼關注
從我們開始上學的時候就知道,如果要實現加法運算就要使用“+”符號,如果要實現減法運算就要使用“-”符號……,甚至在今天的計算機中也是一樣的,我們只知道怎麼使用,但很少去關注他的底層是怎麼實現的。如果突然哪天給你一道面試題,讓你不使用"+"來實現兩個數相加,你該怎麼做呢,今天我們就來看一下該怎麼實現。
一:不使用“+”實現兩個數相加
我們先來看一道非常簡單的題,在計算機中數字是由二進制位表示的,也就是說是由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]));
}
我們來看下運行結果
結果和我們預想的完全一致。