Calculate the sum of two integers a and b, but you are not allowed to use the operator + and -.

这是来自 leetcode 的一道经典题目——不使用+实现求和。本文将从整数(有符号和无符号)在内存的表现入手并讨论该题解法。

formula 以上是有无符号整数的公式化表示,截图自CMU 15-213讲座视频,其中w表示该数在内存中占的bit数。

首先先了解有无符号整数的表示方法和他们的最大最小值。

无符号整数

先从最简单的无符号整数开始,以下是一个无符号整数(8bit)的二进制表示的例子。

#87654321
1286432168421
二进制01100100
x=0643200400
Sum100

以上是无符号整数二进制01100100的十进制值求法。从其最小位开始,设第一位值1,第二位值2……第n位值2^n。然后做一个简单的求和即可算出来其十进制值。 validation 用 Windows 10 自带的计算器验证结果(BIN 0110 0100 == DEC 100)。

我们可以由上表推算出8位二进制可以表示的最大无符号数为0b1111 1111,即2^0+2^1+2^2+…+2^7=255;最小值当然就是0b0000 0000,即十进制0。

有符号整数(补码)

在“补码”表示的有符号整数中,人们规定把二进制的MSB(most significant bit),即最后一位规定为符号位,具体表现是这样的:

#87654321
-1286432168421
二进制01100100
x=0643200400
Sum100

注意最后一位的“价值”从128变成了-128,而其它各位的“价值”保持不变,这样就在一定程度上保持了有无符号数内存表示的兼容。

由此表可得,有符号数字的最大值是0b0111 1111(除了-128的一位其它位都选上),即127,如下表所示:

#|8|7|6|5|4|3|2|1 :—–:|:—–:|:—–:|:—–:|:—–:|:—–:|:—–:|:—–:| 值|-128|64|32|16|8|4|2|1 二进制|0|1|1|1|1|1|1|1 x=|0|64|32|16|8|4|2|1 Sum|127

而有符号整数的最小值则为0b1000 0000(只选择-128这个负数“价值”的位),即-128:

#87654321
-1286432168421
二进制10000000
x=-1280000000
Sum-128

∴8bit有符号整数的范围为-128~127

加法

二进制的加法其实就和十进制加法一样,列个竖式算个进位就出来了。比如说0b0010 0001 + 0b0001 0111:

  0b 0010 0001 (33)
+ 0b 0001 0111 (23)
= 0b 0011 1000 (56)

有趣的是,相同的规则也能运用到有符号整数中,例如:

  0b 1010 0001 (-95)
+ 0b 0001 0111 (23)
= 0b 1011 1000 (-72)

(所以说其实根本不需要知道补码是什么的吗(╯‵□′)╯︵┻━)) 那么我们只要写程序模拟这个竖式加法过程就可以了。 在此之前先介绍一下异或运算符(^),其运算过程如下:

ABA^B
000
011
101
110

可以看到异或运算和加法运算很像,我们可以利用该运算符来模拟加法,然后只要手动处理进位就可以了。

#include <iostream>
int add(int a, int b) {
    // a+b
    int sum = 0;
    constexpr size_t intLength = sizeof(int) * 8;
    bool lcarry = false;
    for (size_t i = 0; i < intLength; ++i) {
        bool carry = false; // 这一位是否会产生进位
        int mask = 1 << i; // 指定我们正在处理的位数
        bool val = (a&mask) ^ (b&mask); // 这一位的运算结果
        if ((a&mask) && (b&mask)) carry = true; // 如果两个加数这一位都是1的话就会产生进位
        if (lcarry) { // 上一位有进位吗?
            if (val) carry = true; // 如果上一位有进位,这一位是1的话,那这一位也会产生进位
            val ^= 1; // 不然的话就加一
        }
        lcarry = carry;
        if (val) sum |= mask; // 设置这一位的运算结果
    }
    return sum;
}
int main(){
    int a, b;
    std::cin >> a >> b;
    std::cout << add(a, b) << std::endl;
}

这样我们就花了16行实现了a+b。 然而,可能你也发现了,这样实现其实在for循环里还是有一个++运算的orz 不过幸好这个加号很容易就可以去掉——我们只需要手动展开循环就可以了(逃

好吧其实还有更优雅的方式,我们可以一次性把所有位数全部运算一遍!优化过的代码如下:

#include <iostream>
int add(int a, int b) {
    int carry=(a&b)<<1;
    int sum=a^b;
    while (carry!=0){ // 一直加到没有进位为止
        int newSum=sum^carry; // 加进位
        carry=(sum&carry)<<1; // 获取加进位的时候可能产生的进位
        sum=newSum;
    }
    return sum;
}
int main(){
    int a, b;
    std::cin >> a >> b;
    std::cout << add(a, b) << std::endl;
}