0Pricing
Coding Interview Prep · レッスン

Single NumberとXORの性質

XORの自己逆元性を使って、他の要素がすべて2回ずつ現れるリストから1回だけ現れる要素を見つけ、single-number-IIとIIIへ応用します。

「Single NumberとXORの性質」はCoddyKit上の無料Coding Interview Prepレッスンです。 これはレッスン2/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはCoding Interview Prep学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Coding Interview Prepコースには全4レッスンが含まれています。

Single Number 問題

Single Number 問題(LeetCode 136)では、すべての要素がちょうど2回ずつ現れ、1つだけ1回しか現れない配列が与えられます。1回だけ現れる要素を見つけてください。O(n) 時間かつ O(1) 空間という制約により、ハッシュマップ(O(n) 空間)やソート(O(n log n) 時間、またはソートに O(n) 空間が必要)を使う方法は適しません。

この問題には、XOR を使った洗練された解法があります。すべての要素に対して XOR を計算します。同じ要素同士は打ち消し合い(a ^ a = 0)、さらに XOR には可換性と結合性があるため、ペアになった要素はすべて消え、1回だけ現れる要素だけが残ります。これは、競技プログラミング全体の中でも特に魅力的な O(n)/O(1) 解法の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 が機能する理由:3つの重要な性質

XOR の力は、次の3つの代数的性質が組み合わさることで生まれます。

  • 自己逆元:a ^ a = 0 — 同じ値同士が打ち消し合います
  • 単位元:a ^ 0 = a — 0 と XOR しても値は変わりません
  • 可換性と結合性:順序にも、グループ化の仕方にも左右されません

この3つの性質により、多重集合のすべての要素に XOR を適用すると、偶数回現れる要素はすべて 0 に収束し、奇数回現れる要素だけが残ります。Single Number I では、ちょうど1つの要素が1回(奇数回)現れるため、それが XOR の結果になります。

# 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

Single Number を順に追跡する

[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')

Single Number II:すべての要素が3回現れる場合

Single Number II(LeetCode 137)では、1つの要素だけが1回現れ、それ以外のすべての要素が3回現れます。XOR だけでは機能しません。3回現れる要素では、ペアによる打ち消しが起こらないためです。そこで、すべての数について各ビットが何回現れるかを数えます。対象の要素にそのビットがあれば寄与は1、3回現れる要素では寄与は3になります。各ビットの出現回数を3で割った余りを求めることで、対象の要素のビットだけを取り出せます。

これを、ones と twos という2つの整数変数で、ビット単位の3を法とするカウンターとしてシミュレートできます。これはデジタル論理による方法です。ones は2を法として奇数回現れたビットを保持し、twos は3を法として2回現れたビットを保持します。

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

Single Number III:2つの要素が1回ずつ現れる場合

Single Number III(LeetCode 260)では、2つの要素がそれぞれ1回だけ現れ、それ以外のすべての要素が2回現れます。すべての要素に XOR を適用すると、2つの一意な要素の XOR である a ^ b が得られます。a ≠ b なので、a ^ b には少なくとも1つ、1になっているビットがあります。diff = xor_all & (-xor_all) を使って、最下位の1ビットを見つけます。

このビットは a または b のどちらか一方だけで1になっています。このビットがセットされているかどうかを基準に、すべての数を2つのグループに分けます。各グループで 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 で欠落した数を見つける

Missing Number 問題(LeetCode 268)では、0から n までの相異なる 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 を使うと、一時変数なしで2つの変数を入れ替えられます。その仕組みは、a ^ b ^ a = b と a ^ b ^ b = a です。XOR による代入を3回、順番に行います。まず a ^= b、次に b ^= a、最後に a ^= b とします。3回すべて実行すると、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 を適用すると、1バイトのチェックサムが生成されます。送信中に1ビットだけ反転するとチェックサムが変化するため、エラーを検出できます。CRC より単純ですが、1ビットのエラーをすべて検出できます。

XOR はRAID-5 パリティにも使われます。3台のドライブがある場合、2台のドライブのデータに XOR を適用した結果を3台目に保存します。1台のドライブが故障したときは、残り2台に XOR を適用して失われたデータを復元できます。これは Single Number のロジックを逆向きにしたものです。パリティ用ドライブは、3台すべてに 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 への寄与は打ち消し合います。したがって、すべての部分集合の XOR をさらに XOR した結果は、n > 1 では 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

問題に「1つの要素だけが m 回現れ、それ以外のすべての要素が k 回現れる。ただし m mod k != 0」と書かれていたら、XOR による一意性のパターンを思い出してください。k=2、m=1 の場合(Single Number I)は、すべての要素に XOR を適用します。k=3、m=1 の場合(Single Number II)は、ビットの出現回数を3で割った余りを求めます。k=2、m=1 で一意な要素が2つある場合(Single Number III)は、XOR を計算してから、最も低い位置にある異なるビットを基準に分割します。

任意の k に対する一般的な方法は、各ビットの総出現回数を数えて k で割った余りを求めることです。出現回数が0でなければ、そのビットは一意な要素に属します。これにより、任意の 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 に関するよく出る面接問題

Single Number 系列以外にも、XOR は次のような頻出問題で使われます。

  • Find the Difference(LC 389):2つの文字列のすべての文字に XOR を適用すると、追加された文字だけが残ります
  • Hamming Distance(LC 461):2つの数に XOR を適用し、結果に含まれる1ビットの数を数えます
  • Total Hamming Distance(LC 477):すべてのペアについて、各ビット位置の0と1の個数を数えます
  • XOR Queries of a Subarray(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]]))

理解度チェック

このレッスンで学んだ Data Structures & Algorithms — Coding Interview Prep の概念を理解できているか確認しましょう。

レッスンのまとめ

このレッスンでは、すべての数に XOR を適用すると、XOR の自己逆元の性質(a ^ a = 0)によってペアになった要素が打ち消し合い、一意な要素だけが残ること、Single Number II ではビットの出現回数を3で割った余りを使い、Single Number III では最も低い位置にある異なるビットを基準に要素を分割すること、そしてXOR によって欠落した数、差分の検出、ハミング距離、区間 XOR クエリも解決できることを学びました。次は、個々のビットをセット、クリア、反転、確認するためのビットマスクについて学びます。

よくある質問

「Single NumberとXORの性質」レッスンは無料ですか?

はい。「Single NumberとXORの性質」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Coding Interview Prepコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Coding Interview Prepコースには全4レッスンが含まれています。

「Single NumberとXORの性質」で何を学びますか?

XORの自己逆元性を使って、他の要素がすべて2回ずつ現れるリストから1回だけ現れる要素を見つけ、single-number-IIとIIIへ応用します。 ブラウザで直接実行するハンズオンコードでCoding Interview Prepを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Coding Interview Prepを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのCoding Interview Prepは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン2/4です。

「Single NumberとXORの性質」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このCoding Interview Prepレッスンでコードを書いて実行できますか?

はい。すべてのCoding Interview Prepレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. ビット演算子:AND、OR、XOR、NOT、シフト
  2. Single NumberとXORの性質
  3. ビットマスク:セット、クリア、反転、確認
  4. ビットカウント、Missing Number、ビット反転
← Coding Interview Prepに戻る