0Pricing
DSA Interview Prep · 课时

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]))  # 1

XOR 为何有效:三个关键性质

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 反馈 — 无需本地设置。

此课程中的所有课时

  1. 位运算符:AND、OR、XOR、NOT 与移位
  2. Single Number 与 XOR 性质
  3. 位掩码:设置、清除、翻转与检查
  4. 位计数、缺失数字与位反转
← 返回 DSA Interview Prep