371. Sum of Two Integers - Python

不用加减乘除做加法

剑指Offer中也出现过此题,此篇文章着重分析一下为什么Python需要做一些多余的操作。

根据书中的分析,大致分为三步:

  • 相加但不进位,使用^
  • 进位使用&<<
  • 前两步结果相加

这里不再赘述详细过程。那么我们不难写出代码:

1
2
3
4
5
6
7
def get_sum(a, b):
res, carry = 0, 0
while b != 0:
res = a ^ b
carry = (a & b) << 1
a, b = res, carry
return res

但是当输入a=-1, b=1时,会发现程序超时。
先来看看正常的32位int类型运算时发生了什么,因为进行位运算时要使用补码进行运算,而正数的反码和补码都是本身,负数的反码为除符号位,其它按位取反,负数的补码为反码+1。下面列出了32为整数传入a=-1,b=1时,每次循环中ab的补码。

a的原码 a的补码 b的补码 a ^ b a & b
10…0001 11…1111 00…0001 11…1110 00…0001
10…0010 11…1110 00…0010 11…1100 00…0010
10…0100 11…1100 00…0100 11…1000 00…0100
110…000 110…000 010…000 100…000 010…000
100…000 100…000 100…000 000…000 100…000
000…000 000…000 000…000 ‘break’ ‘break’

可以看出由于位数的限制,b和a最后都变成了0。因为Python中的int没有范围限制,所以这就是为什么会死循环的原因。

如何解决这个问题呢?我们需要在b的补码变成1000…000(32个0)时终止循环,如何将一个一个无限位的整型转成32位呢,想到了&,所以我们找到一个mask=2**32-1也就是32个1,用一个无限位的值和mask进行与运算,就变成了上述表格中的例子,于是我们将代码改为:

1
2
3
4
5
6
7
8
def getSum(self, a, b):
res, carry = 0, 0
mask = 0xFFFFFFFF
while b != 0:
res = (a ^ b) & mask
carry = ((a & b) << 1) & mask
a, b = res, carry
return res

提交之后还是不能通过测试用例,当a=-12, b=-8时输出了4294967276而不是-20。好,把mask先去掉,传入这两个值,发现可以正常输出-20,那么这里的mask是怎么造成了一些副作用的。
再来分析一下这两个参数下,具体的过程是怎样的。

为了方便DEBUG,我们添加一些输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def get_sum(self, a, b):
mask = 0xFFFFFFFF
res, carry = 0, 0
while b != 0:
print('bin({}) = {}, bin({}) = {}'.format(a, bin(a), b, bin(b)))
res = (a ^ b) & mask
# res = (a ^ b)
print('\t res = {}, bin(res) = {}'.format(res, bin(res)))
carry = ((a & b) << 1) & mask
# carry = ((a & b) << 1)
print('\t \t a&b = {}, (a&b)<<1 = {}, carry = {}'.format((a&b), ((a&b)+1), carry))
a, b = res, carry
# return res if res <= MAX else ~(res ^ mask)
return res

输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Finished in 20 ms
bin(-12) = -0b1100, bin(-8) = -0b1000
res = 12, bin(res) = 0b1100
a&b = -16, (a&b)<<1 = -15, carry = -32
bin(12) = 0b1100, bin(-32) = -0b100000
res = -20, bin(res) = -0b10100
a&b = 0, (a&b)<<1 = 1, carry = 0
-20
Finished in 24 ms
bin(-12) = -0b1100, bin(-8) = -0b1000
res = 12, bin(res) = 0b1100
a&b = -16, (a&b)<<1 = -15, carry = 4294967264
bin(12) = 0b1100, bin(4294967264) = 0b11111111111111111111111111100000
res = 4294967276, bin(res) = 0b11111111111111111111111111101100
a&b = 0, (a&b)<<1 = 1, carry = 0
4294967276

可以看到在进行-15&mask时,carry变成了一个很大的正数,并丢失了符号。此处为个人猜想,当负数与mask进行与运算时,比如-2,此时-2的补码变为11…10,一个33-bit的数字,然后和32位的mask与操作后,变为了一个33位的正数。

有一个公式可以帮我们还原a,如果一个负数n,它的无符号的32位补码是m,那么m=~(n ^ mask) 或者n=~(m ^ mask)

于是代码修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def getSum(self, a, b):
MASK = 0xffffffff

# in Python, every integer is associated with its two's complement and its sign.
# However, doing bit operation "& mask" loses the track of sign.
# Therefore, after the while loop, a is the two's complement of the final result as a 32-bit unsigned integer.
while b != 0:
a, b = (a ^ b) & MASK, ((a & b) << 1) & MASK

# a is negative if the first bit is 1
if (a >> 31) & 1:
return ~(a ^ MASK)
else:
return a

虽然python进行大数字运算很方便,但是无限制位数往往会对位运算的操作中产生一些陷阱。如果可以直接转成Java中那种32的int,岂不是没有这么多麻烦了,好在numpy为我们提供了这样的方法。

1
2
3
4
5
6
7
import numpy as np

class Solution(object):
def getSum(self, a, b):
while b != 0:
a, b = np.int32(a ^ b), np.int32((a & b) << 1)
return int(a)

最后要再转回int,否则是没法通过测试用例的。

参考: