题目描述

✅ 剑指 Offer 65. 不用加减乘除做加法

image-20261001230752597

image-20260929094913234

题意分析

计算两个整数的和,但函数体中不能使用加、减、乘、除运算符。输入可能包含零和负数,不能只处理正整数。

二进制加法可以拆成两部分:各位先不考虑进位得到的结果,以及需要送往更高一位的进位。前者可以用异或计算,后者可以用按位与再左移计算,反复合并这两部分即可。

解法:异或求和,按位与求进位

核心思路

[!blue]

先看某一位的两个二进制数字:只有其中一个为 1 时,本位结果是 1,不会进位;两个都为 1 时,本位留下 0,向高一位进 1。因此 a ^ b 给出所有位不考虑进位的结果,a & b 标记会产生进位的位置,再左移一位得到 (a & b) << 1。

两部分合起来始终等价于原来的加法,但无进位结果与进位还可能在某些位同时为 1,需要继续处理。每轮先用旧的 a、b 保存 carry,再令 a 更新为异或结果、b 更新为 carry,把同一个问题转成“部分和与剩余进位相加”。

当 b == 0,已经没有需要合并的进位,a 就是结果。不能因为 a == 0 就停止,部分和为零时仍可能有尚未加入的进位;也不能先覆盖 a 再计算按位与,否则会丢失原来两个数同时为 1 的位置。

Java 和 Go 的整数都有固定的位宽,负数以补码表示,位运算同样作用于所有二进制位。因此不需要单独区分正负,结果与对应固定宽度整数加法一致;超出最高位的进位按该位宽舍去。

每轮新的进位只能来自旧进位中的某些位再向左移动,不会向低位退回。只要进位还非零,它的最低有效位就会继续升高,最多经过一个字长的传播就会清零,因此循环能够结束。

解题步骤

  1. 只要 b 非零,就继续合并两个操作数。
  2. 先用旧值计算 carry = (a & b) << 1。
  3. 更新 a = a ^ b,得到不含本轮进位的部分和。
  4. 令 b = carry,下一轮只处理仍需加入的进位。
  5. 当 b 为零时返回 a。

代码实现

class Solution {
    public int add(int a, int b) {
        while (b != 0) {
            // 必须先用旧操作数保存进位,再覆盖无进位和
            int carry = (a & b) << 1;

            a ^= b;
            // 下一轮第二操作数只表示仍需合并的进位
            b = carry;
        }

        return a;
    }
}
func add(a int, b int) int {
    for b != 0 {
        // 必须先用旧操作数保存进位,再覆盖无进位和
        carry := (a & b) << 1
        a ^= b
        // 下一轮第二操作数只表示仍需合并的进位
        b = carry
    }
    return a
}

复杂度分析

  • 时间复杂度:$O(w)$,w 为整数位宽,每轮进位至少向更高位推进一次。Java int 为 32 位,Go int 位宽由平台决定;固定字长下也可记作 $O(1)$。
  • 空间复杂度:$O(1)$,只使用一个进位变量,原操作数直接更新。

关键点总结

[!green]

  • 先保存进位,不能读已被异或覆盖的旧值。
  • 终止条件是进位零,不是部分和零。

易错点总结

[!yellow]

  • 先更新异或结果再算进位:进位必须根据旧操作数中同时为 1 的位置计算。
  • 按位与后不左移:进位应进入更高一位,留在原位置不符合二进制加法规则。
  • 部分和为零时提前结束:是否完成取决于剩余进位,而不是当前部分和的值。
  • 把负数当作无限长二进制处理:本实现依赖 Java/Go 固定位宽,符号位和最高位进位都遵循该位宽的规则。

相似题目

题目 难度 关联与区别
67. 二进制求和 简单 同样把每位的和与进位分开,本题通过异或和按位与直接并行处理机器字。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2024/44537108
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!