leetcode-第29题 两数相除

博客围绕LeetCode上两数相除问题展开,要求不使用乘、除和mod运算符。分析了需考虑的除数为0和溢出等因素,给出递归和迭代两种解法思路。递归用负数避免越界,迭代先找临界点再减半逼近,强调此类题可锻炼思维缜密性。

题目

给定两个整数,被除数 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天     

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值