29. Divide Two Integers [Medium]

本文探讨了在不允许使用乘除运算符和取模运算符的情况下,如何利用位运算处理32位整数除法的溢出问题。文章提供了三种解决方案,包括使用long类型和预处理输入的方法,并详细解释了处理溢出和边界情况的策略。通过位移和位运算,确保计算过程不会导致结果错误。
摘要由CSDN通过智能技术生成

不让用乘、除、取模等运算符,明显应该用位运算,但自己还是不会做

题中有很多边界情况的要求

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累加时也可以少一步位运算

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值