算法题分享 | 两数相除

63 阅读4分钟

image.png

题目

给你两个整数,被除数 dividend 和除数 divisor。将两数相除,要求 不使用 乘法、除法和取余运算。

整数除法应该向零截断,也就是截去(truncate)其小数部分。例如,8.345 将被截断为 8 ,-2.7335 将被截断至 -2 。

返回被除数 dividend 除以除数 divisor 得到的  。

注意: 假设我们的环境只能存储 32 位 有符号整数,其数值范围是 [−231,  231 − 1] 。本题中,如果商 严格大于 231 − 1 ,则返回 231 − 1 ;如果商 严格小于 -231 ,则返回 -231 。

 

示例 1:

输入: dividend = 10, divisor = 3
输出: 3
解释: 10/3 = 3.33333.. ,向零截断后得到 3

示例 2:

输入: dividend = 7, divisor = -3
输出: -2
解释: 7/-3 = -2.33333.. ,向零截断后得到 -2

 

提示:

  • -231 <= dividend, divisor <= 231 - 1
  • divisor != 0

题解

解题思路

为了方便讨论,设被除数为 x,除数为 y,商为 z, 即有 x / y = z,等价为 z * y = x,目的是在不使用乘 法、除法和取余运算的前提下找到满足该等式的 z。

由于 x 和 y 都是可正可负的,如果直接处理,则需要考虑四种情况。我们可以先把 x 和 y 转换为同号后再 开始计算,这样就无需单独考虑符号的问题。 由于转为正数有可能会溢出,所以把 x 和 y 都转换为负数处理。

在 x 和 y 均为负数,这样就有 z * y >= x(因为题目说明了商是向零取整的),所以问题就转换为了找到满足该公式的最大的 z,此时 z 也就是商。由于 z 是正数(或 0),所以查找范围也就是 1 ~ 231 - 1,如果找不到,那么就一定是 0。为了防止超时,我们需要在区间内使用二分查找算法找到目标值。

上面的方法并未能覆盖所有情况,对于一些边界情况,我们需要提前判断直接返回结果,例如当 x 为 -231 而 y 为 -1 时,结果是会溢出的,此时直接返回 231 - 1 即可,同时这里也相当于剪枝可以提高效率。

此外,对于 z * y >= x 也是一个难点,因为不能使用乘法。我们可以使用 快速乘 算法解决,基于二进制分解原理,将乘法运算转化为加法和位移操作。核心思想是将乘数 z 分解为二进制形式,然后根据每一位是否为 1 来决定是否将对应的 y 的倍数加到结果中。

在快速乘实现中,为了提高效率以及防止溢出,要加上剪枝判断,例如当前累加和 res 要加上 add 前先判断一下是否会超出范围,是就直接返回结果即可。注意,为了防止溢出,判断也要注意一些细节。例如要判断 res + add < x,由于 res + add 有可能会溢出,所以需要改为判断 res < x - add 即可(此处 res 和 add 均为负数,两个负数之差不可能会溢出)。

代码

class Solution {
    public int divide(int dividend, int divisor) {
        // 先处理边界情况
        if (dividend == Integer.MIN_VALUE) {
            if (divisor == -1) {
                return Integer.MAX_VALUE;
            } 

            if (divisor == 1) {
                return Integer.MIN_VALUE;
            }

            if (divisor == Integer.MIN_VALUE) {
                return 1;
            }
        }

        if (divisor == Integer.MIN_VALUE) {
            return 0;
        }

        if (dividend == 0) {
            return 0;
        }

        // 被除数和除数均转换为负数
        boolean reverse = false;
        int x = dividend;
        int y = divisor;
        if (dividend > 0) {
            x = -dividend;
            reverse = !reverse;
        }
        if (divisor > 0) {
            y = -divisor;
            reverse = !reverse;
        }

        // 普通情况就使用二分查找得到答案
        // dividend / divisor = z,即 z * divisor >= dividend,满足该条件的最大的 z 即为答案 
        int left = 1;
        int right = Integer.MAX_VALUE;
        int ans = 0;
        while (left <= right) {
            // 1. 要防止溢出 2. 不允许使用除法
            int mid = left + ((right - left) >> 1);
            if (checkByQuickAdd(x, y, mid)) {
                ans = mid;

                // 注意溢出
                if (mid == Integer.MAX_VALUE) {
                    break;
                }

                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return reverse ? -ans : ans;
    }

    /**
     * 通过快速乘法判断 z * y >= x 是否成立
     */
    private boolean checkByQuickAdd(int x, int y, int z) {
        int res = 0;
        int add = y;

        while (z != 0) {
            if ((z & 1) != 0) {
                if (res < x - add) {
                    return false;
                }
                res += add;
            }

            // 更新 add
            if (z != 1 && add < x - add) {
                return false;
            }
            add += add;

            z >>= 1;
        }
        return true;
    } 
}

复杂度分析

  • 时间复杂度:O(log² C) 其中 C 为 32 位整数的最大值。 二分查找时间复杂度为 O(log C),每次使用快速乘判断 z * y >= x 的时间复杂度也为 O(log C)(也可视为 O(32)),所以总时间复杂度为 O(log² C)。

  • 空间复杂度:O(1)

优质项目推荐

推荐一个可用于练手、毕业设计参考、增加简历亮点的项目。

lemon-puls/txing-oj-backend: Txing 在线编程学习平台,集在线做题、编程竞赛、即时通讯、文章创作、视频教程、技术论坛为一体