Single Number 与 XOR 性质
利用 XOR 的自逆性质,在其他元素都出现两次的列表中找出唯一出现一次的元素,然后扩展到 Single Number II 和 III。
Single Number 与 XOR 性质 是 CoddyKit 上的免费 DSA Interview Prep 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 DSA Interview Prep 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 DSA Interview Prep 课程共包含 4 节课。
只出现一次的数字问题
“只出现一次的数字”问题(LeetCode 136)要求:给定一个数组,其中每个元素恰好出现两次,只有一个元素例外,请找出只出现一次的元素。O(n) 时间和 O(1) 空间的限制排除了哈希映射(O(n) 空间)和排序(排序需要 O(n log n) 时间或 O(n) 空间)。
优雅的解法使用 XOR。将所有元素逐一进行 XOR。由于相同的元素会相互抵消(a ^ a = 0),并且 XOR 具有交换律和结合律,所有成对的元素都会消失,只留下那个单独的元素。这是整个竞赛编程中最令人满意的 O(n)/O(1) 解法之一。
def single_number(nums):
result = 0
for n in nums:
result ^= n
return result
# All pairs cancel, leaving the lone element
print(single_number([2, 2, 1])) # 1
print(single_number([4, 1, 2, 1, 2])) # 4
print(single_number([1])) # 1
print(single_number([7, 3, 5, 3, 7])) # 5
# Even more concise with functools.reduce
from functools import reduce
from operator import xor
print(reduce(xor, [2, 2, 1])) # 1XOR 为何有效:三个关键性质
XOR 的力量来自三个代数性质的共同作用:
- 自逆性:
a ^ a = 0——相同的值会相互抵消 - 恒等性:
a ^ 0 = a——与 0 进行 XOR 不会改变值 - 交换律和结合律:运算顺序和分组方式都不影响结果
这三个性质结合起来意味着,对一个多重集中的所有元素进行 XOR 时,出现偶数次的元素都会归约为 0,只留下出现奇数次的元素。对于只出现一次的数字 I,恰好有一个元素出现一次(奇数次),因此结果就是该元素。
# Demonstrating the three XOR properties
print('Self-inverse: a ^ a = 0')
for a in [5, 13, 255, 0]:
print(f' {a} ^ {a} = {a ^ a}')
print('Identity: a ^ 0 = a')
for a in [5, 13, 0, 1024]:
print(f' {a} ^ 0 = {a ^ 0}')
print('Commutativity and Associativity:')
a, b, c = 3, 5, 7
print(f' a^b^c = {a^b^c}')
print(f' c^a^b = {c^a^b}') # same result
print(f' (a^b)^c = {(a^b)^c}')
print(f' a^(b^c) = {a^(b^c)}') # same result逐步跟踪只出现一次的数字
下面逐步跟踪 [4, 1, 2, 1, 2],观察抵消是如何发生的。将所有元素进行 XOR:4 ^ 1 ^ 2 ^ 1 ^ 2。由于 XOR 具有交换律,可以重新排列为 (1 ^ 1) ^ (2 ^ 2) ^ 4 = 0 ^ 0 ^ 4 = 4。成对的元素相互抵消,最后只剩下 4。
在实际算法中,我们不会重新排列,而是从左到右进行 XOR。但由于交换律和结合律保证运算顺序不会影响结果,最终结果仍然相同。您可以在任意位置将成对元素分组,它们都会相互抵消。
nums = [4, 1, 2, 1, 2]
result = 0
print(f'Start: result = {result} ({bin(result)})')
for n in nums:
prev = result
result ^= n
print(f'XOR {n:2d}: {bin(prev):8s} ^ {bin(n):6s} = {bin(result):8s} = {result}')
print(f'Final: {result}') # 4
# Alternative: show pair cancellation
print('\nMath view:')
print('4 ^ 1 ^ 2 ^ 1 ^ 2')
print('= 4 ^ (1^1) ^ (2^2)')
print('= 4 ^ 0 ^ 0')
print('= 4')只出现一次的数字 II:每个元素出现三次
只出现一次的数字 II(LeetCode 137):每个元素都出现三次,只有一个元素出现一次。单独使用 XOR 不再有效,因为三个相同的元素不会像成对元素那样抵消。相反,我们统计所有数字中每一位出现的次数。如果某一位出现在目标元素中,它会贡献 1;如果出现在出现三次的元素中,它会贡献 3。对每一位的计数取模 3,即可提取目标元素的各个二进制位。
我们可以使用两个整数变量 ones 和 twos 来模拟一个模 3 的位级计数器。这是一种数字逻辑方法:ones 保存按模 2 计算后出现奇数次的位,twos 保存按模 3 计算后出现两次的位。
def single_number_II(nums):
ones, twos = 0, 0
for n in nums:
ones = (ones ^ n) & ~twos # bits seen 1 mod 3 times
twos = (twos ^ n) & ~ones # bits seen 2 mod 3 times
return ones # bits seen exactly once
print(single_number_II([2, 2, 3, 2])) # 3
print(single_number_II([0, 1, 0, 1, 0, 1, 99])) # 99
# Simpler but O(32) bit-by-bit approach
def single_number_II_simple(nums):
result = 0
for bit in range(32):
total = sum((n >> bit) & 1 for n in nums)
if total % 3 == 1:
result |= (1 << bit)
return result
print(single_number_II_simple([2, 2, 3, 2])) # 3只出现一次的数字 III:两个元素各出现一次
只出现一次的数字 III(LeetCode 260):两个元素各出现一次,其他元素都出现两次。将所有元素进行 XOR,得到 a ^ b(两个唯一元素的 XOR 结果)。由于 a ≠ b,a ^ b 中至少有一位为 1——使用 diff = xor_all & (-xor_all) 找出 a ^ b 的最低位置位。
这一位在 a 或 b 中恰好只有一个为 1。根据这一位是否为 1,将所有数字划分为两组。分别对两组进行 XOR——成对的元素会相互抵消,最后一个组留下 a,另一个组留下 b。
def single_number_III(nums):
xor_all = 0
for n in nums:
xor_all ^= n # xor_all = a ^ b
diff = xor_all & (-xor_all) # isolate lowest differing bit
a = 0
for n in nums:
if n & diff: # group 1: has the diff bit set
a ^= n
b = xor_all ^ a # a ^ b ^ a = b
return [a, b]
print(sorted(single_number_III([1, 2, 1, 3, 2, 5]))) # [3, 5]
print(sorted(single_number_III([-1, 0]))) # [-1, 0]
print(sorted(single_number_III([0, 1]))) # [0, 1]使用 XOR 找出缺失数字
缺失数字问题(LeetCode 268):给定一个包含 n 个互不相同数字的数组,这些数字取自 0 到 n,找出缺失的数字。将数组中的所有数字与 0 到 n 之间的所有数字进行 XOR。成对的数字会相互抵消,最后留下缺失的数字。这一方法的时间复杂度为 O(n),空间复杂度为 O(1)。
另一种方法是使用算术求和公式:expected = n*(n+1)//2,然后减去实际总和。两种方法的复杂度都是 O(n)/O(1)。XOR 更加稳健,因为在使用定长整数的语言中,它可以避免潜在的整数溢出。
def missing_number_xor(nums):
n = len(nums)
result = n # start with n (the last expected value)
for i, num in enumerate(nums):
result ^= i ^ num # XOR with both index and value
return result
def missing_number_sum(nums):
n = len(nums)
expected = n * (n + 1) // 2
return expected - sum(nums)
for nums, expected in [([3,0,1], 2), ([0,1], 2), ([9,6,4,2,3,5,7,0,1], 8)]:
xor_ans = missing_number_xor(nums)
sum_ans = missing_number_sum(nums)
print(f'nums={nums}: XOR={xor_ans}, Sum={sum_ans}, expected={expected}')不使用临时变量交换:XOR
XOR 可以让您在不使用临时变量的情况下交换两个变量。其原理是 a ^ b ^ a = b 和 a ^ b ^ b = a。依次执行三次 XOR 赋值:a ^= b,然后执行 b ^= a,最后执行 a ^= b。三步完成后,a 保存原来的 b,b 保存原来的 a。
重要注意事项:如果 a 和 b 引用同一内存位置(也就是说它们是同一个变量),这一技巧就会失效。此时,a ^= a 会将 a 设为 0,原值也会丢失。在 Python 中,元组解包(a, b = b, a)更加安全、清晰。XOR 交换主要适用于不提供额外内存的 C/嵌入式场景。
# XOR swap
a, b = 17, 42
print(f'Before: a={a}, b={b}')
a ^= b # a = 17 ^ 42
b ^= a # b = 42 ^ (17 ^ 42) = 17
a ^= b # a = (17 ^ 42) ^ 17 = 42
print(f'After: a={a}, b={b}') # a=42, b=17
# The caveat: same variable/reference => broken
c = 99
# If a and b pointed to same value:
c ^= c # c = 0 (destroyed!)
print(f'Same-variable XOR swap: c={c}') # 0, not 99
# Pythonic swap: always prefer this
a, b = 17, 42
a, b = b, a # safe, clear, handles aliases
print(f'Pythonic: a={a}, b={b}')XOR 在哈希和校验和中的应用
XOR 是校验和和奇偶校验中常用的基础构件。将数据块中的所有字节进行 XOR,可以生成一个单字节校验和。如果数据传输过程中有一个比特发生翻转,校验和就会改变,从而检测出错误。这种方法比 CRC 更简单,但能够捕获所有单比特错误。
XOR 也用于RAID-5 奇偶校验:对于三个驱动器,将其中两个驱动器的数据进行 XOR,并把结果存储在第三个驱动器上。如果一个驱动器发生故障,就对剩余两个驱动器的数据进行 XOR,以重建丢失的数据。这正好是反向应用的只出现一次的数字逻辑——奇偶校验驱动器就是编码了三者进行 XOR 时抵消结果的“唯一元素”。
# Simple XOR checksum
def xor_checksum(data):
result = 0
for byte in data:
result ^= byte
return result
data = [0x48, 0x65, 0x6C, 0x6C, 0x6F] # 'Hello' in ASCII
checksum = xor_checksum(data)
print(f'Checksum: {hex(checksum)}')
# Detect corruption
corrupted = data[:]
corrupted[2] ^= 0xFF # flip all bits of 3rd byte
new_checksum = xor_checksum(corrupted)
print(f'Original checksum: {hex(checksum)}')
print(f'Corrupted checksum: {hex(new_checksum)}')
print(f'Error detected: {checksum != new_checksum}')
# RAID-5 parity recovery
d1 = [1, 0, 1, 1]
d2 = [0, 1, 1, 0]
parity = [d1[i] ^ d2[i] for i in range(4)]
recovered = [parity[i] ^ d2[i] for i in range(4)] # recover d1
print(f'd1={d1}, parity={parity}, recovered={recovered}')XOR 与子集问题
当您需要计算所有子集的 XOR时,XOR 会出现在子集问题中。一个关键洞见是:对于 n 个元素,每个元素恰好出现在 2^(n-1) 个子集中。当 n > 1 时,每个元素都会出现在偶数个子集中,因此其 XOR 贡献会相互抵消。对于 n > 1,所有子集 XOR 结果的 XOR 为 0。
当 n == 1 时,唯一的非空子集就是该元素本身,因此所有子集的 XOR 就是该元素。这类利用 XOR 性质和计数进行推理的方法,会在高级位运算问题中受到考查。
from itertools import combinations
from functools import reduce
from operator import xor
def xor_of_all_subsets(arr):
n = len(arr)
total_xor = 0
for r in range(1, n + 1):
for subset in combinations(arr, r):
subset_xor = reduce(xor, subset)
total_xor ^= subset_xor
return total_xor
# For n > 1, each element appears 2^(n-1) times (even) => cancels
# Result is always 0 for n > 1
for arr in [[1,2,3], [5,7], [1], [1,2,3,4]]:
result = xor_of_all_subsets(arr)
predicted = arr[0] if len(arr) == 1 else 0
print(f'arr={arr}: XOR of all subsets = {result}, predicted = {predicted}')面试模式:使用 XOR 判断唯一性
当题目表述为:“每个元素出现 k 次,只有一个元素出现 m 次,并且 m mod k != 0”时,请识别出使用 XOR 判断唯一性的模式。对于 k=2、m=1(只出现一次的数字 I),将所有元素进行 XOR。对于 k=3、m=1(只出现一次的数字 II),统计各位并对 3 取模。对于 k=2、m=1 且有两个唯一元素(只出现一次的数字 III),先进行 XOR,再根据最低的不同位进行分组。
对于任意 k,通用方法是统计每一位出现的总次数,然后对 k 取模。如果计数不为零,该位就属于唯一元素。对于任意 k,这都能得到一个 O(32n) = O(n) 的算法,并且空间复杂度为 O(1)。
def single_number_k_times(nums, k):
'''Find the element that appears m times when all others appear k times.'''
# Count each bit's occurrence and take mod k
result = 0
for bit in range(32):
total = sum((n >> bit) & 1 for n in nums)
if total % k != 0:
result |= (1 << bit)
# Handle negative 32-bit numbers
if result >= (1 << 31):
result -= (1 << 32)
return result
# k=2, element appears once
print(single_number_k_times([2,2,1], 2)) # 1
# k=3, element appears once
print(single_number_k_times([2,2,3,2], 3)) # 3
# k=4, element appears once
print(single_number_k_times([1,1,1,1,7,2,2,2,2], 4)) # 7常见的 XOR 面试问题
除了只出现一次的数字系列,XOR 还出现在以下常见问题中:
- 找出差异(LC 389):对两个字符串中的所有字符进行 XOR,最后留下多出的字符
- 汉明距离(LC 461):对两个数字进行 XOR,然后统计结果中的 1 位数量
- 总汉明距离(LC 477):统计所有数字对在每个比特位置上的 0 和 1 的数量
- 子数组 XOR 查询(LC 1310):使用 XOR 前缀数组进行范围查询
在每种情况下,XOR 的抵消性质都会消除冗余,并将 O(n²) 的暴力方法降低为 O(n)。
# Find the difference between two strings
def find_the_difference(s, t):
result = 0
for c in s + t:
result ^= ord(c)
return chr(result)
print(find_the_difference('abcd', 'abcde')) # 'e'
# Hamming distance: count differing bits
def hamming_distance(x, y):
diff = x ^ y
count = 0
while diff:
count += diff & 1
diff >>= 1
return count
# or: bin(x ^ y).count('1')
print(hamming_distance(1, 4)) # 2: 001 vs 100 differ in bits 0 and 2
print(hamming_distance(3, 1)) # 1: 011 vs 001 differ in bit 1
# Prefix XOR for range queries
def xor_queries(arr, queries):
prefix = [0] * (len(arr) + 1)
for i, v in enumerate(arr):
prefix[i+1] = prefix[i] ^ v
return [prefix[r+1] ^ prefix[l] for l, r in queries]
print(xor_queries([1,3,4,8], [[0,1],[1,2],[0,3],[3,3]]))快速检查
请测试您对本课中“数据结构与算法——编程面试准备”概念的理解。
课程回顾
在本课中,您学习了:XOR 的自逆性质(a ^ a = 0)会使成对元素相互抵消,当所有数字进行 XOR 后只留下唯一元素;只出现一次的数字 II 使用对 3 取模的位计数,而只出现一次的数字 III 根据最低的不同位对元素进行分组;以及XOR 还可以解决缺失数字、找出差异、汉明距离和范围 XOR 查询问题。接下来,我们将探索用于设置、清除、切换和检查单个位的位掩码。
常见问题解答
「Single Number 与 XOR 性质」课时是免费的吗?
是的 — 「Single Number 与 XOR 性质」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 DSA Interview Prep 课程的其余内容,请升级到 CoddyKit PRO。 DSA Interview Prep 课程共包含 4 节课。
「Single Number 与 XOR 性质」这节课中我会学到什么?
利用 XOR 的自逆性质,在其他元素都出现两次的列表中找出唯一出现一次的元素,然后扩展到 Single Number II 和 III。 你通过在浏览器中直接运行的动手代码来练习 DSA Interview Prep,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 DSA Interview Prep 需要有经验吗?
无需任何先前经验。CoddyKit 上的 DSA Interview Prep 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。
「Single Number 与 XOR 性质」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 DSA Interview Prep 课中编写并运行代码吗?
能。每节 DSA Interview Prep 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 位运算符:AND、OR、XOR、NOT 与移位
- Single Number 与 XOR 性质
- 位掩码:设置、清除、翻转与检查
- 位计数、缺失数字与位反转