题目

给定两个整数,被除数 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 位有符号整数,其数值范围是 JAVA两个正数相除向上取整 两个整数相除代码_leetcode。本题中,如果除法结果溢出,则返回JAVA两个正数相除向上取整 两个整数相除代码_ide_02

接下来看一下解题思路:

思路一:常规解法:

    除法其实就是看被除数可以被 除数减几次。
    根据题目要求还需要考虑溢出问题:
如果除法结果溢出,那么我们需要返回 JAVA两个正数相除向上取整 两个整数相除代码_leetcode_03

  • 当被除数为 JAVA两个正数相除向上取整 两个整数相除代码_ide_04 位有符号整数的最小值 JAVA两个正数相除向上取整 两个整数相除代码_JAVA两个正数相除向上取整_05 时:
            如果除数为 JAVA两个正数相除向上取整 两个整数相除代码_leetcode_06,那么我们可以直接返回答案 JAVA两个正数相除向上取整 两个整数相除代码_JAVA两个正数相除向上取整_05
            如果除数为 JAVA两个正数相除向上取整 两个整数相除代码_JAVA两个正数相除向上取整_08,那么答案为 JAVA两个正数相除向上取整 两个整数相除代码_leetcode_09,产生了溢出。此时我们需要返回 JAVA两个正数相除向上取整 两个整数相除代码_ide_10
  • 当除数为 JAVA两个正数相除向上取整 两个整数相除代码_ide_04 位有符号整数的最小值 JAVA两个正数相除向上取整 两个整数相除代码_JAVA两个正数相除向上取整_05时:
            如果被除数同样为 JAVA两个正数相除向上取整 两个整数相除代码_JAVA两个正数相除向上取整_05,那么可以直接返回答案 JAVA两个正数相除向上取整 两个整数相除代码_leetcode_06;对于其余的情况,返回答案 JAVA两个正数相除向上取整 两个整数相除代码_ide_15
            当被除数为 JAVA两个正数相除向上取整 两个整数相除代码_ide_15 时,可以直接返回答案 JAVA两个正数相除向上取整 两个整数相除代码_ide_15

对于一般的情况,根据除数和被除数的符号,需要考虑 JAVA两个正数相除向上取整 两个整数相除代码_ide_18

如果我们将被除数和除数都变为正数,那么可能会导致溢出。例如当被除数为 JAVA两个正数相除向上取整 两个整数相除代码_算法_19时,它的相反数 JAVA两个正数相除向上取整 两个整数相除代码_算法_20产生了溢出。因此,可以考虑将被除数和除数都变为负数,这样就不会有溢出的问题,在编码时只需要考虑 JAVA两个正数相除向上取整 两个整数相除代码_算法_21

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

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

JAVA两个正数相除向上取整 两个整数相除代码_JAVA两个正数相除向上取整_22,除数为 JAVA两个正数相除向上取整 两个整数相除代码_leetcode_23,并且 JAVA两个正数相除向上取整 两个整数相除代码_JAVA两个正数相除向上取整_22JAVA两个正数相除向上取整 两个整数相除代码_leetcode_23 都是负数。需要找出 JAVA两个正数相除向上取整 两个整数相除代码_ide_26 的结果 JAVA两个正数相除向上取整 两个整数相除代码_JAVA两个正数相除向上取整_27JAVA两个正数相除向上取整 两个整数相除代码_JAVA两个正数相除向上取整_27 一定是正数或 JAVA两个正数相除向上取整 两个整数相除代码_leetcode_29

根据除法以及余数的定义,我们可以将其改成乘法的等价形式,即:
JAVA两个正数相除向上取整 两个整数相除代码_JAVA两个正数相除向上取整_30
因此,可以使用二分查找的方法得到 JAVA两个正数相除向上取整 两个整数相除代码_JAVA两个正数相除向上取整_27,即找出最大的 JAVA两个正数相除向上取整 两个整数相除代码_JAVA两个正数相除向上取整_27 使得 JAVA两个正数相除向上取整 两个整数相除代码_算法_33

由于不能使用乘法运算符,因此需要使用「快速乘」算法得到 JAVA两个正数相除向上取整 两个整数相除代码_二分查找_34 的值。「快速乘」算法与「快速幂」类似,前者通过加法实现乘法,后者通过乘法实现幂运算。「快速乘」算法只需要在「快速幂」算法的基础上,将乘法运算改成加法运算即可。

细节

由于只能使用 JAVA两个正数相除向上取整 两个整数相除代码_二分查找_35

  • 首先,二分查找的下界为 JAVA两个正数相除向上取整 两个整数相除代码_leetcode_36,上界为 JAVA两个正数相除向上取整 两个整数相除代码_二分查找_37。唯一可能出现的答案为 JAVA两个正数相除向上取整 两个整数相除代码_算法_38的情况已经在「思路一」部分进行了特殊处理,因此答案的最大值为 JAVA两个正数相除向上取整 两个整数相除代码_算法_39。如果二分查找失败,那么答案一定为 JAVA两个正数相除向上取整 两个整数相除代码_二分查找_40
  • 在实现「快速乘」时,我们需要使用加法运算,然而较大的 JAVA两个正数相除向上取整 两个整数相除代码_算法_41 也会导致加法运算溢出。例如我们要判断 JAVA两个正数相除向上取整 两个整数相除代码_leetcode_42 是否小于 JAVA两个正数相除向上取整 两个整数相除代码_二分查找_43 时(其中 JAVA两个正数相除向上取整 两个整数相除代码_ide_44 均为负数),JAVA两个正数相除向上取整 两个整数相除代码_leetcode_42 可能会产生溢出,因此我们必须将判断改为JAVA两个正数相除向上取整 两个整数相除代码_ide_46 是否成立。由于任意两个负数的差一定在 JAVA两个正数相除向上取整 两个整数相除代码_JAVA两个正数相除向上取整_47
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;
}
复杂度分析
  • 时间复杂度:JAVA两个正数相除向上取整 两个整数相除代码_二分查找_48,其中 JAVA两个正数相除向上取整 两个整数相除代码_JAVA两个正数相除向上取整_49 表示 JAVA两个正数相除向上取整 两个整数相除代码_ide_04 位整数的范围。二分查找的次数为 JAVA两个正数相除向上取整 两个整数相除代码_leetcode_51,其中的每一步我们都需要 JAVA两个正数相除向上取整 两个整数相除代码_leetcode_51 使用「快速乘」算法判断 JAVA两个正数相除向上取整 两个整数相除代码_ide_53 是否成立,因此总时间复杂度为 JAVA两个正数相除向上取整 两个整数相除代码_二分查找_48
  • 空间复杂度:JAVA两个正数相除向上取整 两个整数相除代码_算法_55