260. Single Number III
Problem Description
You're given an integer array where every element appears exactly twice, except for two special elements that appear only once. Your task is to find these two unique elements.
The problem has specific constraints:
- The array contains mostly duplicates (each appearing exactly twice)
- Only two elements are unique (appearing exactly once)
- You can return the two unique elements in any order
- Your solution must run in linear time complexity
O(n)
- Your solution must use only constant extra space
O(1)
For example, if the input array is [1, 2, 1, 3, 2, 5]
, the two unique elements are 3
and 5
since:
1
appears twice2
appears twice3
appears only once5
appears only once
The challenge is to identify these two unique numbers efficiently without using extra space for tracking occurrences (like a hash map), which makes this a classic bit manipulation problem.
Intuition
Let's think about what happens when we XOR all numbers in the array. Since XOR has the property that a ^ a = 0
and a ^ 0 = a
, all the duplicate pairs will cancel out. We'll be left with unique1 ^ unique2
- the XOR of the two unique numbers.
Now we have unique1 ^ unique2
, but how do we separate them? The key insight is that since unique1
and unique2
are different numbers, their XOR result must have at least one bit that is 1
. This 1
bit represents a position where unique1
and unique2
differ - one has a 0
at that position while the other has a 1
.
We can use this differentiating bit to partition the entire array into two groups:
- Group A: numbers that have a
1
at this bit position - Group B: numbers that have a
0
at this bit position
Here's the crucial observation: since duplicate numbers are identical, both copies of any duplicate will fall into the same group. Meanwhile, unique1
and unique2
will be separated into different groups because they differ at this bit position.
Once we've partitioned the array this way, each group becomes a simpler problem - finding the single unique number among pairs of duplicates. We can XOR all numbers in Group A to get one unique number, and XOR all numbers in Group B to get the other.
To find a differentiating bit efficiently, we use the trick xs & -xs
which isolates the rightmost 1
bit in xs
(where xs = unique1 ^ unique2
). This is called the lowbit operation and gives us a clean way to pick a bit position for partitioning.
Solution Approach
Let's walk through the implementation step by step:
Step 1: XOR all elements
xs = reduce(xor, nums)
We XOR all numbers in the array. Due to XOR properties (x ^ x = 0
and x ^ 0 = x
), all duplicate pairs cancel out, leaving us with xs = unique1 ^ unique2
.
Step 2: Find the differentiating bit
lb = xs & -xs
This lowbit operation isolates the rightmost 1
bit in xs
. Here's how it works:
- If
xs = 0110
(binary), then-xs = 1010
(two's complement) xs & -xs = 0110 & 1010 = 0010
, which isolates the rightmost1
bit
This bit represents a position where unique1
and unique2
differ.
Step 3: Partition and find the first unique number
a = 0 for x in nums: if x & lb: a ^= x
We iterate through the array and XOR only the numbers that have a 1
at the differentiating bit position (checked by x & lb
). This effectively:
- Groups all numbers based on whether they have
1
or0
at that bit position - XORs all numbers in the group that has
1
at that position - Since duplicates fall into the same group and cancel out, we're left with one of the unique numbers
Step 4: Find the second unique number
b = xs ^ a
Since xs = unique1 ^ unique2
and we've found one unique number a
, we can find the other by XORing: b = xs ^ a = (unique1 ^ unique2) ^ unique1 = unique2
.
Step 5: Return the result
return [a, b]
The algorithm achieves O(n)
time complexity with two passes through the array and O(1)
space complexity using only a few variables.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through the algorithm with the array [1, 2, 1, 3, 2, 5]
:
Step 1: XOR all elements
1 ^ 2 ^ 1 ^ 3 ^ 2 ^ 5
= (1 ^ 1) ^ (2 ^ 2) ^ 3 ^ 5 // Rearranging pairs
= 0 ^ 0 ^ 3 ^ 5 // Pairs cancel out
= 3 ^ 5
= 011 ^ 101 (in binary)
= 110 (which is 6 in decimal)
So xs = 6
(binary: 110
)
Step 2: Find the differentiating bit
xs = 110 (binary) -xs = ...11111010 (two's complement) xs & -xs = 110 & ...11111010 = 010 (which is 2 in decimal)
So lb = 2
(binary: 010
), meaning we'll partition based on the 2nd bit from the right.
Step 3: Partition based on the differentiating bit
Check each number's 2nd bit (using x & lb
):
1
(binary:001
):001 & 010 = 000
→ Group B (bit is 0)2
(binary:010
):010 & 010 = 010
→ Group A (bit is 1)1
(binary:001
):001 & 010 = 000
→ Group B (bit is 0)3
(binary:011
):011 & 010 = 010
→ Group A (bit is 1)2
(binary:010
):010 & 010 = 010
→ Group A (bit is 1)5
(binary:101
):101 & 010 = 000
→ Group B (bit is 0)
Group A contains: [2, 3, 2]
Group B contains: [1, 1, 5]
XOR Group A elements:
a = 0 a = 0 ^ 2 = 2 a = 2 ^ 3 = 1 a = 1 ^ 2 = 3
So a = 3
Step 4: Find the second unique number
b = xs ^ a = 6 ^ 3 = 110 ^ 011 = 101 = 5
Step 5: Return result
Return [3, 5]
- the two unique numbers we were looking for!
The beauty of this approach is that the duplicate pairs (1,1
and 2,2
) automatically ended up in the same groups and canceled themselves out through XOR, leaving only the unique numbers.
Solution Implementation
1from functools import reduce
2from operator import xor
3from typing import List
4
5class Solution:
6 def singleNumber(self, nums: List[int]) -> List[int]:
7 # XOR all numbers to get the XOR of the two unique numbers
8 # Since all duplicates will cancel out (x ^ x = 0)
9 xor_result = reduce(xor, nums)
10
11 # Find the rightmost set bit in xor_result
12 # This bit must differ between the two unique numbers
13 # Using two's complement: -x = ~x + 1
14 rightmost_bit = xor_result & -xor_result
15
16 # Partition numbers into two groups based on the rightmost bit
17 # Group 1: numbers with this bit set
18 group1_xor = 0
19 for num in nums:
20 if num & rightmost_bit:
21 group1_xor ^= num
22
23 # The other unique number is in the other group
24 # Can be found by XORing with the total XOR result
25 group2_xor = xor_result ^ group1_xor
26
27 return [group1_xor, group2_xor]
28
1class Solution {
2 public int[] singleNumber(int[] nums) {
3 // Step 1: XOR all numbers to get the XOR of the two unique numbers
4 // Since duplicate numbers cancel out (a ^ a = 0), we get uniqueNum1 ^ uniqueNum2
5 int xorOfTwoUniques = 0;
6 for (int num : nums) {
7 xorOfTwoUniques ^= num;
8 }
9
10 // Step 2: Find the rightmost set bit in the XOR result
11 // This bit must differ between the two unique numbers
12 // Using x & -x isolates the rightmost set bit
13 int differentiatingBit = xorOfTwoUniques & -xorOfTwoUniques;
14
15 // Step 3: Partition numbers into two groups based on the differentiating bit
16 // Group 1: Numbers with this bit set
17 // Group 2: Numbers with this bit unset
18 // Each group will contain one unique number and pairs of duplicates
19 int firstUniqueNum = 0;
20 for (int num : nums) {
21 // XOR all numbers that have the differentiating bit set
22 // Duplicates cancel out, leaving only one unique number
23 if ((num & differentiatingBit) != 0) {
24 firstUniqueNum ^= num;
25 }
26 }
27
28 // Step 4: Calculate the second unique number
29 // Since xorOfTwoUniques = firstUniqueNum ^ secondUniqueNum
30 // We can get secondUniqueNum = xorOfTwoUniques ^ firstUniqueNum
31 int secondUniqueNum = xorOfTwoUniques ^ firstUniqueNum;
32
33 return new int[] {firstUniqueNum, secondUniqueNum};
34 }
35}
36
1class Solution {
2public:
3 vector<int> singleNumber(vector<int>& nums) {
4 // Step 1: XOR all numbers to get the XOR of the two unique numbers
5 // Since duplicate numbers cancel out (x ^ x = 0), we get a ^ b
6 long long xorSum = 0;
7 for (int& num : nums) {
8 xorSum ^= num;
9 }
10
11 // Step 2: Find the rightmost set bit in xorSum
12 // This bit must differ between the two unique numbers
13 // Using x & -x isolates the rightmost set bit
14 int rightmostBit = xorSum & -xorSum;
15
16 // Step 3: Partition numbers into two groups based on the rightmost bit
17 // Group 1: numbers with this bit set
18 // Group 2: numbers with this bit unset
19 // Each group will contain one unique number and pairs of duplicates
20 int firstUnique = 0;
21 for (int& num : nums) {
22 // XOR all numbers that have the rightmost bit set
23 // Duplicates cancel out, leaving only one unique number
24 if (num & rightmostBit) {
25 firstUnique ^= num;
26 }
27 }
28
29 // Step 4: Calculate the second unique number
30 // Since xorSum = firstUnique ^ secondUnique
31 // We can get secondUnique = xorSum ^ firstUnique
32 int secondUnique = xorSum ^ firstUnique;
33
34 return {firstUnique, secondUnique};
35 }
36};
37
1/**
2 * Finds two unique numbers in an array where all other numbers appear exactly twice.
3 * Uses XOR properties to separate the two unique numbers.
4 *
5 * @param nums - Array containing numbers where exactly two numbers appear once and others appear twice
6 * @returns Array containing the two unique numbers
7 */
8function singleNumber(nums: number[]): number[] {
9 // XOR all numbers to get the XOR of the two unique numbers
10 // Since duplicate numbers cancel out (x ^ x = 0), we get uniqueNum1 ^ uniqueNum2
11 const xorOfTwoUniques: number = nums.reduce((accumulator, current) => accumulator ^ current);
12
13 // Find the rightmost set bit in the XOR result
14 // This bit must be different between the two unique numbers
15 // Using two's complement: -x flips all bits and adds 1, & with x isolates rightmost set bit
16 const rightmostSetBit: number = xorOfTwoUniques & -xorOfTwoUniques;
17
18 // Partition numbers into two groups based on the rightmost set bit
19 // One group will contain one unique number, the other group will contain the other
20 let firstUniqueNumber: number = 0;
21 for (const num of nums) {
22 // If this bit is set in the current number, XOR it with firstUniqueNumber
23 // Duplicates in this group will cancel out, leaving only one unique number
24 if (num & rightmostSetBit) {
25 firstUniqueNumber ^= num;
26 }
27 }
28
29 // The second unique number can be obtained by XORing the first unique number with xorOfTwoUniques
30 // Since xorOfTwoUniques = firstUniqueNumber ^ secondUniqueNumber
31 // Therefore: secondUniqueNumber = xorOfTwoUniques ^ firstUniqueNumber
32 const secondUniqueNumber: number = xorOfTwoUniques ^ firstUniqueNumber;
33
34 return [firstUniqueNumber, secondUniqueNumber];
35}
36
Time and Space Complexity
The time complexity is O(n)
, where n
is the length of the array nums
. This is because:
- The
reduce(xor, nums)
operation iterates through alln
elements once to compute the XOR of all numbers, takingO(n)
time - The second loop
for x in nums:
also iterates through alln
elements once to separate the two unique numbers, takingO(n)
time - All other operations (
xs & -xs
,xs ^ a
, etc.) are constant timeO(1)
operations - Total:
O(n) + O(n) + O(1) = O(n)
The space complexity is O(1)
. This is because:
- The algorithm only uses a fixed number of variables (
xs
,a
,b
,lb
) regardless of the input size - No additional data structures that grow with input size are created
- The
reduce
function operates in-place without creating additional storage proportional to the input
Common Pitfalls
1. Misunderstanding the Lowbit Operation
Many developers struggle with understanding why xs & -xs
isolates the rightmost set bit. A common mistake is trying to use other bit operations like xs & 1
(which only checks if the number is odd) or xs >> 1
(which shifts bits).
Why it happens: The two's complement representation (-x
) flips all bits and adds 1, which creates a specific pattern that when ANDed with the original number, isolates only the rightmost 1
bit.
Solution: Remember that for any number x
:
-x
in binary flips all bits to the left of the rightmost1
and keeps the rightmost1
and all0
s to its right unchanged- When you AND them, only the rightmost
1
survives
2. Incorrect Group Partitioning Logic
A frequent error is using the wrong condition to partition numbers into groups. Some might try if x == lb:
or if x ^ lb:
instead of if x & lb:
.
Why it happens: Confusion about what we're checking - we need to test if a specific bit position is set, not compare values or XOR them.
Solution: Use x & lb
which correctly checks if the bit at position lb
is set in number x
. This returns non-zero (truthy) if the bit is set, and zero (falsy) if it's not.
3. Forgetting Edge Cases
While not common in this specific problem formulation, developers might forget to handle:
- Arrays with exactly 2 elements (both unique)
- Very large arrays where integer overflow might be a concern in other languages (though Python handles big integers automatically)
Solution: Test with minimal cases like [1, 2]
and ensure the algorithm still works correctly.
4. Attempting to Optimize Prematurely
Some developers try to combine steps or eliminate the second pass through the array, potentially breaking the algorithm's correctness.
Why it happens: Misunderstanding that we need two passes - one to find the XOR of all elements, and another to partition and find one unique number.
Solution: Stick to the clear two-pass approach. The algorithm is already optimal at O(n) time and O(1) space.
5. Using Wrong XOR Identity
A subtle mistake is misremembering XOR properties, such as thinking x ^ x = 1
or x ^ 0 = 0
.
Solution: Remember the fundamental XOR properties:
x ^ x = 0
(self-cancellation)x ^ 0 = x
(identity element)- XOR is commutative and associative
Which of these properties could exist for a graph but not a tree?
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!