Problem overview
The method singleNumber receives a non-empty array of integers in which every value appears exactly twice, except for one value that appears once. It returns that single value.
For nums = [2, 2, 1] the answer is 1. For [4, 1, 2, 1, 2] it is 4, and for [1] it is 1. The lesson asks for O(n) time and O(1) extra space, so sorting the array or counting values in a map is not allowed. The array can also hold negative numbers.
Approach: XOR cancels the pairs
The bitwise XOR operator, written ^, combines two numbers bit by bit: each result bit is 1 when exactly one of the two input bits is 1. Two properties solve this problem:
x ^ xis 0, because a number cancels itself.x ^ 0isx, because XOR with 0 changes nothing.
XOR is also commutative and associative, so the order in which numbers are combined doesn't matter. XOR all the elements of the array together. Every value that appears twice is combined with its twin, wherever the twin sits, and the pair contributes 0. Only the single value has no twin, so the final result is that value.
For [4, 1, 2, 1, 2] the calculation is 4 ^ 1 ^ 2 ^ 1 ^ 2. Regrouped, it is 4 ^ (1 ^ 1) ^ (2 ^ 2), which is 4 ^ 0 ^ 0, which is 4.
Step by step
- Declare
swithlet, starting at 0. It is the accumulator. Zero is the neutral element of XOR, so it doesn't disturb the first value. - Loop over
numswithfor...of, naming each elementval. - Assign
s = s ^ valin the loop. Each element is folded into the running result. - Return
safter the loop.
The solution in TypeScript
class Solution {
singleNumber(nums: number[]): number {
let s = 0;
for (const val of nums) {
s = s ^ val;
}
return s;
}
}
The TypeScript details worth a closer look:
- Bitwise operators work on 32-bit integers. A TypeScript
numberis a double, but^first converts both operands to 32-bit signed integers and gives a 32-bit signed result. The lesson's values are within plus or minus 30,000, far inside that range, so nothing is lost. A value beyond2**31would be truncated by the conversion, and the answer would be wrong. - Negative numbers are fine. Negative integers are stored in two's complement, and the XOR rules still hold.
[-3, 5, -3]gives 5, and[-30000, 7, 7]gives -30000. s = s ^ valcan be writtens ^= val. They mean the same.- Inferred types.
let s = 0is inferred asnumber. The annotation onnumsis erased when compiling, so nothing checks at run time that the argument really is an array of numbers. for...ofversusfor...in.for (const val of nums)gives the values.for...inwould give the indexes, as strings.
Complexity
Time: O(n). The loop visits each element once and does one XOR per element.
Space: O(1). The method uses the accumulator and the loop variable, however long the array is.
Edge cases and common mistakes
- One element.
[1]gives0 ^ 1, which is 1. - The single value is first, last or in the middle. Its position doesn't matter, because the order of XOR doesn't matter.
[1, 2, 2],[2, 1, 2]and[2, 2, 1]all return 1. - Negative values. The result keeps its sign:
[-3, 5, -3]returns 5, and[-1, -1, -5]returns -5. - Starting with something other than 0. A different start value would stay mixed into the result.
- Using
+instead of^. Adding the elements only gives their sum, and nothing cancels. In TypeScript the symbol^is XOR, not exponentiation (that is**). - Relying on the guarantee. The method only works when every other value appears exactly twice. A value that appears three times would survive the XOR.
Testing
nums = [2, 2, 1]gives1.nums = [4, 1, 2, 1, 2]gives4.nums = [1]gives1.nums = [-3, 5, -3]gives5.
