Mastering the Two Pointers Technique: Count Unique Values in Sorted Arrays

Mastering the Two Pointers Technique: Count Unique Values in Sorted Arrays

The two pointers technique is one of the most elegant and efficient patterns in programming. While we often think of two pointers as starting from opposite ends of an array and moving toward each other, there’s another powerful variation where both pointers start from the same side and move in the same direction at different speeds. Today, we’ll explore this technique by solving a classic problem: counting unique values in a sorted array.

Understanding the Problem

Let’s start with a clear problem statement:

Write a function called findUniqueCount that accepts a sorted array and returns the count of unique values in that array.

Here are some examples to illustrate what we’re trying to achieve:

findUniqueCount([2, 2, 4, 6, 6, 8]) // Should return 4 (values: 2, 4, 6, 8)
findUniqueCount([1, 3, 5, 7, 9, 11, 13]) // Should return 7 (all values are unique)
findUniqueCount([]) // Should return 0 (empty array)
findUniqueCount([-3, -1, -1, 0, 2, 2, 2]) // Should return 4 (values: -3, -1, 0, 2)

The Naive Approach vs. Two Pointers

Before diving into the two pointers solution, let’s consider what a naive approach might look like:

// Naive approach - using additional space
function findUniqueCountNaive(sortedArray) {
    const uniqueSet = new Set(sortedArray);
    return uniqueSet.size;
}

While this works, it uses extra space proportional to the number of unique elements. Can we do better?

The Two Pointers Approach Explained

The beauty of the two pointers technique for this problem lies in leveraging the fact that our array is already sorted. Since identical values are grouped together, we can use two pointers moving at different speeds to identify and count unique values efficiently.

The Core Concept

Think of our two pointers as:

  • Slow pointer (left): Points to the position where we’ll place the next unique value
  • Fast pointer (right): Scouts ahead to find the next different value

Here’s the key insight: we can modify the original array in-place, building up a collection of unique values at the beginning of the array.

Step-by-Step Walkthrough

Let’s trace through an example with the array [3, 3, 5, 7, 7, 7, 9, 11, 11]:

Initial State:

Array: [3, 3, 5, 7, 7, 7, 9, 11, 11]
        ↑  ↑
      left right

Step 1: Compare values at left and right

  • arr[left] = 3, arr[right] = 3 → They’re equal, so move right forward

Step 2:

Array: [3, 3, 5, 7, 7, 7, 9, 11, 11]
        ↑     ↑
      left  right
  • arr[left] = 3, arr[right] = 5 → They’re different! Found a new unique value
  • Move left forward and place the new value: arr[++left] = arr[right]

Step 3:

Array: [3, 5, 5, 7, 7, 7, 9, 11, 11]
           ↑     ↑
         left  right

We continue this process until right reaches the end of the array.

Complete Implementation

Here’s the complete solution with detailed comments:

function findUniqueCount(sortedArray) {
    // Handle edge case: empty array
    if (sortedArray.length === 0) {
        return 0;
    }
    
    // Initialize our slow pointer
    let left = 0;
    
    // Fast pointer starts from index 1
    for (let right = 1; right < sortedArray.length; right++) {
        // If we find a different value
        if (sortedArray[left] !== sortedArray[right]) {
            // Move slow pointer forward
            left++;
            // Place the new unique value at the slow pointer position
            sortedArray[left] = sortedArray[right];
        }
        // If values are equal, we just continue (right pointer moves automatically)
    }
    
    // The count of unique values is left + 1 (since left is 0-indexed)
    return left + 1;
}

Let’s Test Our Solution

// Test cases
console.log(findUniqueCount([2, 2, 4, 6, 6, 8])); // Output: 4
console.log(findUniqueCount([1, 3, 5, 7, 9, 11, 13])); // Output: 7
console.log(findUniqueCount([])); // Output: 0
console.log(findUniqueCount([-3, -1, -1, 0, 2, 2, 2])); // Output: 4
console.log(findUniqueCount([10])); // Output: 1
console.log(findUniqueCount([5, 5, 5, 5])); // Output: 1

Alternative Implementation: Without Modifying the Original Array

If you prefer not to modify the input array, here’s a variation that only counts without storing:

