XOR Patterns
Exploit XOR's self-cancelling property to find unpaired elements, missing numbers, swap without a temporary, and answer range-XOR queries.
Overview
XOR has four properties that make it a problem-solving tool: identity x ^ 0 = x; self-inverse x ^ x = 0; commutative a ^ b = b ^ a; associative (a ^ b) ^ c = a ^ (b ^ c). Together they mean the XOR of a multiset depends only on which values appear an odd number of times, in any order.
Single number: in an array where every value appears twice except one, XOR everything; pairs cancel and the lone value remains. Missing number in 0..n: XOR all indices 0..n with all values; every present number cancels with its index. Two single numbers: XOR all to get a ^ b, pick any set bit of it (d & -d), and split the array by that bit — each group has exactly one unpaired value.
XOR swap: a ^= b; b ^= a; a ^= b exchanges two variables without a temporary — a curiosity that breaks when both refer to the same location. Prefix XOR: px[i] = a[0] ^ … ^ a[i-1] gives any range XOR as px[r+1] ^ px[l] in O(1), the XOR analogue of Prefix Sum — see Prefix XOR. Other identities: x ^ ~x = -1 (all ones), a ^ b = 0 ⟺ a == b, and (a ^ b) & (a ^ b) - 1 style tricks build on Power-of-Two Tricks.
Intuition
A mental model before the formal terms.
XOR is a light switch: flipping it twice returns it to where it started. XOR-ing a whole list into an accumulator flips a bit once per occurrence of each number, so anything appearing an even number of times ends up back at "off", and only odd occurrences leave a trace. That is why pairs vanish and the singleton survives regardless of order.
Prefix XOR is a running "switch state" from the start. The state of the range [l, r] alone is the state at r with the state before l undone — and undoing is the same operation as doing.
How it works
- Single number:
acc = 0; for x in a: acc ^= x; return acc. - Missing number:
acc = n; for i, x in enumerate(a): acc ^= i ^ x; return acc(nis included because indices only run ton - 1). - Two singles:
d = XOR of all;bit = d & -d;a = XOR of elements with (x & bit) != 0;b = d ^ a. - Swap:
a ^= b(a holdsa ^ b);b ^= a(b holdsb ^ a ^ b = a);a ^= b(a holdsa ^ b ^ a = b). - Prefix XOR:
px[0] = 0; px[i + 1] = px[i] ^ a[i]; queryl..raspx[r + 1] ^ px[l]. To count subarrays with XOR equal tok, count earlier prefixes equal topx ^ kin a hash map.
Why it works
By associativity and commutativity, x1 ^ x2 ^ … ^ xm can be regrouped so identical values sit next to each other; each pair reduces to 0 by self-inverse, and 0 is the identity, so only values with odd multiplicity contribute.
Two singles: d = a ^ b ≠ 0 because a ≠ b, so some bit differs. Splitting by that bit places a and b in different groups while every pair stays together (equal numbers have equal bits), so each group's XOR is its unpaired element.
Range XOR: px[r+1] = a[0] ^ … ^ a[r] and px[l] = a[0] ^ … ^ a[l-1]; XOR-ing them cancels the common prefix a[0..l-1], leaving a[l] ^ … ^ a[r].
Recognition
How to tell a problem wants this.
- "Every element appears twice except one", "appears an even number of times", "find the missing/duplicate number in
0..n". - "O(1) extra space" plus "without sorting" plus pairs — the classic signal for XOR cancellation.
- "XOR of subarray", "count subarrays with XOR k", "maximum XOR of two numbers" (the latter combines prefix bits with a Trie).
Interactive visualization
Play, step, change the input. ← → and space work too.
Showing the closely related Bitwise Operators visualization.
1a & b # 1 only where both bits are 12a | b # 1 where either bit is 13a ^ b # 1 where bits differ4~a # flip every bit (8-bit view)5a << 1 # shift left: doubles, drops the top bit6a >> 1 # shift right: halves, drops the low bitPseudocode
1// single number2acc = 03for x in a: acc ^= x4return acc5// missing number in 0..n (len(a) == n)6acc = n7for i in 0..n-1: acc ^= i ^ a[i]8return acc9// prefix xor query [l, r]10return px[r + 1] ^ px[l]Implementations
1from typing import List, Tuple2 3 41 · Single unpaired number5def single_number(nums: List[int]) -> int:6 acc = 07 for x in nums:8 acc ^= x # pairs cancel: a ^ a == 0, order irrelevant9 return acc10 11 122 · Missing number in 0..n13def missing_number(nums: List[int]) -> int:14 acc = len(nums) # seed with the one index the loop skips15 for i, x in enumerate(nums):16 acc ^= i ^ x17 return acc18 19 203 · Swap without a temporary21def xor_swap(a: int, b: int) -> Tuple[int, int]:22 # Included for the pattern; idiomatic Python is simply a, b = b, a23 if a == b:24 return a, b25 a ^= b26 b ^= a27 a ^= b28 return a, b29 30 314 · Prefix XOR for range queries32class XorRange:33 def __init__(self, a: List[int]) -> None:34 self.prefix = [0] * (len(a) + 1) # prefix[i] = a[0] ^ ... ^ a[i-1]35 for i, x in enumerate(a):36 self.prefix[i + 1] = self.prefix[i] ^ x37 38 def query(self, l: int, r: int) -> int:39 return self.prefix[r + 1] ^ self.prefix[l] # xor of a[l..r]40 41 425 · Demo43if __name__ == "__main__":44 assert single_number([4, 1, 2, 1, 2]) == 445 assert missing_number([3, 0, 1]) == 246 assert xor_swap(5, 9) == (9, 5)47 assert XorRange([1, 3, 4, 8]).query(1, 2) == 3 ^ 4- Python ints are unbounded, so the XOR folds are exact for any size — no 32-bit ceiling to think about.
missing_numberusesenumerateto XOR each index with its value; the seedlen(nums)supplies the final index.xor_swapis shown for the pattern;a, b = b, ais the idiomatic swap and the aliasing hazard cannot arise with immutable ints.XorRange.prefixis a plain list of exclusive prefixes;queryXORs two entries.
functools.reduce(operator.xor, nums, 0)is the stdlib fold.- XOR of negative ints follows infinite two's complement:
-1 ^ 1 == -2; results are exact, just mind the sign. - Since ints are immutable,
xor_swapreturns a tuple — it cannot swap the caller's variables in place.
- Using
sum(range(n + 1)) - sum(nums)and calling it equivalent — it is, in Python, but the XOR form is the transferable pattern (no bignum crutch elsewhere). - Expecting
xor_swapto mutate its arguments like the C++ reference version. - Rebuilding the prefix list on every query.
- Value range: C++ XOR is exact per type width (use
long longfor 64-bit); JS/TS truncate to 32-bit signed (BigInt beyond); Python is unbounded. - In-place swap: C++ swaps through references (aliasing guard required); JS/TS/Python cannot rebind caller variables — use destructuring / tuple assignment, which is also the idiomatic swap.
- The sum-formula alternative for missing-number can overflow C++
intand lose precision past 2^53 in JS/TS; the XOR version is exact in every language. - Fold spelling:
std::accumulate+std::bit_xor<>(C++),reduce((a, x) => a ^ x, 0)(JS/TS),functools.reduce(operator.xor, ...)(Python).
Complexity
Single/missing/two-singles are one or two linear passes with O(1) memory. Prefix XOR is O(n) build, O(1) per query, O(n) space.
Compare growth rates in the Complexity Explorer →When to use — and when not to
- Finding elements with odd multiplicity in
O(n)time andO(1)space, where sorting or hashing would cost more. - Range XOR queries on a static array, or counting subarrays by XOR value with a hash map of prefix values.
- Verifying equality of two multisets cheaply as a first filter (equal XOR is necessary, not sufficient).
- When the odd element appears three times or a different odd count and others appear twice — plain XOR does not distinguish; use per-bit counting mod 3 (Single Number II) or a hash map.
- XOR swap in real code: it is slower than a temporary on modern CPUs and silently zeroes a variable swapped with itself.
- When the array may contain the same value with different multiplicities that are both even — XOR yields 0 and tells you nothing about which values were present.
Alternatives
Common mistakes
- Missing number: forgetting to XOR
nitself (the index range is0..n-1but the value range is0..n). - Two singles: splitting on an arbitrary bit rather than a bit that is set in
a ^ b; the bit must differ between the two. - XOR swap of an element with itself (
swap(a[i], a[i])) — the first step sets it to 0. - Prefix XOR queries off by one: the query is
px[r + 1] ^ px[l], notpx[r] ^ px[l]. - Assuming XOR of a range equals the sum; it is not additive and cannot be used to compute sums.
Interview patterns
- Single Number I (XOR all), II (count bits mod 3), III (split by differing bit).
- Missing Number, Find the Duplicate (with a bit-counting variant), Find the Difference between two strings.
- XOR Queries of a Subarray via prefix XOR; count subarrays with XOR
kvia hash map of prefixes. - Maximum XOR of two numbers in an array: greedy bit by bit with a binary Trie of prefixes.
- Recognizing the approach from an array and a targetIntermediate
- Deciding whether O(n²) can be improvedIntermediate
- Recognizing a sliding-window problemIntermediate
- Prefix sum or segment tree?Intermediate
- Minimum Size Subarray SumIntermediate
- Subarray Sum Equals KIntermediate