题目描述

✅ LCR 001. 两数相除

image-20260928234610669

image-20260928234610672

题意分析

给定两个 32 位有符号整数 a、b,求整数商,舍弃小数部分并向零截断。除数保证不为零,计算中不能使用乘法、除法和取余运算符,也不能依赖比 32 位更宽的整数范围。

商溢出时返回最大 32 位整数,唯一需要截断的组合是最小负数除以负一。最小负数的绝对值无法作为正 int 保存,因此中间过程不能一律先转为正数。

解法:负数域倍增减法

核心思路

[!blue]

逐次减去除数虽然可以得到商,但商很大时需要过多循环。改为每轮先把除数不断翻倍,找到剩余量能够容纳的最大二进制倍数,再一次减去它,并把对应倍数加到商中。

本实现用负数保存被除数余量 a 与除数 b,用非负的 answer 保存已经累计的商的绝对值。正数取负总能在 32 位范围内表示,最小负数也无需转换;原结果是否同号在转换前记录,最后再应用符号。

外层的 a <= b 表示余量的绝对值还不小于除数。内层从 x = b、cnt = 1 开始,x 代表本轮要减去的负倍数,cnt 代表它含有多少份原除数。只要翻倍后仍能被余量容纳,就同时左移 x 与 cnt,直到不能继续。

左移前要先检查 x >= (Integer.MIN_VALUE >> 1),保证翻倍仍在负数范围内,再判断 a <= (x << 1)。顺序不能反,溢出后的倍数已经无法正确参与比较。选定后执行 answer += cnt、a -= x,后者把负余量向零推进。

始终有“原被除数的绝对值 = 已累计商对应的除数总量 + 当前余量绝对值”。最大可减倍数至少消耗一半当前余量,下一轮继续处理更小部分;当余量绝对值小于除数时结束,舍去它正好得到向零截断的商。

由于商按正数累计,需要提前处理 b == 1,直接返回 a,并截断最小负数除以负一的溢出组合。这两处处理后,其余商的绝对值都能放入正 int,cnt 和 answer 的累计才安全;这与全程累计负商的实现有所不同。

解题步骤

  1. 除数为一时返回被除数;最小负数除以负一时返回最大正整数。
  2. 记录两操作数是否同号,再把正操作数取负,令 answer = 0。
  3. 余量绝对值仍足够时,从一份除数开始,先验证移位安全,再不断翻倍寻找本轮最大可减倍数。
  4. 把倍数计入正商累计量,从负余量中减去该负倍数。
  5. 余量不足一份除数时结束,根据原符号返回 answer 或 -answer。

代码实现

class Solution {
    public int divide(int a, int b) {
        // 除数为 1 时商就是 a 本身,提前返回可以避开 a = MIN_VALUE 的溢出。
        if (b == 1) {
            return a;
        }

        // 唯一超出 int 上界的组合,按题意截断。
        if (a == Integer.MIN_VALUE && b == -1) {
            return Integer.MAX_VALUE;
        }

        boolean sign = (a > 0 && b > 0) || (a < 0 && b < 0);

        // 统一到负数域:负半轴容得下正半轴的全部取值,取负不会溢出。
        a = a > 0 ? -a : a;
        b = b > 0 ? -b : b;

        int answer = 0;

        while (a <= b) {
            int x = b;
            int cnt = 1;

            // 先卡住移位溢出,再判断翻倍后是否仍不超过剩余的被除数。
            while (x >= (Integer.MIN_VALUE >> 1) && a <= (x << 1)) {
                x <<= 1;
                cnt <<= 1;
            }

            answer += cnt;
            a -= x;
        }

        return sign ? answer : -answer;
    }
}
import (
    "math"
)

func divide(a int, b int) int {
    if b == 1 {
        return a
    }
    if a == math.MinInt32 && b == -1 {
        return math.MaxInt32
    }

    sign := (a > 0 && b > 0) || (a < 0 && b < 0)
    if a > 0 {
        a = -a
    }
    if b > 0 {
        b = -b
    }
    answer := 0

    for a <= b {
        x := b
        cnt := 1
        for x >= (math.MinInt32>>1) && a <= (x<<1) {
            x <<= 1
            cnt <<= 1
        }
        answer += cnt
        a -= x
    }

    if sign {
        return answer
    }
    return -answer
}

复杂度分析

  • 时间复杂度:设 W 为输入整数位宽,最坏 $O(W^2)$。外层每轮至少将余量绝对值减半,至多 W 轮;内层每轮都从原除数重新倍增,至多再做 W 次。题目固定位宽为 32,一般也可用输入数值写作 $O(1 + \log^2(\lvert a\rvert + 1))$。
  • 空间复杂度:$O(1)$,只维护符号、余量、商和本轮倍数等固定变量。

关键点总结

[!green]

  • 负数域保存余量,正数累计商,两类变量的范围问题要分别处理。
  • x 和 cnt 同步翻倍,分别表示可减数值与要增加的商。
  • 负数越小绝对值越大,因此 a <= x 才表示余量足够。
  • 必须先验证翻倍不溢出,再判断这个倍数是否能减去。
  • 内层每轮重新倍增,不能只看外层次数就把这份实现写成单次对数时间。

易错点总结

[!yellow]

  • 先对最小负数取正的绝对值会溢出,应将正操作数转负。
  • 本实现的 answer 是正的绝对值累计量,不能删除为它保障范围的两个入口处理。
  • 内层先移位后检查范围,可能已经把正确倍数变成错误值。
  • 只增加商却忘记扣除余量,会重复计算同一部分并无法结束。
  • 向零截断只舍弃不足一份除数的余量,不是对负数向下取整。

相似题目

题目 难度 关联与区别
50. Pow(x, n) 中等 同样按二进制拆分次数并成倍处理,原题通过平方加速幂,本题通过倍增除数确定商。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/80034294
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!