XOR techniques
XOR cancels equal values, which gives clean linear-time, constant-space solutions to a family of puzzles: the number that appears once, the missing number, and range XOR queries. This lesson also covers the one XOR trick you should avoid.
Learning goals
- State the properties of XOR that make cancellation work
- Find the unique element and the missing number in one pass
- Handle variants: values repeated three times, and two unique values
- Answer range XOR queries with prefix XOR, and explain why XOR swap is not worth using
Prerequisites
XOR cancellation
XOR has four properties: a ^ a = 0, a ^ 0 = a, it is commutative (a ^ b = b ^ a) and associative. Together they mean that in a long chain of XORs, every value that appears twice cancels, whatever the order.
So x ^ y ^ x = y. The next lessons build on this: XOR everything and only the values with odd multiplicity survive.
| Property | Example |
|---|---|
| Self-inverse | 5 ^ 5 = 0 |
| Identity | 5 ^ 0 = 5 |
| Commutative | 3 ^ 6 = 6 ^ 3 |
| Associative | (1 ^ 2) ^ 3 = 1 ^ (2 ^ 3) |
| Undo | (a ^ b) ^ b = a |
Find unique number
In an array where every value appears twice except one, XOR all elements. The pairs cancel to 0 and the unique value remains. The animation shows the running XOR in binary: each repeated value flips its bits back.
Time is O(n) and space O(1). A hash map or sort would work too, but needs O(n) extra space or O(n log n) time.
1static int findUnique(int[] a) {2int x = 0;3for (int v : a) x ^= v;4return x;5}
Worst
O(n)
Space
O(1)
XOR: the number that appears once
Interactive · edit the inputint[] arr · length 5
x = 0
x starts at 0, the identity for XOR: 0 ^ v = v.
1static int findUnique(int[] a) {2int x = 0;3for (int v : a) {4x ^= v; // equal pairs cancel: v ^ v == 05}6return x;7}
Live state
x
0
Execution trace (dry run table)Show 7 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | x=0 | x starts at 0, the identity for XOR: 0 ^ v = v. |
| 2 | L4 | i=0 v=4 x=4 | x = 0 ^ 4 = 4. |
| 3 | L4 | i=1 v=1 x=5 | x = 4 ^ 1 = 5. |
| 4 | L4 | i=2 v=2 x=7 | x = 5 ^ 2 = 7. |
| 5 | L4 | i=3 v=1 x=6 | x = 7 ^ 1 = 6. 1 appeared earlier, so this XOR cancels it out. |
| 6 | L4 | i=4 v=2 x=4 | x = 6 ^ 2 = 4. 2 appeared earlier, so this XOR cancels it out. |
| 7 | L6 | return=4 | return 4. Every value that appears twice cancelled itself (v ^ v = 0), and the order did not matter because XOR is commutative and associative. One pass, O(1) extra space. |
Missing number
Given n distinct numbers taken from 0..n, one number is missing. XOR the indices 0..n-1 and the value n together with all the array values. Every number present appears twice (once as an index or n, once as a value) and cancels, leaving the missing one. Try the “Missing 0” and “Missing n” presets.
The sum formula n(n + 1)/2 - sum also works, but the sum can overflow int for large n. XOR cannot overflow.
Worst
O(n)
Space
O(1)
XOR: the missing number
Interactive · edit the inputint[] arr · length 5
x = n = 5
The numbers 0..5 should all appear. XOR every expected number (the indices 0..4 and n) with every number present: everything cancels except the missing one.
1static int missing(int[] a) { // a holds 0..n except one value2int x = a.length; // start with n3for (int i = 0; i < a.length; i++) {4x ^= i ^ a[i]; // XOR in index and value5}6return x;7}
Live state
n
5
x
5
Execution trace (dry run table)Show 7 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | n=5 x=5 | The numbers 0..5 should all appear. XOR every expected number (the indices 0..4 and n) with every number present: everything cancels except the missing one. |
| 2 | L4 | i=0 a[i]=3 x=6 | x = 5 ^ 0 ^ 3 = 6. |
| 3 | L4 | i=1 a[i]=0 x=7 | x = 6 ^ 1 ^ 0 = 7. |
| 4 | L4 | i=2 a[i]=1 x=4 | x = 7 ^ 2 ^ 1 = 4. |
| 5 | L4 | i=3 a[i]=5 x=2 | x = 4 ^ 3 ^ 5 = 2. |
| 6 | L4 | i=4 a[i]=2 x=4 | x = 2 ^ 4 ^ 2 = 4. |
| 7 | L6 | return=4 | return 4. It is the one number from 0..5 that has an index or n on one side but no matching value on the other. O(n) time, O(1) space, and no overflow risk, unlike the sum formula n(n+1)/2 - sum. |
Single Number variants
If every value appears three times except one, plain XOR fails (three copies do not cancel). Instead count, for each bit position, how many numbers have that bit set, modulo 3. The bits that remain make up the unique number.
If two values appear once and the rest twice, XOR everything to get a ^ b. Any set bit of that result separates a from b. Use the lowest one to split the array into two groups, and XOR each group to recover a and b.
1// every number appears 3 times except one2static int singleOfThree(int[] a) {3int result = 0;4for (int bit = 0; bit < 32; bit++) {5int count = 0;6for (int v : a) count += (v >> bit) & 1;7if (count % 3 != 0) result |= 1 << bit;8}9return result;10}1112// two numbers appear once, all others twice13static int[] twoSingles(int[] a) {14int xor = 0;15for (int v : a) xor ^= v; // a ^ b16int lowest = xor & -xor; // a bit where a and b differ17int x = 0, y = 0;18for (int v : a) {19if ((v & lowest) != 0) x ^= v; else y ^= v;20}21return new int[] { x, y };22}
XOR prefix
As with sums, define prefix[i] = a[0] ^ … ^ a[i-1]. Then the XOR of any range is prefix[r + 1] ^ prefix[l], because the shared prefix cancels. Range XOR queries take O(1) after an O(n) build.
It also solves counting problems: the number of subarrays with XOR equal to k is the number of pairs of prefixes whose XOR is k, found with a hash map of prefix values.
1static int[] buildPrefixXor(int[] a) {2int[] p = new int[a.length + 1];3for (int i = 0; i < a.length; i++) p[i + 1] = p[i] ^ a[i];4return p;5}6static int rangeXor(int[] p, int l, int r) { return p[r + 1] ^ p[l]; } // inclusive l..r78// count subarrays whose XOR is k9static int countXorK(int[] a, int k) {10Map<Integer, Integer> seen = new HashMap<>();11seen.put(0, 1);12int prefix = 0, count = 0;13for (int v : a) {14prefix ^= v;15count += seen.getOrDefault(prefix ^ k, 0); // an earlier prefix p with p ^ prefix == k16seen.merge(prefix, 1, Integer::sum);17}18return count;19}
XOR swap pitfalls
The XOR swap, a ^= b; b ^= a; a ^= b;, exchanges two variables without a temporary. It is a well-known trick but a poor choice: it is not faster than a temporary on modern CPUs and compilers, and it is harder to read.
It also has a real bug: if both operands refer to the same location, for example swapping arr[i] with arr[j] when i == j, the first step sets that value to x ^ x = 0, and the value is lost.
1static void xorSwap(int[] arr, int i, int j) {2arr[i] ^= arr[j];3arr[j] ^= arr[i];4arr[i] ^= arr[j];5}6int[] a = {7, 8};7xorSwap(a, 0, 1); // [8, 7] works8xorSwap(a, 1, 1); // [8, 0] the element was zeroed, because i == j910static void swap(int[] arr, int i, int j) { int t = arr[i]; arr[i] = arr[j]; arr[j] = t; } // prefer this
Practice ladder
Now try it yourself
Easy
- Single number (every other appears twice)
- Missing number in 0..n
- Check whether two numbers have opposite signs using XOR
Medium
- Single number III (two values appear once)
- Single number II (others appear three times)
- XOR queries of a subarray
Advanced
- Count subarrays with XOR equal to k
- Maximum XOR of two numbers in an array (bitwise trie)
- Find the number that appears odd times when several numbers do
Key takeaways
- XOR is self-inverse, commutative and associative, so equal pairs cancel in any order.
- One XOR pass finds the unique or missing number in O(n) time and O(1) space with no overflow.
- Prefix XOR answers range XOR queries; skip the XOR swap.