不用加减乘除做加法
剑指Offer中也出现过此题,此篇文章着重分析一下为什么Python需要做一些多余的操作。
根据书中的分析,大致分为三步:
- 相加但不进位,使用
^
- 进位使用
&
和<<
- 前两步结果相加
这里不再赘述详细过程。那么我们不难写出代码:
1 | def get_sum(a, b): |
但是当输入a=-1, b=1
时,会发现程序超时。
先来看看正常的32位int类型运算时发生了什么,因为进行位运算时要使用补码进行运算,而正数的反码和补码都是本身,负数的反码为除符号位,其它按位取反,负数的补码为反码+1。下面列出了32为整数传入a=-1,b=1
时,每次循环中a
和b
的补码。
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 | def getSum(self, a, b): |
提交之后还是不能通过测试用例,当a=-12, b=-8
时输出了4294967276
而不是-20
。好,把mask先去掉,传入这两个值,发现可以正常输出-20
,那么这里的mask是怎么造成了一些副作用的。
再来分析一下这两个参数下,具体的过程是怎样的。
为了方便DEBUG,我们添加一些输出:
1 | def get_sum(self, a, b): |
输出如下:
1 | Finished in 20 ms |
可以看到在进行-15&mask
时,carry变成了一个很大的正数,并丢失了符号。此处为个人猜想,当负数与mask
进行与运算时,比如-2,此时-2的补码变为11…10
,一个33-bit的数字,然后和32位的mask
与操作后,变为了一个33位
的正数。
有一个公式可以帮我们还原a
,如果一个负数n
,它的无符号的32位补码是m
,那么m=~(n ^ mask)
或者n=~(m ^ mask)
于是代码修改为:
1 | def getSum(self, a, b): |
虽然python进行大数字运算很方便,但是无限制位数往往会对位运算的操作中产生一些陷阱。如果可以直接转成Java中那种32的int,岂不是没有这么多麻烦了,好在numpy
为我们提供了这样的方法。
1 | import numpy as np |
最后要再转回int
,否则是没法通过测试用例的。
参考: