不让用乘、除、取模等运算符,明显应该用位运算,但自己还是不会做
题中有很多边界情况的要求
1. 整数存储范围只有32位,即解题过程中不能使用long类型
2. 结果有可能溢出,即要考虑和处理溢出的情况
如果不做任何处理:
(1) 两数若有Integer.MIN_VALUE的,转为正数就会溢出
(2) 位运算过程中,也会溢出
(3) 最终结果如果是 - Integer.MIN_VALUE(只有可能被除数为Integer.MIN_VALUE,且除数为-1),即结果本身就会溢出,如果不单独处理该情况,则无论计算过程如何,都免不了发生溢出
处理方法
对于(3),因为是真实结果溢出,所以无法避免,无论怎么解题都需要单独处理(dividend == Integer.MIN_VALUE && divisor == -1)的情况;
对于(1)(2),有以下两种解决方案
一、用long类型
用long类型保存res、取绝对值得到的newDividend、newDivisor,最后return的时候再转换为int类型。这样能得到正确答案,但是不符合题目中计算机只能存储32位整数的设定。
二、预处理输入
要保证使用int类型但不因为溢出导致结果错误,需要考虑到所有会溢出的case并处理:
对于(1),我们无法避免要将两个输入转换为绝对值处理,否则无法正常计算,所以只能提前判断输入的两个值是否为Integer.MIN_VALUE,并处理;
对于(2),运算过程中只有newDivisor左移的时候可能导致溢出。为了避免,可以在每次左移之前都判断左移之后是否会溢出:第一个循环条件设为 while ((newDividend >> 1) >= newDivisor),能保证newDivisor左移结果不超过newDividend,也即不超过整数范围。但这样仍然可能导致(newDividend >= newDivisor << 1)的edge case,因为右移可能损失一定精度,让右移结果小于newDividend / 2。为了避免漏掉这个edge case,可以在进入下一个while循环前就用当前times计算一次,且不减少times,这样进入循环后会再检查一次这个times还能不能再用一次(如果不这样,在下一个while循环中一个times只会被计算一次,参与一个loop之后就被--),就能把edge case考虑进去。
/**
* 本段思路:
* 除了加减,其他计算都用位运算(用位运算将除数以2的次数倍扩大)
* 先用一个while循环找到小于被除数的、最大的 除数的倍数,如果还有其他的除数的倍数,也一定小于它
* 再用另一个while循环从大到小找小于等于当前被除数剩余值的除数的倍数,将其从被除数中减去后继续找更小的
* 直到除数本身已经大于了被除数剩余值,result的绝对值就确定了,再根据之前记录的异号情况决定返回(int)res还是(int)-res
* 用long,不严格符合题目要求
* Runtime: 1 ms, faster than 99.98%
* Memory Usage: 36.1 MB, less than 54.24%
*/
class Solution {
public int divide(int dividend, int divisor) {
if (dividend == Integer.MIN_VALUE && divisor == -1) { // to avoid overflowing of the result
return Integer.MAX_VALUE;
}
boolean positive = (dividend >= 0 && divisor > 0) || (dividend <= 0 && divisor < 0);
long newDividend = Math.abs((long)dividend), newDivisor = Math.abs((long)divisor), res = 0;
int times = 0;
while (newDividend >= newDivisor) {
newDivisor <<= 1;
times++;
}
newDivisor >>= 1;
times--;
while (times >= 0) {
if (newDividend >= newDivisor) {
newDividend -= newDivisor;
res += 1 << times;
}
newDivisor >>= 1;
times--;
}
return positive ? (int)res : (int)-res;
}
}
/**
* 不用long类型的神仙做法
* 该做法有两个关键点
* 1. 被除数为Integer.MIN_VALUE且除数不为-1时的处理,从被除数抽出1或-1个除数,再调用自身方法,使当前被除数不再等于Integer.MIN_VALUE
* 2. 第一个while循环的条件,保证了newDivisor左移不会超出int范围
* Runtime: 1 ms, faster than 99.98%
* Memory Usage: 35.8 MB, less than 90.26%
*/
class Solution {
public int divide(int dividend, int divisor) {
if (dividend == Integer.MIN_VALUE) {
if (divisor == -1) {
return Integer.MAX_VALUE;
}
if (divisor < 0) {
return 1 + divide(dividend - divisor, divisor); // niubility
} else {
return -1 + divide(dividend + divisor, divisor); // niubility
}
}
if (divisor == Integer.MIN_VALUE) {
return 0; // if (dividend == Integer.MIN_VALUE), the result would be 1, but that case is already taken care of before
}
boolean positive = (dividend >= 0 && divisor > 0) || (dividend <= 0 && divisor < 0);
int newDividend = Math.abs(dividend), newDivisor = Math.abs(divisor), res = 0, times = 0;
while ((newDividend >> 1) >= newDivisor) { // ensure (newDivisor << 1) is less than newDividend before conducting the bit manipulation, to prevent overflowing on newDivisor
newDivisor <<= 1;
times++;
}
if (newDividend >= newDivisor) { // count the largest "times" once before the next "while loop" so that it would be counted twice, to prevent the edge case "(newDividend >> 1) < newDivisor, but (newDividend >= newDivisor << 1)"
newDividend -= newDivisor;
res += 1 << times;
}
while (times >= 0) {
if (newDividend >= newDivisor) {
newDividend -= newDivisor;
res += 1 << times;
}
times--;
newDivisor >>= 1;
}
return positive ? res : -res;
}
}
/**
* 在循环求最终结果的地方用了不同的实现
* 这里在内层while每次都从小到大找到比当前剩余被除数小的除数的最大2的次方倍,外层while循环保证剩余被除数大于等于除数
* 前一种实现是先找到除数的最大2的次方倍,再每次右移一位地从大往小找,直到除数恢复到本身
* 和前一种实现的时间效率谁更高不一定
* Runtime: 1 ms, faster than 99.98%
* Memory Usage: 36.1 MB, less than 68.83%
*/
class Solution {
public int divide(int dividend, int divisor) {
if (dividend == Integer.MIN_VALUE) {
if (divisor == -1) { // 唯一一种真实结果会越界的情况
return Integer.MAX_VALUE;
}
if (divisor > 0) { // 为了防止dividend取绝对值溢出
return -1 + divide(dividend + divisor, divisor);
} else {
return 1 + divide(dividend - divisor, divisor);
}
}
if (divisor == Integer.MIN_VALUE) { // 为了防止divisor取绝对值溢出
return 0; // 因为(dividend == Integer.MIN_VALUE)的情况已经被处理掉,其他情况下结果都只能是0
}
boolean isPositive = (divisor > 0 && dividend >= 0) || (divisor < 0 && dividend <= 0);
int res = 0, newDividend = Math.abs(dividend), newDivisor = Math.abs(divisor);
while (newDividend >= newDivisor) {
int tmp = newDivisor, time = 1;
while ((newDividend >> 1) >= tmp) {
tmp <<= 1;
time <<= 1;
}
newDividend -= tmp;
res += time;
}
return isPositive ? res : -res;
}
}
time可以直接像前两段那样记录2的几次方,也可以像最后一段那样记录具体是几倍
前者:每次newDivisor左移之后time++,res累加1 << time
后者:每次newDivisor左移之后time也左移一位,res直接累加time
个人感觉后者更好,因为位运算应该比加减运算符更快,而且直接算出几倍在res累加时也可以少一步位运算