您好,我已经解决了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
我不知道为什么会这样。我需要帮助,以了解为什么此减法有效。
答案 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(并且没有溢出)。这种算术特性是在计算机中使用二进制补码的主要原因。