LeetCode-29-两数相除

题目

来源:LeetCode.

给定两个整数,被除数 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 位有符号整数,其数值范围是 [ − 2 31 , 2 31 − 1 ] [−2^{31}, 2^{31 − 1}] [231,2311]。本题中,如果除法结果溢出,则返回 2 31 − 1 2^{31 − 1} 2311

接下来看一下解题思路:

思路一:常规解法:

    除法其实就是看被除数可以被 除数减几次。
    根据题目要求还需要考虑溢出问题:
如果除法结果溢出,那么我们需要返回 2 31 − 1 2^{31 - 1} 2311 作为答案。所以,可以首先对于溢出或者容易出错的边界情况进行讨论:

  • 当被除数为 32 32 32 位有符号整数的最小值 − 2 31 -2^{31} 231 时:
            如果除数为 1 1 1,那么我们可以直接返回答案 − 2 31 -2^{31} 231
            如果除数为 − 1 −1 1,那么答案为 2 31 2^{31} 231,产生了溢出。此时我们需要返回 2 31 − 1 2^{31 - 1} 2311
  • 当除数为 32 32 32 位有符号整数的最小值 − 2 31 -2^{31} 231时:
            如果被除数同样为 − 2 31 -2^{31} 231,那么可以直接返回答案 1 1 1;对于其余的情况,返回答案 0 0 0
            当被除数为 0 0 0 时,可以直接返回答案 0 0 0

对于一般的情况,根据除数和被除数的符号,需要考虑 4 4 4 种不同的可能性。因此,为了方便编码,可以将被除数或者除数取相反数,使得它们符号相同。

如果我们将被除数和除数都变为正数,那么可能会导致溢出。例如当被除数为 − 2 31 -2^{31} 231时,它的相反数 2 31 2^{31} 231产生了溢出。因此,可以考虑将被除数和除数都变为负数,这样就不会有溢出的问题,在编码时只需要考虑 1 1 1 种情况了。

如果我们将被除数和除数的其中(恰好)一个变为了正数,那么在返回答案之前,我们需要对答案也取相反数。

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;
}
思路三:二分查找:

    根据「思路一」的讨论,记被除数为 X X X,除数为 Y Y Y,并且 X X X Y Y Y 都是负数。需要找出 X / Y X/Y X/Y 的结果 Z Z Z Z Z Z 一定是正数或 0 0 0

根据除法以及余数的定义,我们可以将其改成乘法的等价形式,即:
Z × Y ≥ X > ( Z + 1 ) × Y Z \times Y \geq X > (Z+1) \times Y Z×YX>(Z+1)×Y
因此,可以使用二分查找的方法得到 Z Z Z,即找出最大的 Z Z Z 使得 Z × Y ≥ X Z \times Y \geq X Z×YX 成立。

由于不能使用乘法运算符,因此需要使用「快速乘」算法得到 Z × Y Z \times Y Z×Y 的值。「快速乘」算法与「快速幂」类似,前者通过加法实现乘法,后者通过乘法实现幂运算。「快速乘」算法只需要在「快速幂」算法的基础上,将乘法运算改成加法运算即可。

细节

由于只能使用 32 32 32 位整数,因此二分查找中会有很多细节。

  • 首先,二分查找的下界为 1 1 1,上界为 2 31 − 1 2^{31- 1} 2311。唯一可能出现的答案为 2 31 2^{31} 231的情况已经在「思路一」部分进行了特殊处理,因此答案的最大值为 2 31 − 1 2^{31 - 1} 2311。如果二分查找失败,那么答案一定为 0 0 0

  • 在实现「快速乘」时,我们需要使用加法运算,然而较大的 Z Z Z 也会导致加法运算溢出。例如我们要判断 A + B A + B A+B 是否小于 C C C 时(其中 A , B , C A, B, C A,B,C 均为负数), A + B A + B A+B 可能会产生溢出,因此我们必须将判断改为 A < C − B A < C - B A<CB 是否成立。由于任意两个负数的差一定在 [ − 2 31 + 1 , 2 31 − 1 ] [-2^{31} + 1, 2^{31-1} ] [231+1,2311] 范围内,这样就不会产生溢出。

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;
}
复杂度分析
  • 时间复杂度: O ( log ⁡ 2 C ) O(\log^2 C) O(log2C),其中 C C C 表示 32 32 32 位整数的范围。二分查找的次数为 O ( log ⁡ C ) O(\log C) O(logC),其中的每一步我们都需要 O ( log ⁡ C ) O(\log C) O(logC) 使用「快速乘」算法判断 Z × Y ≥ X Z \times Y \geq X Z×YX 是否成立,因此总时间复杂度为 O ( log ⁡ 2 C ) O(\log^2 C) O(log2C)
  • 空间复杂度: O ( 1 ) O(1) O(1)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值