JAVA面试题:不用+号怎么做加法,我写了5种

48 阅读4分钟

面试官问:不用加法运算符,怎么算两个整数的和?

这道题蛮有意思的,下面用Java 17写6种实现,你可以留意一下第一种,属于脑筋急转弯的做法。

下文的代码,是在window 11+java 17的环境下执行的。

减法取反

数学上 a + b 等价于 a - (-b)。减号不算加法运算符,所以:

public static int add(int a, int b) {
    return a - (-b);
}

一行代码搞定,需要注意一个边界情况:当b是Integer.MIN_VALUE(-2147483648)时,-b会溢出。Java中int的范围是-2147483648到2147483647,最小值取反超出最大值,结果还是-2147483648。如果追求严谨,可以加一层判断:

public static int add(int a, int b) {
    if (b == Integer.MIN_VALUE) {
        // MIN_VALUE取反溢出,拆成两步来加
        return (a - Integer.MAX_VALUE) - 1;
    }
    return a - (-b);
}

这个方法有点像脑筋急转弯,但它确实符合题目要求。

位运算半加器

这是最经典的答案,也是面试中最被期待的那种。

核心思路来自数字电路里的半加器。两个二进制位相加,结果分两部分:本位和用异或(XOR)得到,进位用与(AND)得到。把这个逻辑扩展到32位整数,循环处理进位,就是一台软件加法器。

public static int add(int a, int b) {
    while (b != 0) {
        // 不考虑进位的和
        int sum = a ^ b;
        // 进位,左移一位对齐到正确的位置
        int carry = (a & b) << 1;
        a = sum;
        b = carry;
    }
    return a;
}

我们用以3 + 5为例把执行过程写出来。3的二进制是0011,5是0101。

第一轮:异或得0110(6),与运算得0001,左移一位得0010(2)。a变成6,b变成2。

第二轮:异或得0100(4),与运算得0010,左移一位得0100(4)。a变成4,b变成4。

第三轮:异或得0000(0),与运算得0100,左移一位得1000(8)。a变成0,b变成8。

第四轮:异或得1000(8),与运算得0000(0)。b为0,循环结束,返回8。

进位每轮都往高位推一格,直到所有位的进位都消化完。int是32位,最多循环32次,时间复杂度O(1),空间复杂度O(1)。

CPU里的ALU做加法,本质上就是这套逻辑的硬件实现,只不过用逻辑门代替了位运算符。 面试时如果能从位运算聊到CPU加法器,面试官对你的判断会从会写代码升级到懂底层。

对数指数

利用一个数学恒等式:

log(e^a × e^b) = log(e^(a+b)) = a + b

加法被转换成了指数和乘法运算:

public static int add(int a, int b) {
    return (int) Math.round(Math.log(Math.exp(a) * Math.exp(b)));
}

这个方法的问题很多:

浮点精度不可靠。add(1, 2)的中间结果是Math.log(20.0855...),算出来可能是2.9999999999999996,需要round来修正。但round本身也不是万无一失的。

数字一大Math.exp就溢出。Math.exp(1000)直接返回Infinity,后续运算全部失效。

负数处理不了。Math.exp(-5)虽然能算,但和正数相乘后取对数的精度更差,而且当两个数一正一负时,指数值差距巨大,浮点数的有效位数不够用。

这里展示了一种思路,生产环境肯定不能这么玩的。

BigInteger

import java.math.BigInteger;

public static int add(int a, int b) {
    return BigInteger.valueOf(a)
                     .add(BigInteger.valueOf(b))
                     .intValue();
}

直接调BigInteger的add方法。代码里没有出现+号,完全合法。

有人可能会说这不是耍赖吗,加法运算藏在BigInteger.add()里面。这样说没毛病,但换个角度看:BigInteger能处理任意大小的整数,不存在溢出问题。在金融计算、密码学这些对精度要求极高的领域,BigInteger是标准做法,没人会用位运算手写一个大整数加法。

Stream.reduce

import java.util.stream.IntStream;

public static int add(int a, int b) {
    return IntStream.of(a, b).reduce(0, Integer::sum);
}

用Stream把两个数reduce成一个。

这个方法更适合展示编程风格,正式环境,不要这么用。

方法对比

方法真正避开加法时间复杂度实用性
位运算半加器O(1)面试考察、教学
减法取反否,减法内部有加法O(1)特定场景可用
对数指数否,浮点运算内部有加法O(1)数学演示
BigInteger否,库函数内部有加法O(1)生产环境,尤其大数场景
Stream.reduce否,lambda里有+号O(1)函数式风格演示