题目
给定两个整数,被除数 dividend 和除数 divisor。将两数相除,要求不使用乘法、除法和 mod 运算符。
返回被除数 dividend 除以除数 divisor 得到的商。
整数除法的结果应当截去(truncate)其小数部分,例如:truncate(8.345) = 8 以及 truncate(-2.7335) = -2
示例 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 位有符号整数,其数值范围是 [−2^31, 2^31 − 1]。本题中,如果除法结果溢出,则返回 2^31 − 1。来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/divide-two-integers
分析
这个题看起来简单,实际上非常复杂,需要考虑的因素很多。下面一一分析这些因素。
(1)看到除法,首先要考虑除数为0,提示里说明了除数不为0,所以这个条件可以跳过
(2)溢出,到处是溢出风险,具体代码里说明
首先写一下思路,这个题不让用乘法、除法和mod运算。最暴力的方法就是每次增加一个divisor,去逼近dividend,这个时间复杂度是O(dividend/divisor)。
这个有没有办法进行优化呢?很明显是可以的,在逼近dividend的过程中,把步长修改一下,不是每次增加一个divisor,而是每次翻倍。翻倍操作的临界点是:当前置tryData再次翻倍就超过了dividend了;这个临界点其实是把dividend分开的一个临界点,tryData正好超过dividend的一半,dividend-tryData的部分可以再次用上面的方法进行计算。这就是一个递归思想了,当然也可以用迭代的方法,从tryData开始减半去尝试逼近dividend,这有一个贪心算法的思想。
递归:
这里用负数来表示dividend和divisor,这样可以避免越界问题。
本来,倍增和减半都可以用位移操作,但是负数的位移操作并不是除法,比如5 >> 1 = 2, -5 >> 1= -3。所以这里都避免了使用位移操作而是用加法。
class Solution {
public int divide(int dividend, int divisor) {
// 递归结束条件1
if (dividend == 0) return 0;
if (divisor == 1) return dividend;
if (divisor == -1) return dividend == Integer.MIN_VALUE ? Integer.MAX_VALUE : -dividend;
// 最终结果的符号
boolean flag = dividend >= 0 && divisor >= 0 || dividend < 0 && divisor < 0;
// 都用负数表示,因为用绝对值无法表示负数最小值,这样可以避免越界问题
dividend = dividend < 0 ? dividend : -dividend;
divisor = divisor < 0 ? divisor : -divisor;
// 递归结束条件2
if (dividend > divisor) return 0;
// 逼近dividend
int multi = 1;
int tryData = divisor;
while (tryData + tryData - dividend >= 0) { // 这里用减法而不是用tryData + tryData >= dividend是为了兼容越界问题
multi += multi;
tryData += tryData;
}
// 递归
int result = multi + divide(dividend - tryData, divisor);
return flag ? result : -result;
}
}
迭代:
这个是先找到临界点,然后用临界点逐步减半逼近dividend。(逼近过程中其实也可以加一个快速结束,比如当前的sum距离dividend其实已经不超过一个divisor了,就没必要尝试了,可以直接结束)。
用了long去保存dividend和divisor的绝对值。所以最终结果需要考虑一些特殊情况的越界问题。
class Solution {
public int divide(int dividend, int divisor) {
// 返回结果的符号
boolean flag = dividend >= 0 && divisor >=0 || dividend < 0 && divisor < 0;
// 求绝对值,用long来避免溢出
long longDividend = Math.abs((long)dividend);
long longDivisor = Math.abs((long)divisor);
// 被除数小于除数,返回0
if (longDividend < longDivisor) return 0;
// 倍增逼近dividend
long multi = 1, tryData = longDivisor;
while (tryData <= (longDividend >> 1)) {
tryData <<= 1;
multi <<= 1;
}
// 从临界点逐渐减半进行尝试,逼近dividend
long result = multi;
long sum = tryData;
while (multi > 1) {
tryData >>= 1;
multi >>= 1;
if (sum + tryData <= longDividend) {
sum += tryData;
result += multi;
}
}
// 处理越界问题,比如Integer.MIN_VALUE / -1其实是Integer.MAX_VALUE ,但是用long表示之后,可以计算出Integer.MAX_VALUE + 1
result = result > Integer.MAX_VALUE && flag ? Integer.MAX_VALUE : result;
return flag ? (int)result : (int)-result;
}
}
总结:
问题看似简单,实际上细节非常多,这样的题目需要多做一些,锻炼思维的缜密。另外递归的思想其实还是有一些绕的,一开始想到了用递归,但是没有想到具体的实现方法,还需要多练习。
耗时:2天
博客围绕LeetCode上两数相除问题展开,要求不使用乘、除和mod运算符。分析了需考虑的除数为0和溢出等因素,给出递归和迭代两种解法思路。递归用负数避免越界,迭代先找临界点再减半逼近,强调此类题可锻炼思维缜密性。

2545

被折叠的 条评论
为什么被折叠?



