LC 29. 两数相除

题目描述

这是 LeetCode 上的 29. 两数相除 ,难度为 中等

给定两个整数,被除数 dividend 和除数 divisor

将两数相除,要求不使用乘法、除法和 mod 运算符。

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

整数除法的结果应当截去(truncate)其小数部分,例如:truncate(8.345) = 8 以及 truncate(-2.7335) = -2

示例 1:

1
2
3
4
5
输入: dividend = 10, divisor = 3

输出: 3

解释: 10/3 = truncate(3.33333..) = truncate(3) = 3

示例 2:
1
2
3
4
5
输入: dividend = 7, divisor = -3

输出: -2

解释: 7/-3 = truncate(-2.33333..) = -2

提示:

  • 被除数和除数均为 $32$ 位有符号整数。
  • 除数不为 $0$。
  • 假设我们的环境只能存储 32 位有符号整数,其数值范围是 [−$2^{31}$, $2^{31}$ − 1]。本题中,如果除法结果溢出,则返回 $2^{31}$ − 1。

基本分析

主要的歧义在于第三点限制「假设我们的环境只能存储 $32$ 位有符号整数,其数值范围是 $[−2^{31}, 2^{31} − 1]$。本题中,如果除法结果溢出,则返回 $2^{31} − 1$」的理解。

该限制有两种理解方式:

  1. 不限制算法使用 long,只是解释为什么在溢出时,返回 $2^{31} − 1$;
  2. 限制算法使用 long

原始题解在 这里


理解一(不限制 long

当不限制使用 long 时,基本思路为:

  • 首先,dividenddivisor 均有「正数」和「负数」两种可能,当且仅当其中一者为负数时,结果为负,为了方便,我们可以先记录最终结果的正负号,然后将 dividenddivisor 都当成正数来处理;
  • 现在两者都满足 $x >= 0$,然后利用 dividenddivisor 均为 int,可以确定答案的绝对值落在 $[0, dividend]$ 范围内(当且仅当 divisor 是范围在 $[0, 1]$ 的浮点数时,答案会大于 dividend);
  • 假设答案为 $x$,那么在以 $x$ 为分割点的整数数轴上,具有「二段性」,因此我们可以二分找到该分割点:
    • 大于 $x$ 的数 $y$ 满足 $y * b > a$;
    • 小于等于 $x$ 的数 $y$ 满足 $y * b <= a$;
  • 根据「二段性」分析,我们发现二分的 check 实现需要用到乘法,因此我们需要实现一个「不用乘法符号」的乘法实现(这可以使用倍增思想来实现 mul 操作)。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Solution {
int INF = Integer.MAX_VALUE;
public int divide(int _a, int _b) {
long a = _a, b = _b;
boolean flag = false;
if ((a < 0 && b > 0) || (a > 0 && b < 0)) flag = true;
if (a < 0) a = -a;
if (b < 0) b = -b;
long l = 0, r = a;
while (l < r) {
long mid = l + r + 1 >> 1;
if (mul(mid, b) <= a) l = mid;
else r = mid - 1;
}
r = flag ? -r : r;
if (r > INF || r < -INF - 1) return INF;
return (int)r;
}
long mul(long a, long k) {
long ans = 0;
while (k > 0) {
if ((k & 1) == 1) ans += a;
k >>= 1;
a += a;
}
return ans;
}
}

  • 时间复杂度:在 $[0, a]$ 范围内二分操作,复杂度为$O(\log{a})$;倍增乘法的与操作数的二进制长度相关,复杂度为 $O(\log{b})$。整体复杂度为 $O(\log{a} \times \log{b})$
  • 空间复杂度:$O(1)$

理解二(限制 long

对于全程不使用 long 的做法,我们需要将所有数映射到负数进行处理(以 $0$ 为分割点,负数所能表示的范围更大)。

基本思路为:

  • 起始先对边界情况进行特判;
  • 记录最终结果的符号,并将两数都映射为负数;
  • 可以预处理出倍增数组,或采取逐步增大 dividend 来逼近 divisor 的方式。

由于操作数都是负数,因此自倍增过程中,如果操作数小于 INT_MIN 的一半(-1073741824),则代表发生溢出。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
int MIN = Integer.MIN_VALUE, MAX = Integer.MAX_VALUE;
int LIMIT = -1073741824; // MIN 的一半
public int divide(int a, int b) {
if (a == MIN && b == -1) return MAX;
boolean flag = false;
if ((a > 0 && b < 0) || (a < 0 && b > 0)) flag = true;
if (a > 0) a = -a;
if (b > 0) b = -b;
int ans = 0;
while (a <= b){
int c = b, d = -1;
while (c >= LIMIT && d >= LIMIT && c >= a - c){
c += c; d += d;
}
a -= c;
ans += d;
}
return flag ? ans : -ans;
}
}

  • 时间复杂度:$O(\log{a} \times \log{b})$
  • 空间复杂度:$O(1)$

最后

这是我们「刷穿 LeetCode」系列文章的第 No.29 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!