反转二进制位其实是一个很简单的问题,比如说反转一个32位无符号整数的二进制位,从 0101 变成 1010(注意这里是无符号的)。
这个问题本身的复杂度是很低的,即使是用最直觉的算法,也无非是 O ( k ) O(k) O(k),也就是数字的位数,一位一位地反转。我随便写了一个:
class Solution:
def reverseBits(self, n: int) -> int:
res = 0
for i in range(32):
res *= 2
res += n % 2
n //= 2
return res
当然这里用了一点秦九韶算法的意思,不过也并不难想。
后来了解到这个题有一种使用分治思想的做法,其实就是对这么一个直觉的具体化:如果把数字分成一堆组,然后每一组都反转,那整个数字就反转了;也就是旋转数组那个意思,反转前k位,然后反转后n-k位,然后再整体反转一下,字符串就反过来了。这个过程在纸上比划一下就很容易理解、
放到这个题也是如此。如果自顶向下,就是反转16个、8个、4个、2个,以此类推;如果自底向上,就是反转2个、4个、8个、16个,以此类推。写法也并不复杂,自顶向下是很简单的:
class Solution:
def reverseBits(self, n: int) -> int:
n = n << 16 | n >> 16
n = (n & 0x00ff00ff) << 8 | (n & 0xff00ff00) >> 8
n = (n & 0x0f0f0f0f) << 4 | (n & 0xf0f0f0f0) >> 4
n = (n & 0x33333333) << 2 | (n & 0xcccccccc) >> 2
n = (n & 0x55555555) << 1 | (n & 0xaaaaaaaa) >> 1
return n
第一步之所以不用掩码,是因为反正只考虑16位(总共32位)。如果自底向上呢?很容易就能写出这样的代码:
class Solution:
def reverseBits(self, n: int) -> int:
n = (n & 0x55555555) << 1 | (n & 0xaaaaaaaa) >> 1
n = (n & 0x33333333) << 2 | (n & 0xcccccccc) >> 2
n = (n & 0x0f0f0f0f) << 4 | (n & 0xf0f0f0f0) >> 4
n = (n & 0x00ff00ff) << 8 | (n & 0xff00ff00) >> 8
n = n << 16 | n >> 16
return n
但是这个写法在Python里是错误的。我试了一下其他语言,其他语言都是对的(换成对应的逻辑右移运算符)。在C++、Java中的正确性容易理解,因为他们有明确的int类型,超出后会溢出;但为什么在JavaScript中也是正确的,JS不是和Python一样没有明确的32位整数类型吗?因为JavaScript的位运算符只会作用在整数的后32位上,这也是一个非常著名的不符合直觉的地方。
回过头来看,这段代码之所以在Python里是错误的,是因为Python的位运算不会溢出,加上没有32位的限制,移位后会把数字变大,就超出了32位。正确的写法应该是这样:
class Solution:
def reverseBits(self, n: int) -> int:
n = (n & 0x55555555) << 1 | (n & 0xaaaaaaaa) >> 1
n = (n & 0x33333333) << 2 | (n & 0xcccccccc) >> 2
n = (n & 0x0f0f0f0f) << 4 | (n & 0xf0f0f0f0) >> 4
n = (n & 0x00ff00ff) << 8 | (n & 0xff00ff00) >> 8
n = (n & 0x0000ffff) << 16 | (n & 0x0000ffff) >> 16
return n
通过掩码把范围限制在32位的范围内,才能获得正确结果。
再回头看看自顶向下的解法。之所以不用掩码,一方面是只考虑16位,不需要掩码;另一方面也是后面的掩码会帮助他缩小范围,不再需要在第一步就限制在32位的范围内了。