使用位运算技巧实现加减乘除
使用位运算技巧实现加减乘除
作者:Grey
原文地址:
说明
题目描述见:LeetCode 29. Divide Two Integers
原题目是:要求不使用乘法、除法和取模运算符实现除法。
我们把题目要求提高一点,不用加减乘除和取模运算符号,只使用位运算,不仅实现除法,也实现加减乘法。
实现加法
异或(^
)运算就是两个数对应二进制值的无进位相加,比如a = 13
且b = 20
,a ^ b
的结果如下(用二进制表示)
13 = \(2^3\) + \(2^2\) + \(2^0\),即:01101
20 = \(2^4\) + \(2^2\),即:10100
两个数异或结果如下
01101
^ 10100
--------
11001
结果就是: \(2^0\) + \(2^3\) + \(2^4\) = 25
思路可以转换一下,把加法用异或替换,得到两个数二进制无进位信息相加的结果。然后把这个结果加上进位信息,就是两个数相加的最终结果。
如上例,a ^ b = 25
, a
和b
相加的进位信息是01000
(十进制就是 8)。25 + 8 = 33
,正好是a + b
的结果。
抽象一下:
要计算a + b
先算a ^ b = a'
然后得到 a 和 b 相加的进位信息 b'
则a + b = a' + b'
。由于不能用加号,所以,我们只能逐个把进位信息叠加。
那么问题就变成:何时会产生进位信息?
a 和 b 的二进制对应位置上都是 1,则会产生进位,即:每次处理的进位信息为:(a & b) << 1
。
实现代码和详细注释信息如下
// 原始加法就是:无进位信息(异或) 结合(+) 进位信息
public int add(int a, int b) {
int sum = a;
while (b != 0) {
// 第一次进入这个循环,得到的是原始 a 和 原始 b 的异或结果,即无进位信息相加的结果
// 除了第一次,后面都是把a 和 b 相加的进位信息累加到 sum 中
sum = a ^ b;
// a & b -> 只有 a 和 b 对应的位置都是 1 的情况下,才会是1,其他情况都是0
// 而 a 和 b 对应位置都是 1 的情况下,也正好是进位信息会产生的地方
// << 1 表示把进位信息进位到高位进行累加
// 如果得到的结果不为 0 说明肯定有进位信息
b = (a & b) << 1;
a = sum;
}
return sum;
}
实现减法
a - b = a + (-b)
,
由于不能出现减号,所以,可以用加法来模拟一个数的相反数,因为
x
的相反数等于~x + 1
,即add(~x,1)
。
所以,减法实现如下
// 实现减法
public int minus(int a, int b) {
return add(a, negNum(b));
}
// 某个数n的相反数就是 ~n + 1,由于不能用+号
// 所以是 add(~n,1)
public int negNum(int n) {
return add(~n, 1);
}
实现乘法
小学算术计算两个数的乘法用的是如下方法,
比如 a = 12
,b = 22
,a * b
通过如下方式计算:
19 <--- a
x 22 <--- b
------
38
38
------
418
同样方法也适用于二进制,19 的二进制是 10011,22 的二进制是 10110 ,
10011 <--- a
x 10110 <--- b
-------------
00000
10011
10011
00000
10011
------------
110100010
110100010 就是 418。
其本质就是:
b 的二进制值(10110)从右往左开始,如果 b 的某一位是 1 ,则把 a 左移一位的值加到结果中,模拟 1 * a,如果 b 的某一位是 0,则 a 左移一位的值不加入结果中。 最后累加的结果就是a * b
的答案。
位运算实现乘法的完整代码和注释信息如下
public int multi(int a, int b) {
int res = 0;
while (b != 0) {
// b 的 二进制从右往左开始
if ((b & 1) != 0) {
// b 的某位是 1,则把 a 右移动一位的值加入进来
res = add(res, a);
}
a <<= 1;
// 带符号右移
b >>>= 1;
}
return res;
}
实现除法
实现除法的时候,为了防止溢出,我们首先把所有数先转换成正数来算。最后在判断两个数的符号决定是否把结果取其相反数。
假设 \(a / b = c\),则 \(a = b * c\),
用二进制来说明,如果:
\(a = b * 2^7 + b * 2^4 + b * 2^1\),
则 c 的二进制一定是\(10010010\)。
同理,如果:
\(a = b * 2^3 + b * 2^0\),
则 c 的二进制一定是\(1001\)。
抽象一下,如果\(a = b * 2 ^ x + b * 2 ^ y + b * 2 ^ z\),则 c 的二进制表示中: x 位置,y 位置,z 位置一定是 1,其他位置都是 0。
所以,我们的思路可以转换成 a 是由几个 【b * 2的某次方】的结果组成,
使用位运算实现除法的核心代码如下:
public boolean isNeg(int n) {
return n < 0;
}
// 实现除法
// 假设 $a / b = c$,则 $a = b * c$,
//用二进制来说明,如果:
//$a = b * 2^7 + b * 2^4 + b * 2^1$,
//则 c 的二进制一定是$10010010$。
//同理,如果:
//$a = b * 2^3 + b * 2^0$,
//则 c 的二进制一定是$1001$。
//抽象一下,如果$a = b * 2 ^ x + b * 2 ^ y + b * 2 ^ z$,则 c 的二进制表示中: x 位置,y 位置,z 位置一定是 1,其他位置都是 0。
//所以,我们的思路可以转换成 a 是由几个 【b * 2的某次方】的结果组成,
public int div(int x, int y) {
// 把复数全部转换为正数来算
int a = isNeg(x) ? negNum(x) : x;
int b = isNeg(y) ? negNum(y) : y;
int res = 0;
// 以下就是:for (int i = 31; i > -1; i--)
for (int i = 31; i > negNum(1); i = minus(i, 1)) {
if ((a >> i) >= b) {
res |= (1 << i);
a = minus(a, b << i);
}
}
return isNeg(x) ^ isNeg(y) ? negNum(res) : res;
}
其中
for (int i = 31; i > negNum(1); i = minus(i, 1))
就是
for (int i = 31; i > -1; i--)
循环体内的
if ((a >> i) >= b) {
res |= (1 << i);
a = minus(a, b << i);
}
就是让 a 不断尝试其值是否由【b * 2的某个次方】相加得到。
由于有一些特殊情况,比如在 Java 中,int 类型的系统最小值Integer.MIN_VALUE
的相反数依然是Integer.MIN_VALUE
如果a = Integer.MIN_VALUE
且b != -1 && b != Integer.MIN_VALUE
,则在调用div(a,b)
的时候,应该考虑到 a 取相反数还是其自身,所以需要特殊处理以下,即
\(a / b\)应该通过如下方式来计算,先让\(a + 1\),这个操作目的就是调用div
的时候可以正常取相反数,
然后可以正常调用div
方法,得到:\(c = (a + 1) / b\)的结果,
接下来就是想办法把\((a + 1) / b = c\)这个结论转换成题目要求的\(a/b\)的结果,
接着\(a - (b * c) = d\)
然后\(d / b = e\)
最后\(c + e = (((b * c) / b) + ((a - (b * c)) / b)) = (b * c + a - (b*c))/b = a / b\)
即得到\(a / b\)的值。
根据 LeetCode 题目要求,有如下结论:
Integer.MIN_VALUE / (-1) == Integer.MAX_VALUE
。
所以除法的主流程代码如下(主要是根据题目要求和系统最小值的特殊情况进行了一些边界讨论,见注释说明内容)
public int divide(int a, int b) {
if (b == Integer.MIN_VALUE) {
return a == Integer.MIN_VALUE ? 1 : 0;
}
// 除数不是系统最小
if (a == Integer.MIN_VALUE) {
if (b == negNum(1)) {
// leetcode的题目要求
return Integer.MAX_VALUE;
}
// 求 a / b
// 先算 (a + 1)/b = c
// 然后算 a - (b*c) = d
// 然后 d / b = e
// c + e = (a+1)/b + (a-(b*c))/b = a / b
int c = div(add(a, 1), b);
return add(c, div(minus(a, multi(c, b)), b));
}
// dividend不是系统最小,divisor也不是系统最小
return div(a, b);
}
完整代码见
class Solution {
// 主方法
public int divide(int a, int b) {
if (b == Integer.MIN_VALUE) {
return a == Integer.MIN_VALUE ? 1 : 0;
}
// 除数不是系统最小
if (a == Integer.MIN_VALUE) {
if (b == negNum(1)) {
// leetcode的题目要求
return Integer.MAX_VALUE;
}
// 求 a / b
// 先算 (a + 1)/b = c
// 然后算 a - (b*c) = d
// 然后 d / b = e
// c + e = (a+1)/b + (a-(b*c))/b = a / b
int c = div(add(a, 1), b);
return add(c, div(minus(a, multi(c, b)), b));
}
// dividend不是系统最小,divisor也不是系统最小
return div(a, b);
}
public int add(int a, int b) {
int sum = a;
while (b != 0) {
// 第一次进入这个循环,得到的是原始 a 和 原始 b 的异或结果,即无进位信息相加的结果
// 除了第一次,后面都是把a 和 b 相加的进位信息累加到 sum 中
sum = a ^ b;
// a & b -> 只有 a 和 b 对应的位置都是 1 的情况下,才会是1,其他情况都是0
// 而 a 和 b 对应位置都是 1 的情况下,也正好是进位信息会产生的地方
// << 1 表示把进位信息进位到高位进行累加
// 如果得到的结果不为 0 说明肯定有进位信息
b = (a & b) << 1;
a = sum;
}
return sum;
}
// 某个数n的相反数就是 ~n + 1,由于不能用+号
// 所以是 add(~n,1)
public int negNum(int n) {
return add(~n, 1);
}
public int minus(int a, int b) {
return add(a, negNum(b));
}
// 参考小学算乘法的过程。
// 比如 `a = 12`,`b = 22`,`a * b`通过如下方式计算:
// **b 的二进制值(10110)从右往左开始,如果 b 的某一位是 1 ,
// 则把 a 左移一位的值加到结果中,
// 模拟 1 * a,如果 b 的某一位是 0,
// 则 a 左移一位的值不加入结果中。** 最后累加的结果就是`a * b`的答案。
public int multi(int a, int b) {
int res = 0;
while (b != 0) {
// b 的 二进制从右往左开始
if ((b & 1) != 0) {
// b 的某位是 1,则把 a 右移动一位的值加入进来
res = add(res, a);
}
a <<= 1;
// 带符号右移
b >>>= 1;
}
return res;
}
public boolean isNeg(int n) {
return n < 0;
}
// 实现除法
// 假设 $a / b = c$,则 $a = b * c$,
//用二进制来说明,如果:
//$a = b * 2^7 + b * 2^4 + b * 2^1$,
//则 c 的二进制一定是$10010010$。
//同理,如果:
//$a = b * 2^3 + b * 2^0$,
//则 c 的二进制一定是$1001$。
//抽象一下,如果$a = b * 2 ^ x + b * 2 ^ y + b * 2 ^ z$,则 c 的二进制表示中: x 位置,y 位置,z 位置一定是 1,其他位置都是 0。
//所以,我们的思路可以转换成 a 是由几个 【b * 2的某次方】的结果组成,
public int div(int x, int y) {
// 把复数全部转换为正数来算
int a = isNeg(x) ? negNum(x) : x;
int b = isNeg(y) ? negNum(y) : y;
int res = 0;
for (int i = 31; i > negNum(1); i = minus(i, 1)) {
if ((a >> i) >= b) {
res |= (1 << i);
a = minus(a, b << i);
}
}
return isNeg(x) ^ isNeg(y) ? negNum(res) : res;
}
}
更多
参考资料
本文来自博客园,作者:Grey Zeng,转载请注明原文链接:https://www.cnblogs.com/greyzeng/p/16637476.html