Bitwise XOR Patterns
XOR earns its own page because it has one property no other operator has, and that property solves a whole family of problems:
and .
So XOR-ing a collection cancels everything that appears an even number of times and leaves exactly what appears an odd number of times — without a hash map, in space, and in any order, because XOR is commutative and associative.
Once you see XOR as a self-cancelling accumulator, “find the element that appears once among pairs” stops being a puzzle and becomes a one-liner.
What you’ll learn
- The four identities that make XOR work, and the -space single-number trick.
- The lowest set bit idiom
x & -xx & -x, and how it splits a problem in two. - Why “appears three times” needs a different technique entirely.
- The bitwise trie: how “prefix” can mean bit prefix, turning maximum-XOR from into .
- Three real LeetCode problems solved in the browser: 136, 260, 421.
The cue
The four identities
| Identity | Consequence |
|---|---|
| duplicates cancel | |
00 is the identity, so it is the right accumulator seed | |
| order does not matter | |
| grouping does not matter |
Together they mean you can fold a whole array with XOR and the pairs vanish:
def single_number(nums):
result = 0 # 0 is the identity
for n in nums:
result ^= n # pairs cancel to 0
return result
print(single_number([4, 1, 2, 1, 2])) # 4def single_number(nums):
result = 0 # 0 is the identity
for n in nums:
result ^= n # pairs cancel to 0
return result
print(single_number([4, 1, 2, 1, 2])) # 4 time, space — which is the entire point. A CounterCounter also solves
it in but needs space, and LC 136 explicitly asks for constant.
The lowest set bit, and splitting in two
LC 260 has two singles. XOR-ing everything gives a ^ ba ^ b — the two answers
mixed together, and you cannot separate them from that alone.
But a ^ ba ^ b is non-zero, so it has at least one set bit. Any set bit is a
position where aa and bb differ. Pick one, partition the array by that bit, and
each half now contains exactly one single — so apply LC 136 twice.
def single_number_iii(nums):
xor_all = 0
for n in nums:
xor_all ^= n # == a ^ b
bit = xor_all & -xor_all # isolate the LOWEST set bit
a = b = 0
for n in nums:
if n & bit:
a ^= n # group where the bit is set
else:
b ^= n # group where it is not
return [a, b]def single_number_iii(nums):
xor_all = 0
for n in nums:
xor_all ^= n # == a ^ b
bit = xor_all & -xor_all # isolate the LOWEST set bit
a = b = 0
for n in nums:
if n & bit:
a ^= n # group where the bit is set
else:
b ^= n # group where it is not
return [a, b]Bit prefixes and the bitwise trie
LC 421 asks for the maximum XOR over all pairs. Brute force is , too slow at .
Two approaches, both built on the same idea — treat the numbers’ binary representations as strings and reason about prefixes:
def find_maximum_xor(nums):
best = 0
for shift in range(31, -1, -1): # decide the answer bit by bit
best <<= 1
prefixes = {n >> shift for n in nums} # all high-bit prefixes
candidate = best | 1 # can we achieve a 1 here?
if any(candidate ^ p in prefixes for p in prefixes):
best = candidate
# else the bit stays 0 (best already has a trailing 0 from the shift)
return bestdef find_maximum_xor(nums):
best = 0
for shift in range(31, -1, -1): # decide the answer bit by bit
best <<= 1
prefixes = {n >> shift for n in nums} # all high-bit prefixes
candidate = best | 1 # can we achieve a 1 here?
if any(candidate ^ p in prefixes for p in prefixes):
best = candidate
# else the bit stays 0 (best already has a trailing 0 from the shift)
return bestThe reasoning is greedy from the top: higher bits dominate, so if a 11 is
achievable at this position you always take it. And a ^ b = candidatea ^ b = candidate implies
candidate ^ a = bcandidate ^ a = b, so checking whether some pair achieves candidatecandidate is a set
membership test.
time, space.
The trie version
inserts every number as a 32-step path of 00/11 children, then for each number
walks down greedily taking the opposite bit wherever that child exists. Same
complexity, and it generalises better to problems with extra constraints (LC 1707
adds a limit on which numbers may be used).
The variant map
| Variant | The technique | Canonical problem |
|---|---|---|
| One single among pairs | Fold with XOR | 136 · 389 |
| Missing number in a range | XOR indices with values | 268 |
| Two singles among pairs | x & -xx & -x to split, then fold each half | 260 |
| One single among triples | Bit counting mod 3 — not XOR | 137 |
| Maximum XOR pair | Greedy bit prefixes, or a bitwise trie | 421 · 1707 |
| Decode an XOR-encoded array | XOR back cumulatively | 1720 · 1734 |
| Count subarrays with XOR = k | Prefix XOR + hash map | 1442-adjacent |
Practice — real LeetCode problems
LC 136 — Single Number · Easy
Problem. Every element appears twice except for one. Find that one. Your solution must have linear time complexity and use constant extra space.
Constraints. 1 <= len(nums) <= 3 * 10^41 <= len(nums) <= 3 * 10^4,
-3 * 10^4 <= nums[i] <= 3 * 10^4-3 * 10^4 <= nums[i] <= 3 * 10^4, and exactly one element appears once.
Examples. [2,2,1][2,2,1] gives 11 · [4,1,2,1,2][4,1,2,1,2] gives 44 · [1][1] gives 11
Editorial — approach, complexity, follow-ups
XOR every element into an accumulator seeded at 00. Each duplicated value
contributes x ^ x = 0x ^ x = 0, so only the unpaired value remains.
Time . Space .
The commutativity and associativity matter: they are why you do not need the duplicates to be adjacent, or the array sorted, or the pairs identified. A single pass in any order works.
[-1,-1,5][-1,-1,5] giving 55 is worth including because XOR on negative numbers can look
suspicious. Python’s integers are arbitrary-precision with conceptually infinite
sign extension, so -1 ^ -1 == 0-1 ^ -1 == 0 and the identity holds exactly as for positives.
In a fixed-width language the two’s-complement bits cancel the same way.
[0,1,0][0,1,0] giving 11 confirms that a 00 in the data is harmless — it is also the
accumulator’s identity, so it changes nothing.
Follow-ups you should expect:
- “What if one element appears once and the rest three times (LC 137)?” XOR fails, because three copies do not cancel. Count set bits per position mod 3.
- “Two elements appear once (LC 260)?” Split by a differing bit — next problem.
- “Find the missing number in
0..n0..n(LC 268)?” XOR the indices with the values; or use the sum formula, noting XOR cannot overflow. - “Find the duplicate without modifying the array (LC 287)?” XOR does not apply — use fast and slow pointers on the index graph.
- “Without XOR at all?”
CounterCounter, or2 * sum(set(nums)) - sum(nums)2 * sum(set(nums)) - sum(nums); both space, so they miss the stated requirement.
LC 260 — Single Number III · Medium
Problem. Exactly two elements appear once; every other element appears twice. Return the two singles in any order. Linear time, constant extra space.
Constraints. 2 <= len(nums) <= 3 * 10^42 <= len(nums) <= 3 * 10^4, and the answer is unique.
Examples. [1,2,1,3,2,5][1,2,1,3,2,5] gives [3,5][3,5] (or [5,3][5,3]) · [-1,0][-1,0] gives
[-1,0][-1,0] · [0,1][0,1] gives [0,1][0,1]
Editorial — approach, complexity, follow-ups
Folding the array gives a ^ ba ^ b, which mixes the two answers. To separate them,
find a bit where they differ — any set bit of a ^ ba ^ b qualifies, since a set bit
there means the two values disagree at that position.
Partitioning on that bit gives two groups, and both required properties hold:
- Pairs stay intact. Equal values have identical bits, so both copies land in the same group and still cancel.
aaandbbseparate. They differ at this bit by construction.
So each group reduces to LC 136.
Time — two passes. Space .
Any set bit works; x & -xx & -x just picks the lowest cheaply. xor_allxor_all is guaranteed
non-zero because a != ba != b (the problem says the two singles are distinct), so
there is always at least one bit to pick.
[-1, 0][-1, 0] is the case worth thinking about. -1 ^ 0 = -1-1 ^ 0 = -1, and -1 & 1 = 1-1 & 1 = 1, so the
lowest set bit is 11. Then -1-1 has that bit set and 00 does not, so they split
correctly. Negative numbers work because Python’s two’s-complement semantics make
-1-1 conceptually all-ones.
Follow-ups you should expect: “Why any differing bit rather than specifically
the lowest?” — any works; the lowest is just the cheapest to extract. “What does
x & -xx & -x do?” — isolates the lowest set bit via two’s complement; be ready to
explain it. “Three singles?” — much harder; a single bit no longer partitions them
cleanly and you need a more elaborate scheme. “Two singles among triples?” — XOR
does not apply at all; count bits with a different modulus.
LC 421 — Maximum XOR of Two Numbers in an Array · Medium
Problem. Return the maximum result of nums[i] ^ nums[j]nums[i] ^ nums[j] over all pairs.
Constraints. 1 <= len(nums) <= 2 * 10^51 <= len(nums) <= 2 * 10^5, 0 <= nums[i] <= 2^31 - 10 <= nums[i] <= 2^31 - 1.
Examples. [3,10,5,25,2,8][3,10,5,25,2,8] gives 2828 (5 ^ 255 ^ 25) ·
[14,70,53,83,49,91,36,80,92,51,66,70][14,70,53,83,49,91,36,80,92,51,66,70] gives 127127 · [0][0] gives 00
Editorial — approach, complexity, follow-ups
Build the answer one bit at a time, starting from the most significant. Higher
bits dominate the value, so whenever a 11 is achievable at the current position
you should take it — no later bit can compensate for giving it up.
To test achievability, consider only the high bits seen so far: prefixesprefixes holds
n >> shiftn >> shift for every number. If there exist prefixes pp and qq with
p ^ q == candidatep ^ q == candidate, then candidatecandidate is reachable — and since
p ^ q == candidatep ^ q == candidate is equivalent to candidate ^ p == qcandidate ^ p == q, that is a set
membership test rather than a nested loop.
Time — 32 bit positions, each doing set work. Space .
Two details:
best <<= 1best <<= 1before testing. It shifts in a trailing00, so if the candidate fails,bestbestis already correct for this position with a00there. Noelseelsebranch needed.00must work.[0][0]gives00: no pair exists other than the element with itself, and0 ^ 0 = 00 ^ 0 = 0. Note the problem permitsi == ji == j, so a single-element array is legal.
The bitwise trie alternative is worth describing: insert each number as a
32-level path of 00/11 children, then for each number walk down always
preferring the opposite bit when that child exists, since a differing bit
contributes a 11 at that position. Same , and it extends to variants with
extra constraints (LC 1707 restricts which numbers may be paired).
The connection worth naming: “prefix” need not mean a character prefix. Once you see numbers as bit strings, trie techniques apply to a whole family of XOR problems.
Follow-ups you should expect: “Implement it with a trie” — have the structure ready. “Why greedy from the top?” — higher bits outweigh all lower bits combined. “Maximum XOR with a limit on the second number (LC 1707)?” — sort the queries and insert into the trie incrementally. “Minimum XOR pair?” — different: sort the array, and the answer is between adjacent elements.
LeetCode problem set
| # | Problem | Difficulty | The twist |
|---|---|---|---|
| 136 | Single Number | Easy | Fold with XOR; space is the requirement |
| 268 | Missing Number | Easy | XOR indices with values — no overflow, unlike the sum formula |
| 1720 | Decode XORed Array | Easy | XOR is its own inverse, so just XOR forward |
| 260 | Single Number III | Medium | x & -xx & -x to split into two LC 136 problems |
| 137 | Single Number II | Medium | Triples, so not XOR — count bits mod 3 |
| 421 | Maximum XOR of Two Numbers in an Array | Medium | Greedy bit prefixes, or a bitwise trie |
Interview follow-ups
| They ask | What they’re checking | The answer |
|---|---|---|
| “Why does XOR work here?” | The identities | and , plus commutativity, so pairs cancel in any order |
“What does x & -xx & -x do?” | Bit fluency | Isolates the lowest set bit, via two’s complement (-x-x is ~x + 1~x + 1) |
| “Why is any differing bit enough?” | Rigour | Pairs share all bits so they stay in one group and cancel; aa and bb differ at that bit so they separate |
| “What about elements appearing three times?” | Knowing the boundary | XOR fails; count set bits per position mod 3 |
| “Why XOR over the sum formula for LC 268?” | Practical detail | XOR cannot overflow; the sum can in a fixed-width language |
| “Beat for maximum XOR?” | Depth | Greedy bit prefixes with a set, or a bitwise trie — |
| “Count subarrays with XOR = k?” | Composition | Prefix XOR + hash map, exactly like prefix sums, since XOR is its own inverse |
Edge-case checklist
- Single element —
[1][1]for LC 136;[0][0]for LC 421. - Negative numbers —
[-1,-1,5][-1,-1,5]and[-1,0][-1,0]; two’s complement makes the identities hold, but test them. - Zeros in the data — harmless for XOR, since
00is the identity;[0,1,0][0,1,0]gives11. - The two singles differing only in the lowest bit (LC 260) — e.g.
[0,1][0,1];x & -xx & -xstill isolates a valid split bit. - All elements identical in pairs — the accumulator ends at
00, which is only correct if a00really is the single. - Large values near (LC 421) — the loop must cover bit 31.
i == ji == jpermitted (LC 421) — so a one-element array returns00.- Assuming XOR handles triples — the classic wrong reflex; it does not.
Recap
- XOR is a self-cancelling accumulator: , , and it is commutative and associative. Folding an array leaves exactly what appears an odd number of times, in space.
- For two singles, XOR everything to get
a ^ ba ^ b, isolate any set bit withx & -xx & -x, and partition — pairs stay together and cancel, whileaaandbbseparate. Then it is LC 136 twice. x & -xx & -xisolates the lowest set bit through two’s complement; it also drives Fenwick trees.- XOR’s power comes from pairs. For triples, count set bits mod 3 instead.
- Prefer XOR over sum formulas when overflow is a concern.
- “Prefix” can mean bit prefix: greedy top-down bit construction or a bitwise trie turns maximum-XOR from into .
- Because XOR is its own inverse, prefix XOR behaves like prefix sums, and the prefix-plus-hash-map pattern transfers directly.
Next: Number Theory for Competitive Programming — primes, modular arithmetic, and the identities worth having memorised.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
