TL;DR
XOR the whole array; paired values cancel and the loner remains — O(n) time, O(1) space.
Approach 1 — Brute force: hash set (or counter)
The naive intuition: track what you’ve seen. Toggle each value in a set — add on first sight, remove on second — so only the unpaired value is left.
class Solution:
def singleNumber(self, nums: list[int]) -> int:
seen = set()
for num in nums:
if num in seen:
seen.remove(num)
else:
seen.add(num)
return seen.pop()
Complexity: O(n) time, O(n) space for the set.
Why we can do better: it meets the time bound but blows the O(1) space requirement — in the worst case the set holds ~n/2 elements. XOR removes the space entirely.
Approach 2 — XOR fold
The insight: XOR is self-inverse — x ^ x = 0 — and it is commutative and associative, so the order you combine values in doesn’t matter. Fold every element together with XOR: each value that appears twice meets its twin somewhere in the fold and the pair collapses to 0, no matter how the duplicates are scattered. The one value without a partner is XORed only with the accumulated 0, and x ^ 0 = x, so it survives as the final result. One variable, one pass, no extra space.
At the bit level, XOR compares the two operands column by column and outputs 1 only where the bits differ. Two copies of the same number agree in every column, so they XOR to all-zeros; that is why pairs vanish.
class Solution:
def singleNumber(self, nums: list[int]) -> int:
result = 0
for num in nums:
result ^= num
return result
Walkthrough on [4, 1, 2, 1, 2] (result starts at 0):
| num | result before | result after (^= num) |
|---|
| 4 | 0 | 4 |
| 1 | 4 | 5 |
| 2 | 5 | 7 |
| 1 | 7 | 6 |
| 2 | 6 | 4 |
Returns 4. ✓ Reordering the array as [1, 1, 2, 2, 4] makes it obvious: (1^1) ^ (2^2) ^ 4 = 0 ^ 0 ^ 4 = 4.
Complexity: O(n) time, O(1) space — one accumulator.
A one-liner using the same fold:
from functools import reduce
from operator import xor
class Solution:
def singleNumber(self, nums: list[int]) -> int:
return reduce(xor, nums, 0)
Common pitfalls
- Seeding with the wrong identity: start the accumulator at
0 (XOR’s identity). Seeding with nums[0] also works only if you then skip that element — easy to double-count.
- Reaching for sum tricks:
2 * sum(set(nums)) - sum(nums) gives the same answer but uses O(n) space for the set — no better than the hash approach.
- Assuming “twice” means adjacent: duplicates can be anywhere; XOR’s order-independence is exactly what makes that irrelevant.
- Generalizing carelessly: this XOR fold only works when the odd one out appears once and everyone else an even number of times. “Every other element appears three times” (Single Number II) needs a different bit-counting scheme.
Pattern takeaway
XOR is the canonical tool for “find the unpaired element”: folding a collection with XOR cancels every value that appears an even number of times and leaves the odd one out. Remember the two identities — x ^ x = 0 and x ^ 0 = x — and that order never matters, and a whole class of “find the single/different value” problems collapses to one pass and one variable.