Facebook Pixel

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 at mid + 1 in a normal pattern
  • If mid is odd, its pair should be at mid - 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 Implementation

1class Solution:
2    def singleNonDuplicate(self, nums: List[int]) -> int:
3        """
4        Find the single element using binary search template.
5        Feasible condition: nums[mid] != nums[mid ^ 1] (pair pattern broken)
6        """
7        left, right = 0, len(nums) - 1
8        first_true_index = -1
9
10        while left <= right:
11            mid = (left + right) // 2
12
13            # XOR with 1 gives expected pair partner:
14            # even index -> checks next element
15            # odd index -> checks previous element
16            if nums[mid] != nums[mid ^ 1]:
17                # Pair pattern is broken - single element is at mid or left
18                first_true_index = mid
19                right = mid - 1
20            else:
21                # Pair pattern intact - single element is to the right
22                left = mid + 1
23
24        return nums[first_true_index]
25
1class Solution {
2    public int singleNonDuplicate(int[] nums) {
3        int left = 0;
4        int right = nums.length - 1;
5        int firstTrueIndex = -1;
6
7        while (left <= right) {
8            int mid = left + (right - left) / 2;
9
10            // XOR with 1 gives expected pair partner
11            if (nums[mid] != nums[mid ^ 1]) {
12                // Pair pattern broken - single element at mid or left
13                firstTrueIndex = mid;
14                right = mid - 1;
15            } else {
16                // Pair pattern intact - single element to the right
17                left = mid + 1;
18            }
19        }
20
21        return nums[firstTrueIndex];
22    }
23}
24
1class Solution {
2public:
3    int singleNonDuplicate(vector<int>& nums) {
4        int left = 0;
5        int right = nums.size() - 1;
6        int firstTrueIndex = -1;
7
8        while (left <= right) {
9            int mid = left + (right - left) / 2;
10
11            // XOR with 1 gives expected pair partner
12            if (nums[mid] != nums[mid ^ 1]) {
13                // Pair pattern broken - single element at mid or left
14                firstTrueIndex = mid;
15                right = mid - 1;
16            } else {
17                // Pair pattern intact - single element to the right
18                left = mid + 1;
19            }
20        }
21
22        return nums[firstTrueIndex];
23    }
24};
25
1function singleNonDuplicate(nums: number[]): number {
2    let left = 0;
3    let right = nums.length - 1;
4    let firstTrueIndex = -1;
5
6    while (left <= right) {
7        const mid = Math.floor((left + right) / 2);
8
9        // XOR with 1 gives expected pair partner
10        if (nums[mid] !== nums[mid ^ 1]) {
11            // Pair pattern broken - single element at mid or left
12            firstTrueIndex = mid;
13            right = mid - 1;
14        } else {
15            // Pair pattern intact - single element to the right
16            left = mid + 1;
17        }
18    }
19
20    return nums[firstTrueIndex];
21}
22

Solution Approach

We implement binary search using the standard template with first_true_index to find the single element.

The key insight is defining the feasible condition: at any index, we can check if the pair pattern has been disrupted (meaning the single element is at or before this index).

Feasible Condition: For any index mid, the condition nums[mid] != nums[mid ^ 1] indicates the pair pattern is broken. The XOR operation mid ^ 1 elegantly gives us the expected pair partner:

  • If mid is even, mid ^ 1 = mid + 1 (checks next element)
  • If mid is odd, mid ^ 1 = mid - 1 (checks previous element)

Binary Search Template:

Initialize:

  • left = 0, right = len(nums) - 1
  • first_true_index = -1 to track the first position where the pattern breaks

Loop while left <= right:

  1. Calculate mid = (left + right) // 2
  2. Check if feasible(mid): nums[mid] != nums[mid ^ 1]
    • If true: the single element is at mid or to its left. Store first_true_index = mid, then search left with right = mid - 1
    • If false: the pair pattern is intact, so the single element must be to the right. Search right with left = mid + 1

Return the result: After the loop, first_true_index points to the single element. Return nums[first_true_index].

The algorithm maintains the invariant that the single element is always within the search range. By finding the first index where the pair pattern breaks, we locate the single element in O(log n) time 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 Evaluator

Example Walkthrough

Let's trace through the algorithm with the array [1,1,2,3,3,4,4] using the standard binary search template:

Initial state:

  • Array: [1,1,2,3,3,4,4] (indices 0-6)
  • left = 0, right = 6, first_true_index = -1
  • Feasible condition: nums[mid] != nums[mid ^ 1] (pair pattern is broken)

Iteration 1:

  • mid = (0 + 6) // 2 = 3
  • Check feasible: nums[3] = 3, mid ^ 1 = 3 ^ 1 = 2, nums[2] = 2
  • 3 != 2 → feasible is TRUE
  • Update: first_true_index = 3, right = 2
  • Search space: indices 0-2

Iteration 2:

  • left = 0, right = 2
  • mid = (0 + 2) // 2 = 1
  • Check feasible: nums[1] = 1, mid ^ 1 = 1 ^ 1 = 0, nums[0] = 1
  • 1 == 1 → feasible is FALSE
  • Update: left = 2
  • Search space: indices 2-2

Iteration 3:

  • left = 2, right = 2
  • mid = (2 + 2) // 2 = 2
  • Check feasible: nums[2] = 2, mid ^ 1 = 2 ^ 1 = 3, nums[3] = 3
  • 2 != 3 → feasible is TRUE
  • Update: first_true_index = 2, right = 1
  • Loop ends: left > right

Final state:

  • first_true_index = 2
  • Return nums[2] = 2

The algorithm correctly identifies 2 as the single element. The first_true_index tracks the earliest position where the pair pattern breaks, which is exactly where the single element is located.

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. Using Wrong Binary Search Template Variant

The Pitfall: A common mistake is using while left < right with right = mid instead of the standard template with while left <= right and right = mid - 1.

Incorrect (non-standard template):

while left < right:
    mid = (left + right) // 2
    if nums[mid] != nums[mid ^ 1]:
        right = mid
    else:
        left = mid + 1
return nums[left]

Correct (standard template):

first_true_index = -1
while left <= right:
    mid = (left + right) // 2
    if nums[mid] != nums[mid ^ 1]:
        first_true_index = mid
        right = mid - 1
    else:
        left = mid + 1
return nums[first_true_index]

The standard template with first_true_index clearly tracks the answer and handles edge cases consistently.

2. Incorrect Pair Pattern Interpretation

The Pitfall: Misunderstanding what the XOR operation tells us about the search direction. When nums[mid] == nums[mid ^ 1], developers might incorrectly think the single element is in the left half.

The Solution: Remember: before the single element, pairs start at even indices (0, 2, 4...). After the single element, pairs start at odd indices. When we find a valid pair at mid, the pattern hasn't been disrupted yet, so we search right.

3. Edge Case with Single Element Array

The Pitfall: Not handling arrays with only one element properly.

The Solution: The standard template handles this correctly:

  • When nums = [1]: left = 0, right = 0
  • First iteration: mid = 0, check nums[0] != nums[1] - this would be out of bounds!

Important: For this problem, we need to handle the edge case where mid ^ 1 could be out of bounds:

# Safe version for single element
if len(nums) == 1:
    return nums[0]

Or ensure mid ^ 1 is always valid within the search range.

Loading...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following shows the order of node visit in a Breadth-first Search?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More