用位運(yùn)算實(shí)現(xiàn)加減乘除(2)
位運(yùn)算實(shí)現(xiàn)除法
先說一種普遍的除法,比如下圖中的a / b:

b向左移到最接近a的時(shí)候,但是又沒a大,要保證左移到最接近a但是不比a大。
除的最終結(jié)果里,這個(gè)位置一定有1,見下圖:

然后把a(bǔ)減掉這個(gè)數(shù),變成:

然后再把b移動(dòng)到最接近a的時(shí)候,這個(gè)位置一定是1:

這就是最終的結(jié)果:

a/b的過程就是上面的。如果不太好理解的話,看下面這種解釋:
如果a / b = c,如圖:

說明啥?a可以這么表示:


再舉個(gè)例子:

看到上面的例子,那我們應(yīng)該怎么做?
怎么求這個(gè)c呢?這么想:
a - 一個(gè)盡可能大的2某次方 * b = a'
a' - 一個(gè)盡可能大的2某次方 * b = a''
a'' - 一個(gè)盡可能大的2某次方 * b = a'''
...
一直這么減,就把結(jié)果得到了。
b*c的演算過程:



a = b * 2 + b * 22,也就是 a = b << 1 + b << 2
假設(shè),a = b*2? + b*2? + b*23,你說c是啥?在第7、5、3位置上是1,其他都是0 :

所以,怎么算a/b呢?
a我先看看,最大的2的某次方 * b,而且2?*b不能超過a,找到最大2的某次方。假設(shè)是7,那么我就知道最終結(jié)果7的位置上一定是1。
a': a - b*2?,再次找到最接近a'的2某次方 * b,假設(shè)是5,那么我知道最終結(jié)果5的位置上一定是1。
a'': a' - b*2?, 再次找到最接近a''的2某次方 * b,假設(shè)是3,那么我知道最終結(jié)果3的位置上一定是1。
...
所以c就是 10101000

思路就是這樣。具體如何操作?

a/b等于幾?
b離a最近的又不超過a的,左移了幾位?5位。

然后a 減掉 這個(gè)數(shù),再去看b左移幾位然后不超過a剛好夠的?左移2位。

a再減掉這個(gè)數(shù),等于0,結(jié)束。
a = b*2? + b*22,c就是:100100
代碼實(shí)現(xiàn):
package com.cy.class05;
/**
* 位運(yùn)算實(shí)現(xiàn)+-*\\/
* 測(cè)試連接:https://leetcode.com/problems/divide-two-integers
*/
public class Code03_BitAddMinusMultiDiv {
public static int add(int a, int b) {
int sum = a;
while(b != 0) {
sum = a ^ b; //無進(jìn)位相加信息 -> sum
b = (a & b) << 1; //進(jìn)位信息 -> b -> b'
a = sum; //無進(jìn)位相加信息a -> a'
}
return sum;
}
/**
* 返回n的相反數(shù)
* 相反數(shù) = 取反 + 1
* -n = ~n + 1
*/
public static int negNum(int n) {
return add(~n, 1);
}
public static int minus(int a, int b){
return add(a, negNum(b));
}
/**
* 乘法
*
* 右移:
* b >> 1,表示右移1,最高位拿符號(hào)位來補(bǔ)。
* b >>>1, 表示右移1,最高位一律拿0來補(bǔ)。
*
* 支持負(fù)數(shù),道理不好解釋,負(fù)數(shù)的情況下到底發(fā)生了什么,跟補(bǔ)碼有關(guān),你只要知道同樣這套方法既支持正數(shù)也支持負(fù)數(shù)。
*/
public static int multi(int a, int b) {
int res = 0;
while(b != 0) {
if ((b & 1) != 0) {
res = add(res, a);
}
a <<= 1;
b >>>= 1;
}
return res;
}
public static boolean isNeg(int n) {
return n < 0;
}
/**
* 除法
*
* 這個(gè)方法在正式開始之前,a和b一定要都轉(zhuǎn)成正數(shù)
*
* x是非負(fù)的,第32位是符號(hào)位,肯定是0,所以沒必要右移31位。所以for循環(huán)i從30開始。
*
* 這里^的運(yùn)算是:非短路邏輯異或,兩邊的表達(dá)式都會(huì)執(zhí)行
* boolean a = true, b = false;
* boolean c = a ^ b; // true(不同時(shí)為 true/false)
* 如果需要邏輯異或(對(duì)布爾沒必要值),直接用 != 更直觀: boolean xor = (a != b); // 等效于 a ^ b
*
* a ^ b: a和b不同返回true,否則返回false
*
* 這個(gè)除法必須要求x和y轉(zhuǎn)成正數(shù)的形式。
* 這個(gè)方法一定是要能把a(bǔ)和b轉(zhuǎn)成正數(shù)才能繼續(xù)算,但系統(tǒng)最小值沒辦法轉(zhuǎn)絕對(duì)值,所以有局限。
*/
public static int div(int a, int b) {
int x = isNeg(a) ? negNum(a) : a;
int y = isNeg(b) ? negNum(b) : b;
int res = 0;
// x / y
for (int i=30; i>=0; i = minus(i, 1)) {
if ((x >> i) >= y) {
res |= (1 << i);
x = minus(x, y << i);
}
}
//如果a和b符號(hào)不一樣,取個(gè)負(fù)號(hào)返回;如果a和b負(fù)號(hào)一樣,直接返回答案
return isNeg(a) ^ isNeg(b) ? negNum(res) : res;
}
public static void main(String[] args) {
int a = 7;
int b = -3;
System.out.println(multi(a, b));
}
}
div方法解釋:
1.代碼中是x/y,為什么看到x右移?y去夠x,和x去夠y,其實(shí)是一碼事。
為什么是一碼事,看圖解:
y要左移幾位才能夠上x?3位。

同樣道理,x往右移幾位能讓y勾上自己?右移3位,所以x往右移看什么時(shí)候能夠上y,和y往左移看什么時(shí)候能夠上x,是一碼事。
為什么要讓x往右移,因?yàn)閥這個(gè)玩意,左移的話有可能移動(dòng)到符號(hào)位上,就干了。
如下圖,y左移的話,左移3位的話和x相等,要再左移1位才知道比x大了,才知道是左移3位。移4位的時(shí)候才知道上一位才是我應(yīng)該來的位置,但要過一位才能判斷出來,就有風(fēng)險(xiǎn)了。
而x往右移沒這個(gè)風(fēng)險(xiǎn)。

如下圖,x是一個(gè)很大的數(shù),y往左移去夠x,左移n位正好等于x,但是還需要左移一位大于x了后才知道上一位是該來的位置,繼續(xù)左移一位后到符號(hào)位了,y就變成負(fù)數(shù)了,還是不大于x啊。它還會(huì)左移,就會(huì)發(fā)生不可預(yù)知的錯(cuò)誤。

x這個(gè)數(shù)如果不大沒什么問題。就怕它很大。


所以為了安全起見,讓x去夠y。右移不會(huì)牽扯到改符號(hào)位的問題。右移不會(huì)有溢出問題。
2.除不盡會(huì)怎樣?比如9 / 4:

a - (b << 1)之后:

a>>0后,并不比b大,for循環(huán)結(jié)束。res 等于2進(jìn)制的 10,返回2。所以div方法除不盡也沒事。
--
浙公網(wǎng)安備 33010602011771號(hào)