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
Section titled “What you’ll learn”- The four identities that make XOR work, and the -space single-number trick.
- The lowest set bit idiom
x & -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
Section titled “The cue”The four identities
Section titled “The four identities”| Identity | Consequence |
|---|---|
| duplicates cancel | |
0 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])) # 4 time, space — which is the entire point. A Counter also solves
it in but needs space, and LC 136 explicitly asks for constant.
The lowest set bit, and splitting in two
Section titled “The lowest set bit, and splitting in two”LC 260 has two singles. XOR-ing everything gives a ^ b — the two answers
mixed together, and you cannot separate them from that alone.
But a ^ b is non-zero, so it has at least one set bit. Any set bit is a
position where a and b 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]Bit prefixes and the bitwise trie
Section titled “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 bestThe reasoning is greedy from the top: higher bits dominate, so if a 1 is
achievable at this position you always take it. And a ^ b = candidate implies
candidate ^ a = b, so checking whether some pair achieves candidate is a set
membership test.
time, space.
The trie version
inserts every number as a 32-step path of 0/1 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).
Visual intuition
Section titled “Visual intuition”Three properties combine into the trick: x ^ x = 0, x ^ 0 = x, and XOR is
commutative. So duplicates cancel regardless of order:
A hash set solves this too and is easier to explain, but costs O(n) memory — and the O(1) space requirement is the entire reason the problem is asked. Note the trick breaks if a value appears three times.
Dry run
Section titled “Dry run”LC 260 — nums = [1, 2, 1, 3, 2, 5], two singles (3 and 5) among pairs. Watch the running
XOR: the pairs cancel as they complete, not at the end.
n | xor_all after | binary | what just happened |
|---|---|---|---|
| 1 | 1 | 0001 | first 1 enters |
| 2 | 3 | 0011 | first 2 enters |
| 1 | 2 | 0010 | the 1s cancel — bit 0 clears |
| 3 | 1 | 0001 | 3 enters |
| 2 | 3 | 0011 | the 2s cancel, leaving 1 ⊕ 3 |
| 5 | 6 | 0110 | 5 enters; xor_all = 3 ^ 5 = 6 |
xor_all = 6 = 0110 — the two answers mixed. Now isolate the lowest set bit:
6 & -6 = 2 = 0010, and partition on it.
| group | members | XOR of group |
|---|---|---|
| bit 1 set | 2, 3, 2 | 3 |
| bit 1 clear | 1, 1, 5 | 5 |
Answer [3, 5].
- The intermediate values are meaningless and that is fine. After four numbers
xor_allis 1, which is neither a member of the array nor part of the answer. XOR accumulates cancellation, so only the final value carries information — a reason not to try debugging these loops by eyeballing intermediates. - Each pair lands entirely in one group. Both 2s have bit 1 set, both 1s have it clear. Equal numbers have equal bits, so no pair is ever split — which is why cancellation still works inside each group. That is the half of the argument people skip.
- 3 and 5 differ at bit 1 by construction.
3 = 011,5 = 101: the set bits of3 ^ 5 = 6are exactly the positions where they differ, so partitioning on any one of them separates them. The lowest is chosen only becausex & -xmakes it free. 6 & -6 = 2, and12 & -12 = 4. In two’s complement-xis~x + 1, which leaves everything below the lowest set bit as 0, that bit as 1, and everything above inverted — so the AND keeps exactly one bit.
LC 268 (missing number) on [3, 0, 1], the same identity used differently: XOR the indices
0..n together with every value, and everything pairs off except the missing one — result 2.
No sum, so no overflow concern in languages where that matters, and no sorting.
Complexity
Section titled “Complexity”| Problem | Time | Space | Note |
|---|---|---|---|
| LC 136 single number (pairs) | one accumulator | ||
| LC 268 missing number | XOR indices and values together | ||
| LC 389 the difference | XOR both strings’ characters | ||
| LC 260 two singles | two passes, one accumulator each | ||
| LC 137 appears three times | not XOR — bit counting mod 3 | ||
| LC 421 max XOR of two numbers | bitwise trie, or prefix-set per bit | ||
| Hash-set / sorting alternatives | / | / | correct, but the point of XOR is the space |
The space is the entire reason these problems are asked. A hash set solves LC 136 in time too — but the follow-up is always “now do it without extra memory”, and XOR is the answer to that specific question. Say the set solution first, then improve it.
The variant map
Section titled “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 & -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
Section titled “Practice — real LeetCode problems”LC 136 — Single Number · Easy
Section titled “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^4,
-3 * 10^4 <= nums[i] <= 3 * 10^4, and exactly one element appears once.
Examples. [2,2,1] gives 1 · [4,1,2,1,2] gives 4 · [1] gives 1
Editorial — approach, complexity, follow-ups
XOR every element into an accumulator seeded at 0. Each duplicated value
contributes x ^ 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] giving 5 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 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] giving 1 confirms that a 0 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..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?”
Counter, or2 * sum(set(nums)) - sum(nums); both space, so they miss the stated requirement.
LC 260 — Single Number III · Medium
Section titled “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^4, and the answer is unique.
Examples. [1,2,1,3,2,5] gives [3,5] (or [5,3]) · [-1,0] gives
[-1,0] · [0,1] gives [0,1]
Editorial — approach, complexity, follow-ups
Folding the array gives a ^ b, which mixes the two answers. To separate them,
find a bit where they differ — any set bit of a ^ 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.
aandbseparate. They differ at this bit by construction.
So each group reduces to LC 136.
Time — two passes. Space .
Any set bit works; x & -x just picks the lowest cheaply. xor_all is guaranteed
non-zero because a != b (the problem says the two singles are distinct), so
there is always at least one bit to pick.
[-1, 0] is the case worth thinking about. -1 ^ 0 = -1, and -1 & 1 = 1, so the
lowest set bit is 1. Then -1 has that bit set and 0 does not, so they split
correctly. Negative numbers work because Python’s two’s-complement semantics make
-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 & -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
Section titled “LC 421 — Maximum XOR of Two Numbers in an Array · Medium”Problem. Return the maximum result of nums[i] ^ nums[j] over all pairs.
Constraints. 1 <= len(nums) <= 2 * 10^5, 0 <= nums[i] <= 2^31 - 1.
Examples. [3,10,5,25,2,8] gives 28 (5 ^ 25) ·
[14,70,53,83,49,91,36,80,92,51,66,70] gives 127 · [0] gives 0
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 1 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: prefixes holds
n >> shift for every number. If there exist prefixes p and q with
p ^ q == candidate, then candidate is reachable — and since
p ^ q == candidate is equivalent to candidate ^ 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 <<= 1before testing. It shifts in a trailing0, so if the candidate fails,bestis already correct for this position with a0there. Noelsebranch needed.0must work.[0]gives0: no pair exists other than the element with itself, and0 ^ 0 = 0. Note the problem permitsi == j, so a single-element array is legal.
The bitwise trie alternative is worth describing: insert each number as a
32-level path of 0/1 children, then for each number walk down always
preferring the opposite bit when that child exists, since a differing bit
contributes a 1 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
Section titled “LeetCode problem set”Generated from the problem database, so each entry carries its sheet membership and reported companies. Tick them off as you go — progress is saved in this browser, and the Export button writes it to a file you can keep.
Work down the ladder. Tick each problem off as you go — progress is saved in this browser, and the Export button in the filter bar writes it to a file you can keep.
- 136Single NumbereasyFold with XOR; $O(1)$ space is the requirement
- 268Missing NumbereasyXOR indices with values -- no overflow, unlike the sum formula
- 1720Decode XORed ArrayeasyXOR is its own inverse, so just XOR forward
- 137Single Number IImediumTriples, so **not** XOR -- count bits mod 3
- 260Single Number IIImedium`x & -x` to split into two LC 136 problems
- 421Maximum XOR of Two Numbers in an ArraymediumGreedy bit prefixes, or a bitwise trie
Interview follow-ups
Section titled “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 & -x do?” | Bit fluency | Isolates the lowest set bit, via two’s complement (-x is ~x + 1) |
| “Why is any differing bit enough?” | Rigour | Pairs share all bits so they stay in one group and cancel; a and b 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
Section titled “Edge-case checklist”- Single element —
[1]for LC 136;[0]for LC 421. - Negative numbers —
[-1,-1,5]and[-1,0]; two’s complement makes the identities hold, but test them. - Zeros in the data — harmless for XOR, since
0is the identity;[0,1,0]gives1. - The two singles differing only in the lowest bit (LC 260) — e.g.
[0,1];x & -xstill isolates a valid split bit. - All elements identical in pairs — the accumulator ends at
0, which is only correct if a0really is the single. - Large values near (LC 421) — the loop must cover bit 31.
i == jpermitted (LC 421) — so a one-element array returns0.- Assuming XOR handles triples — the classic wrong reflex; it does not.
Self-check
Section titled “Self-check”-
Which property of XOR makes the single-number problems work?
Order-independence is what lets you fold the whole array in one pass. In the dry run the 1s cancel mid-stream, long before the array ends.
pch.quizShowAnswer
B — That x ^ x == 0 and x ^ 0 == x, and it is commutative and associative — so pairs cancel regardless of the order they appear in — Order-independence is what lets you fold the whole array in one pass. In the dry run the 1s cancel mid-stream, long before the array ends.
-
In LC 260 (two singles), why partition on a set bit of `a ^ b`?
Both halves of that argument are needed: the singles separate, AND the pairs stay together so cancellation still works inside each group. The second half is the one people omit.
pch.quizShowAnswer
B — Because the set bits of a ^ b are exactly the positions where a and b differ, so partitioning on any one of them puts them in different groups — while equal numbers have equal bits, so no pair is ever split — Both halves of that argument are needed: the singles separate, AND the pairs stay together so cancellation still works inside each group. The second half is the one people omit.
-
Why does `x & -x` isolate the lowest set bit?
12 & -12 = 4 (1100 → 0100). The same idiom is how a Fenwick tree walks its index structure, so it is worth owning rather than memorising.
pch.quizShowAnswer
B — Because in two's complement -x is ~x + 1, which leaves every bit below the lowest set bit as 0, that bit as 1, and everything above inverted — so the AND keeps exactly one bit — 12 & -12 = 4 (1100 → 0100). The same idiom is how a Fenwick tree walks its index structure, so it is worth owning rather than memorising.
-
The array has one single and everything else appears THREE times (LC 137). Does XOR still work?
XOR's power comes specifically from pairs. When the multiplicity changes, the tool changes — knowing the boundary is more useful than the trick itself.
pch.quizShowAnswer
B — No — three copies XOR to the value itself rather than to zero, so nothing cancels. Count set bits per position mod 3, or use the two-accumulator mod-3 counter — XOR's power comes specifically from pairs. When the multiplicity changes, the tool changes — knowing the boundary is more useful than the trick itself.
-
A hash set also solves LC 136 in O(n) time. Why is XOR the expected answer?
Both are O(n) time. The problem is really testing whether you can get to constant space, so leading with the set and then improving reads better than jumping straight to the trick.
pch.quizShowAnswer
B — Because it uses O(1) space, which is exactly what the standard follow-up asks for — mention the set solution first, then improve it — Both are O(n) time. The problem is really testing whether you can get to constant space, so leading with the set and then improving reads better than jumping straight to the trick.
-
You port an XOR bit-manipulation solution to Python and it loops or returns huge numbers. Why?
The identities themselves are width-independent, so LC 136/260/268 are fine. It bites on LC 371, LC 190 and signed inputs — a Python-specific follow-up worth pre-empting.
pch.quizShowAnswer
B — Python integers have arbitrary precision, so `~x` is -x-1 with infinitely many leading 1s rather than a 32-bit complement — mask with `x & 0xFFFFFFFF` and convert back if the result exceeds 2^31 — The identities themselves are width-independent, so LC 136/260/268 are fine. It bites on LC 371, LC 190 and signed inputs — a Python-specific follow-up worth pre-empting.
Recall card
Section titled “Recall card”- Cue — “every element appears twice except…”, “find the missing/duplicate number”, “the extra character”, or a problem asking for space where a hash set would be the obvious answer.
- The four identities —
x ^ x = 0,x ^ 0 = x, commutative, associative. Pairs cancel regardless of order, which is why one accumulator and one pass suffice. - LC 136 — fold the whole array with
^. - LC 268 — XOR the indices
0..nand the values; everything pairs off but the missing one. - LC 260 (two singles) — XOR everything to get
a ^ b, isolate any differing bit withx & -x, partition on it, then XOR each group. Pairs stay intact because equal numbers have equal bits. x & -xisolates the lowest set bit (two’s complement:~x + 1). Also how a Fenwick tree walks.- Boundary — three-of-a-kind (LC 137) is not XOR: count bits per position mod 3.
- Cost — time, space; that space bound is the reason the problem is asked.
- Python — mask with
0xFFFFFFFFwhen a problem assumes 32-bit signed wrap (LC 371, LC 190).
- 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 ^ b, isolate any set bit withx & -x, and partition — pairs stay together and cancel, whileaandbseparate. Then it is LC 136 twice. x & -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.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading