Skip to content

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:

xx=0x \oplus x = 0 and x0=xx \oplus 0 = x.

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 O(1)O(1) 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 O(1)O(1)-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 O(n2)O(n^2) into O(32n)O(32n).
  • Three real LeetCode problems solved in the browser: 136, 260, 421.

The cue

The four identities

IdentityConsequence
xx=0x \oplus x = 0duplicates cancel
x0=xx \oplus 0 = x00 is the identity, so it is the right accumulator seed
ab=baa \oplus b = b \oplus aorder does not matter
(ab)c=a(bc)(a \oplus b) \oplus c = a \oplus (b \oplus c)grouping does not matter

Together they mean you can fold a whole array with XOR and the pairs vanish:

single_number.py
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
single_number.py
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

O(n)O(n) time, O(1)O(1) space — which is the entire point. A CounterCounter also solves it in O(n)O(n) but needs O(n)O(n) 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.

single_number_iii.py
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]
single_number_iii.py
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 O(n2)O(n^2), too slow at n=2×105n = 2 \times 10^5.

Two approaches, both built on the same idea — treat the numbers’ binary representations as strings and reason about prefixes:

max_xor_prefix.py
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 best
max_xor_prefix.py
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 best

The 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.

O(32n)O(32n) time, O(n)O(n) 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

VariantThe techniqueCanonical problem
One single among pairsFold with XOR136 · 389
Missing number in a rangeXOR indices with values268
Two singles among pairsx & -xx & -x to split, then fold each half260
One single among triplesBit counting mod 3 — not XOR137
Maximum XOR pairGreedy bit prefixes, or a bitwise trie421 · 1707
Decode an XOR-encoded arrayXOR back cumulatively1720 · 1734
Count subarrays with XOR = kPrefix XOR + hash map1442-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 O(n)O(n). Space O(1)O(1).

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, or 2 * sum(set(nums)) - sum(nums)2 * sum(set(nums)) - sum(nums); both O(n)O(n) 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.
  • aa and bb separate. They differ at this bit by construction.

So each group reduces to LC 136.

Time O(n)O(n) — two passes. Space O(1)O(1).

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 O(32n)O(32n) — 32 bit positions, each doing O(n)O(n) set work. Space O(n)O(n).

Two details:

  • best <<= 1best <<= 1 before testing. It shifts in a trailing 00, so if the candidate fails, bestbest is already correct for this position with a 00 there. No elseelse branch needed.
  • 00 must work. [0][0] gives 00: no pair exists other than the element with itself, and 0 ^ 0 = 00 ^ 0 = 0. Note the problem permits i == 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 O(32n)O(32n), 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

#ProblemDifficultyThe twist
136Single NumberEasyFold with XOR; O(1)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
260Single Number IIIMediumx & -xx & -x to split into two LC 136 problems
137Single Number IIMediumTriples, so not XOR — count bits mod 3
421Maximum XOR of Two Numbers in an ArrayMediumGreedy bit prefixes, or a bitwise trie

Interview follow-ups

They askWhat they’re checkingThe answer
“Why does XOR work here?”The identitiesxx=0x \oplus x = 0 and x0=xx \oplus 0 = x, plus commutativity, so pairs cancel in any order
“What does x & -xx & -x do?”Bit fluencyIsolates the lowest set bit, via two’s complement (-x-x is ~x + 1~x + 1)
“Why is any differing bit enough?”RigourPairs 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 boundaryXOR fails; count set bits per position mod 3
“Why XOR over the sum formula for LC 268?”Practical detailXOR cannot overflow; the sum can in a fixed-width language
“Beat O(n2)O(n^2) for maximum XOR?”DepthGreedy bit prefixes with a set, or a bitwise trie — O(32n)O(32n)
“Count subarrays with XOR = k?”CompositionPrefix 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 00 is the identity; [0,1,0][0,1,0] gives 11.
  • The two singles differing only in the lowest bit (LC 260) — e.g. [0,1][0,1]; x & -xx & -x still isolates a valid split bit.
  • All elements identical in pairs — the accumulator ends at 00, which is only correct if a 00 really is the single.
  • Large values near 2312^{31} (LC 421) — the loop must cover bit 31.
  • i == ji == j permitted (LC 421) — so a one-element array returns 00.
  • Assuming XOR handles triples — the classic wrong reflex; it does not.

Recap

  • XOR is a self-cancelling accumulator: xx=0x \oplus x = 0, x0=xx \oplus 0 = x, and it is commutative and associative. Folding an array leaves exactly what appears an odd number of times, in O(1)O(1) space.
  • For two singles, XOR everything to get a ^ ba ^ b, isolate any set bit with x & -xx & -x, and partition — pairs stay together and cancel, while aa and bb separate. Then it is LC 136 twice.
  • x & -xx & -x isolates 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 O(n2)O(n^2) into O(32n)O(32n).
  • 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 coffee

Was this page helpful?

Let us know how we did