function countUniqueValues(sortedArray) {
    if (sortedArray.length === 0) {
        return 0;
    }
    
    let uniqueCount = 1; // First element is always unique
    let left = 0;
    
    for (let right = 1; right < sortedArray.length; right++) {
        if (sortedArray[left] !== sortedArray[right]) {
            uniqueCount++;
            left = right; // Move left to the new unique value position
        }
    }
    
    return uniqueCount;
}

Visual Example with Detailed Tracing

Let’s trace through [8, 8, 10, 12, 12, 15] step by step:

Initial: [8, 8, 10, 12, 12, 15]
          ↑  ↑
        left right

Step 1: arr[0]=8, arr[1]=8 → Equal, move right
        [8, 8, 10, 12, 12, 15]
         ↑     ↑
       left  right

Step 2: arr[0]=8, arr[2]=10 → Different!
        left++ → left=1, arr[1]=10
        [8, 10, 10, 12, 12, 15]
            ↑      ↑
          left   right

Step 3: arr[1]=10, arr[3]=12 → Different!
        left++ → left=2, arr[2]=12
        [8, 10, 12, 12, 12, 15]
               ↑       ↑
             left    right

Step 4: arr[2]=12, arr[4]=12 → Equal, move right
        [8, 10, 12, 12, 12, 15]
               ↑           ↑
             left        right

Step 5: arr[2]=12, arr[5]=15 → Different!
        left++ → left=3, arr[3]=15
        [8, 10, 12, 15, 12, 15]
                  ↑
                left

Final result: left=3, so unique count = 3+1 = 4
Unique values stored: [8, 10, 12, 15]

Time and Space Complexity Analysis

Time Complexity: O(n)

  • We traverse the array exactly once with our fast pointer
  • Each element is examined at most twice (once by each pointer)

Space Complexity: O(1)

  • We use only a constant amount of extra space
  • The algorithm works in-place, modifying the original array

When to Use This Technique

This two pointers approach is ideal when:

  1. The array is sorted – This is crucial for the algorithm to work
  2. In-place modification is acceptable – We’re changing the original array
  3. You need optimal space complexity – No additional data structures required
  4. Linear time complexity is sufficient – Perfect for large datasets

Common Pitfalls and Edge Cases

1. Empty Array Handling

Always check for empty arrays first:

if (sortedArray.length === 0) return 0;

2. Single Element Array

Arrays with one element should return 1:

findUniqueCount([42]); // Should return 1

3. All Elements Same

findUniqueCount([7, 7, 7, 7]); // Should return 1

4. All Elements Different

findUniqueCount([1, 2, 3, 4]); // Should return 4

Practice Problems

Try solving these related problems using the two pointers technique:

  1. Remove Duplicates: Modify the array to contain only unique elements and return the new length
  2. Find Pairs with Target Sum: In a sorted array, find all pairs that sum to a target value
  3. Move Zeros: Move all zeros to the end while maintaining relative order of non-zero elements

Real-World Applications

This technique is commonly used in:

  • Data preprocessing: Cleaning datasets by removing duplicates
  • Database operations: Optimizing queries on sorted data
  • Memory management: Efficient in-place array manipulation
  • Algorithm interviews: A popular pattern in coding challenges

Conclusion

The two pointers technique for counting unique values demonstrates the power of leveraging array properties (in this case, being sorted) to create efficient algorithms. By using two pointers moving at different speeds, we achieve both optimal time complexity O(n) and space complexity O(1).

This pattern is particularly valuable because it teaches us to think creatively about pointer movement – not all two pointer problems require starting from opposite ends. Sometimes, the most elegant solution involves pointers that work together from the same starting point.

Remember: the key to mastering algorithmic patterns like this is practice and understanding the underlying principles. Once you grasp why this works, you’ll start recognizing similar opportunities in other problems.

Key Takeaways

  • Two pointers don’t always start from opposite ends
  • Sorted arrays enable powerful in-place algorithms
  • Consider modifying input arrays when it’s acceptable to do so
  • Always handle edge cases (empty arrays, single elements)
  • Time complexity: O(n), Space complexity: O(1)

Keep practicing, and you’ll find that the two pointers technique becomes a natural part of your problem-solving toolkit!