前端算法第一七二弹-两数相除

120 阅读2分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第28天,点击查看活动详情

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

二分查找

我们记被除数为 XX,除数为 YY,并且 XXYY 都是负数。我们需要找出 X/YX/Y 的结果 ZZZZ 一定是正数或 0。

根据除法以及余数的定义,我们可以将其改成乘法的等价形式,即:

Z×YX>(Z+1)×YZ×Y≥X>(Z+1)×Y

因此,我们可以使用二分查找的方法得到 ZZ,即找出最大ZZ 使得 Z×YXZ \times Y \geq X 成立。

由于我们不能使用乘法运算符,因此我们需要使用「快速乘」算法得到 Z×YZ \times Y 的值。「快速乘」算法与「快速幂」类似,前者通过加法实现乘法,后者通过乘法实现幂运算。

细节

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

首先,二分查找的下界为 1,上界为 23112^{31} - 1。唯一可能出现的答案为 2312^{31} 的情况已经被我们在「前言」部分进行了特殊处理,因此答案的最大值为 23112^{31} - 1。如果二分查找失败,那么答案一定为 0。

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

var divide = function(dividend, divisor) {
    const MAX_VALUE = 2 ** 31 - 1, MIN_VALUE = -(2 ** 31);
    // 考虑被除数为最小值的情况
    if (dividend === MIN_VALUE) {
        if (divisor === 1) {
            return MIN_VALUE;
        }
        if (divisor === -1) {
            return MAX_VALUE;
        }
    }
    // 考虑除数为最小值的情况
    if (divisor === MIN_VALUE) {
        return dividend === MIN_VALUE ? 1 : 0;
    }
    // 考虑被除数为 0 的情况
    if (dividend === 0) {
        return 0;
    }
    
    // 一般情况,使用二分查找
    // 将所有的正数取相反数,这样就只需要考虑一种情况
    let rev = false;
    if (dividend > 0) {
        dividend = -dividend;
        rev = !rev;
    }
    if (divisor > 0) {
        divisor = -divisor;
        rev = !rev;
    }
    
    let left = 1, right = MAX_VALUE, ans = 0;
    while (left <= right) {
        // 注意溢出,并且不能使用除法
        const mid = left + ((right - left) >> 1);
        const check = quickAdd(divisor, mid, dividend);
        if (check) {
            ans = mid;
            // 注意溢出
            if (mid === MAX_VALUE) {
                break;
            }
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }

    return rev ? -ans : ans;
}

// 快速乘
const quickAdd = (y, z, x) => {
    // x 和 y 是负数,z 是正数
    // 需要判断 z * y >= x 是否成立
    let result = 0, add = y;
    while (z !== 0) {
        if ((z & 1) !== 0) {
            // 需要保证 result + add >= x
            if (result < x - add) {
                return false;
            }
            result += add;
        }
        if (z !== 1) {
            // 需要保证 add + add >= x
            if (add < x - add) {
                return false;
            }
            add += add;
        }
        // 不能使用除法
        z >>= 1;
    }
    return true;
};