如何在python3中将负位表示的数字转换为它们的实际负int值?

时间:2019-05-17 13:59:45

标签: python python-3.x algorithm bit-manipulation bit

您好,我已经解决了leetcode问题https://leetcode.com/problems/single-number-ii。目的是解决O(n)时间和0(1)空间中的问题。我编写的代码如下:

class Solution:
    def singleNumber(self, nums: List[int]) -> int:
        counter = [0 for i in range(32)]
        result = 0
        for i in range(32):
            for num in nums:
                if ((num >> i) & 1):
                    counter[i] += 1
            result = result | ((counter[i] % 3) << i)
        return self.convert(result)
        #return result

    def convert(self,x):
        if x >= 2**31:
            x = (~x & 0xffffffff) + 1
            x = -x
        return x

现在有趣的部分在convert函数中,因为python使用对象存储int而不是32位字或其他东西,所以当MSB时,它不知道结果是否定的counter中的设置为1。我通过将其转换为2的补码并返回负值来解决这个问题。

现在其他人通过以下方式发布了他们的解决方案:

def convert(self, x):
    if x >= 2**31:
        x -= 2**32
    return x 

我不知道为什么会这样。我需要帮助,以了解为什么此减法有效。

4 个答案:

答案 0 :(得分:2)

Python整数无限大。随着您添加更多位,它们将不会变为负数,因此二进制补码可能无法按预期工作。您可以以不同方式处理底片。

def singleNumber(nums):
    result = 0
    sign   = [1,-1][sum(int(n<0) for n in nums)%3]
    for i in range(32):
        counter = 0
        for num in nums:
            counter += (abs(num) >> i) & 1
        result = result | ((counter % 3) << i)
    return result * sign

可以像这样优化和简化这种二进制方法:

def singleNumber(nums):
    result = 0
    for i in range(32):
        counter = sum(1 for n in nums if (n>>i)&1)
        if counter > 0: result |= (counter % 3) << i
    return result - 2*(result&(1<<31))

如果您喜欢一种衬板,则可以使用functools中的reduce()来实现它:

result = reduce(lambda r,i:r|sum(1&(n>>i) for n in nums)%3<<i,range(32),sum(n<0 for n in nums)%3*(-1<<32))

请注意,此方法将始终对数据进行32次传递,并且仅限于-2 ^ 31 ... 2 ^ 31范围内的数字。增大此范围将系统地增加通过数字列表的通过次数(即使该列表仅包含较小的值)。另外,由于您不在i循环之外使用counter [i],因此不需要列表来存储计数器。

您可以使用非常相似的方法(以O(n)时间和O(1)空间作为响应)来利用基数3而不是基数2:

def singleNumber(nums):
    result = sign = 0
    for num in nums:
        if num<0 : sign += 1
        base3 = 1
        num   = abs(num)
        while num > 0 :
            num,rest   = divmod(num,3)
            rest,base3 = rest*base3, 3*base3
            if rest == 0 : continue
            digit  = result % base3
            result = result - digit + (digit+rest)%base3      
    return result * (1-sign%3*2)

此代码的优点是它将仅浏览列表一次(因此支持迭代器作为输入)。它不限制值的范围,并且将执行嵌套的while循环尽可能少的次数(根据每个值的大小)

它的工作方式是,以3为基数独立地添加数字,并在不加进位的情况下循环结果(逐位)。

例如:[16,16,32,16]

    Base10    Base 3    Base 3 digits  result (cumulative)
    ------    ------    -------------  ------
      16         121    0 | 1 | 2 | 1     121
      16         121    0 | 1 | 2 | 1     212 
      32        2012    2 | 0 | 1 | 2    2221 
      16         121    0 | 1 | 2 | 1    2012
                        -------------
    sum of digits % 3   2 | 0 | 1 | 2  ==> 32

while num > 0循环处理数字。它将最多运行log(V,3)次,其中V是数字列表中的最大绝对值。因此,它与以2为底的解决方案中的for i in range(32)循环相似,除了它始终使用尽可能小的范围。对于任何给定的值模式,while循环的迭代次数将小于或等于一个常数,从而保留了主循环的O(n)复杂度。

