540. Single Element in a Sorted Array
Problem Description
You have a sorted array where every integer appears exactly twice, except for one integer that appears only once. Your task is to find and return that single element.
The array is already sorted in ascending order. For example, in the array [1,1,2,3,3,4,4,8,8]
, the number 2
appears only once while all other numbers appear twice.
The challenge has specific performance requirements:
- Your solution must run in
O(log n)
time complexity, which means you cannot simply iterate through the array - Your solution must use
O(1)
space complexity, meaning you can only use a constant amount of extra space
The O(log n)
time requirement suggests that a binary search approach should be used rather than a linear scan through the array.
Intuition
Since we need O(log n)
time complexity, we should think about binary search. But how can binary search help us find the single element?
Let's observe a pattern in arrays with all pairs except one single element. Before the single element appears, pairs start at even indices (0, 2, 4, ...). For example, in [1,1,2,2,3,4,4]
, the pairs (1,1)
and (2,2)
start at even indices 0 and 2.
However, after the single element appears, this pattern breaks. The pairs now start at odd indices. In the same example, after the single element 3
at index 4, the pair (4,4)
starts at odd index 5.
This gives us a key insight: we can check any middle position and determine which half contains the single element by checking if the pair pattern is normal or disrupted.
For any index mid
:
- If
mid
is even, its pair should be atmid + 1
in a normal pattern - If
mid
is odd, its pair should be atmid - 1
in a normal pattern
We can elegantly handle both cases using XOR: mid ^ 1
. This operation flips the last bit, giving us mid + 1
when mid
is even, and mid - 1
when mid
is odd.
Now, if nums[mid] == nums[mid ^ 1]
, the pattern is normal up to this point, meaning the single element must be in the right half. Otherwise, if nums[mid] != nums[mid ^ 1]
, the pattern is already disrupted, so the single element is in the left half (including mid
itself).
By repeatedly narrowing down the search space this way, we can find the single element in O(log n)
time.
Learn more about Binary Search patterns.
Solution Approach
We implement binary search with left and right pointers to find the single element.
Initialize two pointers:
l = 0
(left boundary)r = len(nums) - 1
(right boundary)
The binary search continues while l < r
:
-
Calculate middle position:
mid = (l + r) >> 1
- The right shift operator
>> 1
is equivalent to dividing by 2
- The right shift operator
-
Check pair pattern at middle position: Compare
nums[mid]
withnums[mid ^ 1]
- The XOR operation
mid ^ 1
gives us:mid + 1
ifmid
is even (flips 0 to 1 in the last bit)mid - 1
ifmid
is odd (flips 1 to 0 in the last bit)
- This elegantly handles both cases in one expression
- The XOR operation
-
Narrow down the search space:
- If
nums[mid] != nums[mid ^ 1]
: The pair pattern is already broken atmid
, meaning the single element is atmid
or to its left. Setr = mid
to search in[l, mid]
- If
nums[mid] == nums[mid ^ 1]
: The pair pattern is normal up tomid
, meaning the single element must be to the right. Setl = mid + 1
to search in[mid + 1, r]
- If
-
Return the result: When the loop ends with
l == r
, we've found the position of the single element. Returnnums[l]
The algorithm works because it maintains an invariant: the single element is always within the range [l, r]
. By checking the pair pattern at the middle position, we can determine which half contains the single element and eliminate the other half, achieving O(log n)
time complexity with O(1)
space.
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,1,2,3,3,4,4]
:
Initial state:
- Array:
[1,1,2,3,3,4,4]
(indices 0-6) l = 0
,r = 6
Iteration 1:
mid = (0 + 6) >> 1 = 3
- Check pair pattern:
nums[3] = 3
,mid ^ 1 = 3 ^ 1 = 2
- Compare
nums[3]
withnums[2]
:3 != 2
- Pattern is broken at mid, so single element is at mid or left
- Update:
r = mid = 3
- Search space:
[1,1,2,3]
(indices 0-3)
Iteration 2:
l = 0
,r = 3
mid = (0 + 3) >> 1 = 1
- Check pair pattern:
nums[1] = 1
,mid ^ 1 = 1 ^ 1 = 0
- Compare
nums[1]
withnums[0]
:1 == 1
- Pattern is normal, so single element is to the right
- Update:
l = mid + 1 = 2
- Search space:
[2,3]
(indices 2-3)
Iteration 3:
l = 2
,r = 3
mid = (2 + 3) >> 1 = 2
- Check pair pattern:
nums[2] = 2
,mid ^ 1 = 2 ^ 1 = 3
- Compare
nums[2]
withnums[3]
:2 != 3
- Pattern is broken at mid, so single element is at mid or left
- Update:
r = mid = 2
- Search space:
[2]
(index 2)
Final state:
l = 2
,r = 2
(loop exits asl == r
)- Return
nums[2] = 2
The algorithm correctly identifies 2
as the single element. Notice how the XOR operation elegantly handles checking the correct pair partner: when mid
is even (like 2), it checks the next element; when mid
is odd (like 1 or 3), it checks the previous element.
Solution Implementation
1class Solution:
2 def singleNonDuplicate(self, nums: List[int]) -> int:
3 """
4 Find the single element that appears only once in a sorted array
5 where every other element appears exactly twice.
6
7 Uses binary search with XOR trick to determine search direction.
8 Time Complexity: O(log n)
9 Space Complexity: O(1)
10 """
11 # Initialize binary search boundaries
12 left, right = 0, len(nums) - 1
13
14 while left < right:
15 # Calculate middle index
16 mid = (left + right) // 2
17
18 # XOR with 1 flips the last bit:
19 # - If mid is even, mid ^ 1 = mid + 1 (checks next element)
20 # - If mid is odd, mid ^ 1 = mid - 1 (checks previous element)
21 # This helps us check if mid is part of a valid pair
22
23 if nums[mid] != nums[mid ^ 1]:
24 # If mid doesn't form a pair with its expected partner,
25 # the single element is at mid or to the left of mid
26 right = mid
27 else:
28 # If mid forms a valid pair with its partner,
29 # the single element must be to the right
30 left = mid + 1
31
32 # When left == right, we've found the single element
33 return nums[left]
34
1class Solution {
2 public int singleNonDuplicate(int[] nums) {
3 // Initialize binary search boundaries
4 int left = 0;
5 int right = nums.length - 1;
6
7 // Binary search to find the single element
8 while (left < right) {
9 // Calculate middle index
10 int mid = (left + right) >> 1;
11
12 // XOR with 1 toggles the last bit:
13 // - If mid is even, mid ^ 1 = mid + 1 (checks next element)
14 // - If mid is odd, mid ^ 1 = mid - 1 (checks previous element)
15 // In a valid pair, elements at even and odd indices should match
16 if (nums[mid] != nums[mid ^ 1]) {
17 // Single element is in the left half (including mid)
18 right = mid;
19 } else {
20 // Single element is in the right half (excluding mid)
21 left = mid + 1;
22 }
23 }
24
25 // Return the single non-duplicate element
26 return nums[left];
27 }
28}
29
1class Solution {
2public:
3 int singleNonDuplicate(vector<int>& nums) {
4 // Initialize binary search boundaries
5 // left points to the start, right points to the end of array
6 int left = 0, right = nums.size() - 1;
7
8 // Binary search to find the single element
9 while (left < right) {
10 // Calculate middle index using bit shift (equivalent to dividing by 2)
11 int mid = (left + right) >> 1;
12
13 // Key insight: In a sorted array where all elements appear twice except one,
14 // pairs should appear at indices (0,1), (2,3), (4,5), etc.
15 // For even index i, its pair should be at i+1
16 // For odd index i, its pair should be at i-1
17 // XOR with 1 flips the last bit: even->odd, odd->even
18 // This gives us the expected pair index
19
20 // Check if mid element is different from its expected pair
21 if (nums[mid] != nums[mid ^ 1]) {
22 // The single element is at mid or before mid
23 // Move right boundary to mid
24 right = mid;
25 } else {
26 // The pair is intact, single element must be after mid
27 // Move left boundary past mid
28 left = mid + 1;
29 }
30 }
31
32 // When left == right, we've found the single element
33 return nums[left];
34 }
35};
36
1/**
2 * Finds the single element that appears only once in a sorted array where every other element appears exactly twice.
3 * Uses binary search with XOR bit manipulation to efficiently locate the single element.
4 *
5 * @param nums - Sorted array where every element appears twice except for one
6 * @returns The single non-duplicate element
7 */
8function singleNonDuplicate(nums: number[]): number {
9 // Initialize binary search boundaries
10 let left: number = 0;
11 let right: number = nums.length - 1;
12
13 // Binary search to find the single element
14 while (left < right) {
15 // Calculate middle index using bit shift (equivalent to Math.floor((left + right) / 2))
16 const middle: number = (left + right) >> 1;
17
18 // XOR with 1 flips the last bit:
19 // - If middle is even, middle ^ 1 = middle + 1 (checks next element)
20 // - If middle is odd, middle ^ 1 = middle - 1 (checks previous element)
21 // This cleverly checks if middle is part of a valid pair
22 if (nums[middle] !== nums[middle ^ 1]) {
23 // Single element is in the left half (including middle)
24 right = middle;
25 } else {
26 // Single element is in the right half (excluding middle)
27 left = middle + 1;
28 }
29 }
30
31 // When left equals right, we've found the single element
32 return nums[left];
33}
34
Time and Space Complexity
The time complexity is O(log n)
, where n
is the length of the array nums
. This is because the algorithm uses binary search, which divides the search space in half with each iteration. The while loop runs at most log₂(n)
times since we're repeatedly halving the search range between l
and r
.
The space complexity is O(1)
. The algorithm only uses a constant amount of extra space for the variables l
, r
, and mid
, regardless of the input size. No additional data structures are created that scale with the input.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Pair Pattern Interpretation
The Pitfall: A common mistake is misunderstanding what the XOR operation tells us about the search direction. When nums[mid] == nums[mid ^ 1]
, developers might incorrectly think:
- "The single element must be in the left half since we found a valid pair"
- Or get confused about which direction to search based on whether
mid
is even or odd
Why This Happens: The XOR trick mid ^ 1
can be counterintuitive. When mid
is even and forms a pair with mid + 1
, it means all elements up to and including this pair are correctly paired, so the single element must be after position mid + 1
.
The Solution: Remember the invariant: before the single element appears, the first element of each pair will be at an even index (0, 2, 4...). After the single element, this pattern shifts, and the first element of each pair will be at an odd index. When we find a valid pair at mid
, we know the pattern hasn't been disrupted yet, so we search right.
2. Off-by-One Error in Binary Search Update
The Pitfall: Using left = mid
instead of left = mid + 1
when the pair is valid, or using right = mid - 1
instead of right = mid
when the pair is broken. This can cause:
- Infinite loops (when
left = mid
andleft + 1 == right
) - Missing the single element entirely
Why This Happens: Standard binary search often uses right = mid - 1
after checking the middle element. However, in this problem, when the pair is broken at mid
, the single element could BE at position mid
itself.
The Solution:
- When pair is valid: Use
left = mid + 1
because we've confirmed bothmid
andmid ^ 1
are paired correctly - When pair is broken: Use
right = mid
(notmid - 1
) becausemid
itself could be the single element
3. Edge Case with Array Length
The Pitfall: Not considering arrays with only one element, or assuming the array always has an odd length. The code might crash or return incorrect results.
Why This Happens: The problem states every element appears twice except one, implying the array length must be odd. However, edge cases like nums = [1]
might not be handled properly if we don't think about the boundary conditions.
The Solution: The current implementation handles this correctly because:
- When
nums = [1]
,left = 0
andright = 0
, so the while loop doesn't execute - The function returns
nums[left]
which isnums[0]
, the correct answer - No special case handling is needed, but it's important to verify this works
What's the output of running the following function using input [30, 20, 10, 100, 33, 12]
?
1def fun(arr: List[int]) -> List[int]:
2 import heapq
3 heapq.heapify(arr)
4 res = []
5 for i in range(3):
6 res.append(heapq.heappop(arr))
7 return res
8
1public static int[] fun(int[] arr) {
2 int[] res = new int[3];
3 PriorityQueue<Integer> heap = new PriorityQueue<>();
4 for (int i = 0; i < arr.length; i++) {
5 heap.add(arr[i]);
6 }
7 for (int i = 0; i < 3; i++) {
8 res[i] = heap.poll();
9 }
10 return res;
11}
12
1class HeapItem {
2 constructor(item, priority = item) {
3 this.item = item;
4 this.priority = priority;
5 }
6}
7
8class MinHeap {
9 constructor() {
10 this.heap = [];
11 }
12
13 push(node) {
14 // insert the new node at the end of the heap array
15 this.heap.push(node);
16 // find the correct position for the new node
17 this.bubble_up();
18 }
19
20 bubble_up() {
21 let index = this.heap.length - 1;
22
23 while (index > 0) {
24 const element = this.heap[index];
25 const parentIndex = Math.floor((index - 1) / 2);
26 const parent = this.heap[parentIndex];
27
28 if (parent.priority <= element.priority) break;
29 // if the parent is bigger than the child then swap the parent and child
30 this.heap[index] = parent;
31 this.heap[parentIndex] = element;
32 index = parentIndex;
33 }
34 }
35
36 pop() {
37 const min = this.heap[0];
38 this.heap[0] = this.heap[this.size() - 1];
39 this.heap.pop();
40 this.bubble_down();
41 return min;
42 }
43
44 bubble_down() {
45 let index = 0;
46 let min = index;
47 const n = this.heap.length;
48
49 while (index < n) {
50 const left = 2 * index + 1;
51 const right = left + 1;
52
53 if (left < n && this.heap[left].priority < this.heap[min].priority) {
54 min = left;
55 }
56 if (right < n && this.heap[right].priority < this.heap[min].priority) {
57 min = right;
58 }
59 if (min === index) break;
60 [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61 index = min;
62 }
63 }
64
65 peek() {
66 return this.heap[0];
67 }
68
69 size() {
70 return this.heap.length;
71 }
72}
73
74function fun(arr) {
75 const heap = new MinHeap();
76 for (const x of arr) {
77 heap.push(new HeapItem(x));
78 }
79 const res = [];
80 for (let i = 0; i < 3; i++) {
81 res.push(heap.pop().item);
82 }
83 return res;
84}
85
Recommended Readings
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
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
Want a Structured Path to Master System Design Too? Don’t Miss This!