面试官问:不用加法运算符,怎么算两个整数的和?
这道题蛮有意思的,下面用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) | 函数式风格演示 |