题目
给定两个整数,被除数 dividend 和除数 divisor。将两数相除,要求不使用乘法、除法和 mod 运算符。
返回被除数 dividend 除以除数 divisor 得到的商。
整数除法的结果应当截去(truncate)其小数部分,
例如:truncate(8.345) = 8 以及 truncate(-2.7335) = -2
说明:
当 needle 是空字符串时我们应当返回 0 。这与 C 语言的 strstr() 以及 Java 的 indexOf() 定义相符。
示例 1:
输入: dividend = 10, divisor = 3
输出: 3
解释: 10/3 = truncate(3.33333..) = truncate(3) = 3
示例 2:
输入: dividend = 7, divisor = -3
输出: -2
解释: 7/-3 = truncate(-2.33333..) = -2
提示:
被除数和除数均为 32 位有符号整数。
除数不为 0。
假设我们的环境只能存储 32 位有符号整数,其数值范围是 。本题中,如果除法结果溢出,则返回。
接下来看一下解题思路:
思路一:常规解法:
除法其实就是看被除数可以被 除数减几次。
根据题目要求还需要考虑溢出问题:
如果除法结果溢出,那么我们需要返回
- 当被除数为 位有符号整数的最小值 时:
如果除数为 ,那么我们可以直接返回答案 ;
如果除数为 ,那么答案为 ,产生了溢出。此时我们需要返回 。 - 当除数为 位有符号整数的最小值 时:
如果被除数同样为 ,那么可以直接返回答案 ;对于其余的情况,返回答案 。
当被除数为 时,可以直接返回答案 。
对于一般的情况,根据除数和被除数的符号,需要考虑
如果我们将被除数和除数都变为正数,那么可能会导致溢出。例如当被除数为 时,它的相反数 产生了溢出。因此,可以考虑将被除数和除数都变为负数,这样就不会有溢出的问题,在编码时只需要考虑
如果我们将被除数和除数的其中(恰好)一个变为了正数,那么在返回答案之前,我们需要对答案也取相反数。
public static int divide(int dividend, int divisor) {
// 考虑被除数为最小值的情况
if (dividend == Integer.MIN_VALUE) {
if (divisor == 1) {
return Integer.MIN_VALUE;
}
if (divisor == -1) {
return Integer.MAX_VALUE;
}
}
// 考虑除数为最小值的情况
if (divisor == Integer.MIN_VALUE) {
return dividend == Integer.MIN_VALUE ? 1 : 0;
}
// 考虑被除数为 0 的情况
if (dividend == 0) {
return 0;
}
// 将所有的正整数取反,就只用考虑一种情况
int rev= 1;
if (dividend > 0) {
dividend = -dividend;
rev *= -1;
}
if (divisor > 0) {
divisor = -divisor;
rev *= -1;
}
int ans = 0;
while (dividend <= divisor) {
dividend -= divisor;
++ans;
}
// 给结果带上符号
return rev*ans;
}
思路二:思路一优化:
上面这种思路显然效率比较低,对于一次次减是不可行的, 可以减除数的2倍,然后结果+2,4倍+4… 故不停的左移除数, 直到其大于被除数的一半, 然后减去, 右移除数使其小于被除数,减去…依次类推, 直到被除数小于原始除数.
public static int divide2(int dividend, int divisor) {
boolean symbol = true;
if (dividend > 0) {
dividend = -dividend;
symbol = false;
}
if (divisor > 0) {
divisor = -divisor;
symbol = !symbol;
}
int result = 0;
// 直到被除数小于原始除数
while (dividend <= divisor) {
int n = 1;
while (true) {
int compare = dividend >> n;
if (compare >= divisor) {
result -= (1 << (n - 1));
dividend = dividend - (divisor << (n - 1));
break;
}
n++;
}
}
return symbol ? (result == Integer.MIN_VALUE ? Integer.MAX_VALUE : -result) : result;
}
思路三:二分查找:
,除数为 ,并且 和 都是负数。需要找出 的结果 。 一定是正数或 。
根据除法以及余数的定义,我们可以将其改成乘法的等价形式,即:
因此,可以使用二分查找的方法得到 ,即找出最大的 使得
由于不能使用乘法运算符,因此需要使用「快速乘」算法得到 的值。「快速乘」算法与「快速幂」类似,前者通过加法实现乘法,后者通过乘法实现幂运算。「快速乘」算法只需要在「快速幂」算法的基础上,将乘法运算改成加法运算即可。
细节
由于只能使用
- 首先,二分查找的下界为 ,上界为 。唯一可能出现的答案为 的情况已经在「思路一」部分进行了特殊处理,因此答案的最大值为 。如果二分查找失败,那么答案一定为 。
- 在实现「快速乘」时,我们需要使用加法运算,然而较大的 也会导致加法运算溢出。例如我们要判断 是否小于 时(其中 均为负数), 可能会产生溢出,因此我们必须将判断改为 是否成立。由于任意两个负数的差一定在
public static int divide1(int dividend, int divisor) {
// 考虑被除数为最小值的情况
if (dividend == Integer.MIN_VALUE) {
if (divisor == 1) {
return Integer.MIN_VALUE;
}
if (divisor == -1) {
return Integer.MAX_VALUE;
}
}
// 考虑除数为最小值的情况
if (divisor == Integer.MIN_VALUE) {
return dividend == Integer.MIN_VALUE ? 1 : 0;
}
// 考虑被除数为 0 的情况
if (dividend == 0) {
return 0;
}
// 一般情况,使用二分查找
// 将所有的正整数取反,就只用考虑一种情况
boolean rev= false;
if (dividend > 0) {
dividend = -dividend;
rev = !rev;
}
if (divisor > 0) {
divisor = -divisor;
rev = !rev;
}
int left = 1;
int right = Integer.MAX_VALUE;
int ans = 0;
while (left <= right) {
// 注意溢出,不能使用除法
int mid = left + ((right - left) >> 1);
boolean check = quickAdd(divisor, mid, dividend);
if (check) {
ans = mid;
// 注意溢出
if (mid == Integer.MAX_VALUE) {
break;
}
left = mid + 1;
} else {
right = mid - 1;
}
}
return rev ? -ans : ans;
}
// 快速乘
private static boolean quickAdd(int y, int z, int x) {
// x 和 y 是负数, z 是正数
// 需要判断 z * y >= x 是否成立
int result = 0;
int add = y;
while (z != 0) {
// 处理乘数是奇数的情况
if ((z & 1) != 0) {
// 需要保证 result + add >= x
//保证 result >= x,
// 这里提前计算了result 值,所以新的result 值等于 result + add
if (result < x -add) { //以防溢出,提前计算result 值,并变换不等式
return false;
}
result += add;
}
//下面处理乘数为偶数的情况
//保证 y + y >= x ,这里的y值就是后面累加数
if (z != 1) {
// 需要保证 add + add >= x
//防溢出,提前计算y值,并变换不等式
if (add < x - add) {
return false;
}
add += add;
}
// 不能使用除法
z >>= 1;
}
return true;
}
复杂度分析
- 时间复杂度:,其中 表示 位整数的范围。二分查找的次数为 ,其中的每一步我们都需要 使用「快速乘」算法判断 是否成立,因此总时间复杂度为 。
- 空间复杂度:。