我进行了一些性能测试,并且在实践中,当值较小时,base3版本仅比base2方法快。 base3方法总是执行较少的迭代,但是当值较大时,由于取模和按位运算的开销,它会在总执行时间上损失很多。

为了使base2解决方案始终比base 3解决方案更快,它需要通过反转循环嵌套(数字内部的位而不是数字内部的位)来优化通过位的迭代:

def singleNumber(nums):
    bits   = [0]*len(bin(max(nums,key=abs)))
    sign   = 0 
    for num in nums:
        if num<0 : sign += 1 
        num = abs(num)
        bit = 0
        while num > 0:
            if num&1 : bits[bit] += 1
            bit  += 1
            num >>= 1
    result = sum(1<<bit for bit,count in enumerate(bits) if count%3)
    return result * [1,-1][sign%3]

现在,它将每次都优于以3为底的方法。附带的好处是,它不再受值范围的限制,并将支持迭代器作为输入。请注意,位数组的大小可以视为常量,因此这也是O(1)空间解决方案

但是,公平地说,如果我们对基数3的方法应用相同的优化(即使用基数3的“位”的列表),则对于所有值大小,其性能都排在前面:

def singleNumber(nums):
    tribits = [0]*len(bin(max(nums,key=abs))) # enough base 2 -> enough 3
    sign    = 0 
    for num in nums:
        if num<0 : sign += 1 
        num = abs(num)
        base3 = 0
        while num > 0:
            digit = num%3
            if digit: tribits[base3] += digit
            base3  += 1
            num   //= 3
    result = sum(count%3 * 3**base3 for base3,count in enumerate(tribits) if count%3)
    return result * [1,-1][sign%3]

来自集合的计数器将用单行代码在O(n)时间内给出预期结果:

from collections import Counter
numbers = [1,0,1,0,1,0,99]
singleN = next(n for n,count in Counter(numbers).items() if count == 1)

集合也可以在O(n)中工作

distinct = set()
multiple = [n for n in numbers if n in distinct or distinct.add(n)]
singleN  = min(distinct.difference(multiple))

这最后两个解决方案确实使用了可变数量的额外内存,这些内存与列表的大小成比例(即不是O(1)空间)。另一方面,它们的运行速度快了30倍,并且将支持列表中的任何数据类型。他们还支持迭代器

答案 1 :(得分:2)

无符号 n位数字的最高位值为 2 n-1

带符号的补码n位数字的最高位值为 -2 n-1

这两个值之间的差异 2 n

因此,如果将无符号n位数字设置为最高位,则要转换为二进制补码数字,请减去 2 n

在32位数字中,如果设置了第31位,则该数字将为> = 2 31 ,因此公式为:

if n >= 2**31:
    n -= 2**32

我希望这很清楚。

答案 2 :(得分:1)

32位带符号整数每个2**32都环绕,因此设置了符号位的正数(即>= 2**31)与负数具有相同的二进制表示法,2**32

答案 3 :(得分:0)

这就是n位上数字A的二进制补码的确切定义。

  • 如果数字A为正,则使用A的二进制代码

  • 如果A为负,则使用2 ^ n + A(或2 ^ n- | A |)的二进制代码。此数字是您必须添加到| A |的数字。以获得2 ^ n(即| A |到2 ^ n的补数,因此是二进制补码方法的名称)。

因此,如果您用二进制补码编码了一个负数B,则其代码中实际上是2 ^ N + B。要获得其值,您必须从B中减去2 ^ N。

还有两个其他的补码定义(〜A + 1,〜(A-1)等),但这是最有用的,因为它解释了为什么加有符号的补码数与加正数绝对相同。该数字在代码中(如果为负,则添加2 ^ 32),并且加法结果将是正确的,前提是您忽略了可能会作为进位生成的2 ^ 32(并且没有溢出)。这种算术特性是在计算机中使用二进制补码的主要原因。