We are given a sorted array where every element appears twice except for one. We need to find that single element in O(log n) time. The constraint of O(log n) time strongly points to Binary Search.
Let’s observe the pairs and their indices. Before the single element, the pairs follow the pattern:
- First instance is at an even index.
- Second instance is at an odd index.
Example:
[1, 1, 2, 2, 3, 4, 4](Indices:0, 1, 2, 3, 4, 5, 6). For1s: indices0(even),1(odd). For2s: indices2(even),3(odd).
After the single element (which is 3 at index 4), the pattern flips:
- First instance is at an odd index.
- Second instance is at an even index.
For
4s: indices5(odd),6(even).
We can use binary search on the array indices. For any mid, if mid is even, its pair should theoretically be at mid + 1. If nums[mid] == nums[mid + 1], we know the single element is to the right (we are still in the normal even-odd pattern). If they don’t match, the single element is to the left (or is mid itself).
Using bitwise XOR (mid ^ 1) cleverly toggles between the pair components:
- If
midis even,mid ^ 1ismid + 1. - If
midis odd,mid ^ 1ismid - 1. We just need to check ifnums[mid] == nums[mid ^ 1].
// Pseudocode:
// XOR all elements in the array.
// Since every number appears twice, x ^ x = 0.
// The only remaining number will be the single element.
// Or, linearly scan checking pairs.
#include <vector>
class Solution {
public:
int singleNonDuplicate(std::vector<int>& nums) {
int res = 0;
for (int num : nums) {
res ^= num;
}
return res;
// Time: O(N), Space: O(1)
}
};// Pseudocode:
// left = 0, right = nums.size() - 2 (we want to check pairs, so we don't need to consider the last element as a starting point)
// While left <= right:
// mid = left + (right - left) / 2
// If nums[mid] == nums[mid ^ 1]:
// left = mid + 1 // We are in the left half, single element is to the right
// Else:
// right = mid - 1 // Pattern broke, single element is to the left or is mid
// Return nums[left]
#include <vector>
class Solution {
public:
int singleNonDuplicate(std::vector<int>& nums) {
int left = 0;
int right = nums.size() - 2;
while (left <= right) {
int mid = left + (right - left) / 2;
// mid ^ 1 will give the index of the pair:
// if mid is even, mid ^ 1 gives mid + 1
// if mid is odd, mid ^ 1 gives mid - 1
if (nums[mid] == nums[mid ^ 1]) {
// Pair is intact, we are in the left half
left = mid + 1;
} else {
// Pair is broken, we are in the right half (or at the single element)
right = mid - 1;
}
}
return nums[left];
}
};- Time Complexity:
O(log N)whereNis the length of the array. The binary search halves the search space each time. - Space Complexity:
O(1)as we only use pointers.
- Single element array (
[1]): The boundsleft = 0, right = -1. The loop is bypassed,nums[0]is correctly returned. - Target element at the very end (
[1, 1, 2, 2, 3]):rightis initially initialized tonums.size() - 2(index 3). The binary search will keep movingleftup tomid + 1untilleftbecomes index 4. The loop terminates, returningnums[4], which is correct. - Target element at the very beginning (
[1, 2, 2, 3, 3]): The first checknums[mid] == nums[mid^1]will fail immediately,rightwill shrink to-1,leftremains0. Returnsnums[0].
- Recognition:
O(log n)required time on a sorted array usually mandates binary search. Finding a single abnormality in pairs implies index parity mapping. - Bitwise Trick (
mid ^ 1): This is an extremely elegant trick. It pairs0-1,2-3,4-5, etc. This prevents writing messy nestedif-elseblocks to check ifmidis even or odd before deciding to checkmid-1ormid+1. - Another way is to always ensure
midis even (if mid % 2 != 0: mid -= 1), then comparenums[mid]andnums[mid+1]. It’s logically identical but slightly more verbose.