题目
链接:https://leetcode.cn/problems/xoh6Oh
给定两个整数 a 和 b ,求它们的除法的商 a/b ,要求不得使用乘号 ‘*’、除号 ‘/’ 以及求余符号 ‘%’ 。
思路
第一反应是,这题是找茬吧!
对于除法,自己能一下子想到的,就是两种思路。第一种是用减来代替除。第二个就是位运算。
当然,对于一些边界值,还是需要做一下特殊处理。这样既可以提高效率,也可以防止错误。
- 被除数是0
- 除数是1
- 如题中所说,32位整数,也就是int类型,要注意溢出的边界。
解法一:暴力解法
所谓除法,就是看被除数里有几个除数。那用被除数把除数都减掉就好了。
当然,这里需要统一把两个数都转换成正数,方便处理。计算完结果,再判断是否要加上正负号即可。
逻辑
- 处理边界值
- 判断除数和被除数是否有负数,取绝对值后,记录结果是否需要加负号
- 循环的用被除数减去除数
- 返回最终的结果
代码
public int divide(int a, int b) {
// 1.处理边界值
if (a == 0 || b == 1) {
return a;
}
// 溢出情况,即-2的31次方/-1,要将2的31次方处理成2的31次方-1
if (b == -1) {
return a == Integer.MIN_VALUE ? Integer.MAX_VALUE : -a;
}
// 2.判断结果是否为负数。只有a和b符号相同,结果才为正数。这里使用了^异或位运算符
boolean isNegativeResult = a > 0 ^ b > 0;
a = Math.abs(a);
b = Math.abs(b);
// 3.循环,用被除数减去除数,直到结果小于零
int result = 0;
while (a - b >= 0) {
result++;
a -= b;
}
// 4.根据前面的正负号返回结果
return isNegativeResult ? -result : result;
}
解法二:位运算
相比于循环做减法的暴力方式,位运算的效率要高很多。但是要注意的一点是,左移或右移只能对一个数做2的n次方的乘或者除。所以我们要不断的细化结果。
比如,17/3,对3右移2位是12,右移3位是24。17介于12和24之间,因此你要先记录,17里包含至少4个3(2的2次方),之后再看5(17-12)里面有几个3。依次这么计算下去。
逻辑
其实我们直接用位运算的逻辑,替换掉暴力运算中,循环做减法的那部分逻辑即可。
代码
public int divide(int a, int b) {
// 1.处理边界值
if (a == 0 || b == 1) {
return a;
}
// 溢出情况,即-2的31次方/-1,要将2的31次方处理成2的31次方-1
if (b == -1) {
return a == Integer.MIN_VALUE ? Integer.MAX_VALUE : -a;
}
// 2.判断结果是否为负数。只有a和b符号相同,结果才为正数。这里使用了^异或位运算符
boolean isNegativeResult = a > 0 ^ b > 0;
a = Math.abs(a);
b = Math.abs(b);
// 3.位运算逻辑
int result = 0;
// 因为最大是32位的数,因此我们从31开始
for (int i = 31; i >= 0; i--) {
// 因为对b左移会出现溢出的问题,因此我们对a做右移。
// 又因为-2147483648在上一层的处理中,取绝对值后依旧是-2147483648,所以这里用无符号右移
// 用(a >>> i) - b >= 0 而不是(a >>> i) >= b 也是为了防止溢出。
if ((a >>> i) - b >= 0) {
a = a - (b << i);
result += 1 << i;
}
}
// 4.根据前面的正负号返回结果
return isNegativeResult ? -result : result;
}
无关紧要的进一步优化
这里我发现一个问题,即两种方案中的第二步,判断正负号以及取绝对值的逻辑,会影响1ms的性能。
即原有逻辑:
// 2.判断结果是否为负数。只有a和b符号相同,结果才为正数。这里使用了^异或位运算符
boolean isNegativeResult = a > 0 ^ b > 0;
a = Math.abs(a);
b = Math.abs(b);
改为下面的丑陋的写法:
boolean isNegativeResult = false;
if (a > 0 && b < 0) {
b = -b;
isNegativeResult = true;
} else if (a < 0 && b < 0) {
a = -a;
b = -b;
} else if (a < 0 && b > 0) {
a = -a;
isNegativeResult = true;
}
解法二的时间会优化为0ms。
当然,这并不是很重要了。
扩展知识点
这里补充一些本题涉及到的相关知识点。
位运算符
二进制中位相关的运算符。按照我的理解,位运算符就是对每个位之间的运算。
- &运算符:与运算符,两个对应的位都为1时,结果为1,否则为0。
- |运算符:或运算符,两个对应的位都为0时,结果为0,否则为1。
- ^运算符:异或运算符,两个对应的位相同时为1,不同为0。
- ~运算符:取反运算符,对每个位取反。
- <<左移运算符:二进制数的每一位都向左移动n位,低位补0。数值大小变为原来的2的n次方倍。
- >>右移运算符:二进制数的每一位都向右移动n位,如果该数为正,则高位补0,若为负数,则高位补1。移出的部分舍弃。数值大小缩小为原来的2的n次方倍(舍弃余数)。
- >>>无符号右移运算符:也叫逻辑右移,二进制数的每一位都向右移动n位,即若该数为正,则高位补0,而若该数为负数,则右移后高位同样补0。
运算符优先级
位运算符优先级低于加减,因此在代码中,需要将位运算括起来。