Skip to content

Latest commit

 

History

History
663 lines (504 loc) · 19.7 KB

File metadata and controls

663 lines (504 loc) · 19.7 KB

Two Pointers Pattern

The Two Pointers pattern uses two pointers to iterate through a data structure in a single pass. This technique is particularly useful for solving problems involving sorted arrays (or linked lists) where we need to find a set of elements that satisfy certain constraints.

Difficulty Level: ★★☆☆☆ (Easy to Medium)

Prerequisite Knowledge

  • Basic array operations
  • Understanding of pointers/indices
  • Time and space complexity analysis

When to Use Two Pointers

  • Searching for pairs in a sorted array
  • Comparing strings or arrays
  • Removing duplicates
  • Finding subarrays with a specific sum
  • Palindrome problems
  • Merging two sorted arrays
  • Detecting cycles in linked lists

Types of Two Pointers Approaches

1. Opposite Direction (Left and Right)

Start with one pointer at the beginning and one at the end, then move them toward each other.

Visual Representation:

Array: [1, 2, 3, 4, 5, 6, 7, 8]
Initial state:
↓                 ↓
[1, 2, 3, 4, 5, 6, 7, 8]
left              right

After some iterations:
      ↓       ↓
[1, 2, 3, 4, 5, 6, 7, 8]
      left    right

2. Same Direction (Fast and Slow)

Both pointers start from the beginning but move at different speeds.

Visual Representation:

Array: [1, 2, 3, 4, 5, 6, 7, 8]
Initial state:
↓
[1, 2, 3, 4, 5, 6, 7, 8]
slow
fast

After some iterations:
      ↓       ↓
[1, 2, 3, 4, 5, 6, 7, 8]
      slow    fast

3. Sliding Window (Special Case)

A variation where both pointers move in the same direction, maintaining a "window" between them.

Visual Representation:

Array: [1, 2, 3, 4, 5, 6, 7, 8]
Initial state:
↓ ↓
[1, 2, 3, 4, 5, 6, 7, 8]
L R

After expanding window:
↓     ↓
[1, 2, 3, 4, 5, 6, 7, 8]
L     R

After sliding window:
  ↓     ↓
[1, 2, 3, 4, 5, 6, 7, 8]
  L     R

Common Problems and Solutions

1. Two Sum (Find a pair with target sum)

Problem: Given a sorted array of integers, find two numbers such that they add up to a specific target.

Python Solution:

def two_sum(arr, target):
    left, right = 0, len(arr) - 1
    
    while left < right:
        current_sum = arr[left] + arr[right]
        
        if current_sum == target:
            return [left, right]
        elif current_sum < target:
            left += 1
        else:
            right -= 1
            
    return [-1, -1]  # No solution found

# Example usage
arr = [1, 2, 3, 4, 6, 8, 9]
target = 10
print(two_sum(arr, target))  # Output: [1, 6] (indices of 2 and 8)

JavaScript Solution:

function twoSum(arr, target) {
    let left = 0;
    let right = arr.length - 1;
    
    while (left < right) {
        const currentSum = arr[left] + arr[right];
        
        if (currentSum === target) {
            return [left, right];
        } else if (currentSum < target) {
            left++;
        } else {
            right--;
        }
    }
    
    return [-1, -1];  // No solution found
}

// Example usage
const arr = [1, 2, 3, 4, 6, 8, 9];
const target = 10;
console.log(twoSum(arr, target));  // Output: [1, 6] (indices of 2 and 8)

Time Complexity: O(n)
Space Complexity: O(1)

2. Remove Duplicates from Sorted Array

Problem: Given a sorted array, remove the duplicates in-place such that each element appears only once and return the new length.

Python Solution:

def remove_duplicates(arr):
    if not arr:
        return 0
        
    # 'slow' pointer keeps track of the position where we need to update
    slow = 0
    
    # 'fast' pointer scans through the array
    for fast in range(1, len(arr)):
        if arr[fast] != arr[slow]:
            slow += 1
            arr[slow] = arr[fast]
            
    return slow + 1  # Length of the array with duplicates removed

# Example usage
arr = [1, 1, 2, 2, 3, 4, 4, 5]
new_length = remove_duplicates(arr)
print(arr[:new_length])  # Output: [1, 2, 3, 4, 5]

JavaScript Solution:

function removeDuplicates(arr) {
    if (arr.length === 0) {
        return 0;
    }
    
    // 'slow' pointer keeps track of the position where we need to update
    let slow = 0;
    
    // 'fast' pointer scans through the array
    for (let fast = 1; fast < arr.length; fast++) {
        if (arr[fast] !== arr[slow]) {
            slow++;
            arr[slow] = arr[fast];
        }
    }
    
    return slow + 1;  // Length of the array with duplicates removed
}

// Example usage
const arr = [1, 1, 2, 2, 3, 4, 4, 5];
const newLength = removeDuplicates(arr);
console.log(arr.slice(0, newLength));  // Output: [1, 2, 3, 4, 5]

Time Complexity: O(n)
Space Complexity: O(1)

3. Squaring a Sorted Array

Problem: Given a sorted array of integers (which can be negative), return an array of the squares of each number, also in sorted order.

Python Solution:

def sorted_squares(arr):
    n = len(arr)
    result = [0] * n
    left, right = 0, n - 1
    
    # Fill the result array from the end (largest elements first)
    for i in range(n - 1, -1, -1):
        if abs(arr[left]) > abs(arr[right]):
            result[i] = arr[left] ** 2
            left += 1
        else:
            result[i] = arr[right] ** 2
            right -= 1
            
    return result

# Example usage
arr = [-4, -1, 0, 3, 10]
print(sorted_squares(arr))  # Output: [0, 1, 9, 16, 100]

JavaScript Solution:

function sortedSquares(arr) {
    const n = arr.length;
    const result = new Array(n);
    let left = 0;
    let right = n - 1;
    
    // Fill the result array from the end (largest elements first)
    for (let i = n - 1; i >= 0; i--) {
        if (Math.abs(arr[left]) > Math.abs(arr[right])) {
            result[i] = arr[left] ** 2;
            left++;
        } else {
            result[i] = arr[right] ** 2;
            right--;
        }
    }
    
    return result;
}

// Example usage
const arr = [-4, -1, 0, 3, 10];
console.log(sortedSquares(arr));  // Output: [0, 1, 9, 16, 100]

Time Complexity: O(n)
Space Complexity: O(n)

4. 3Sum (Find triplets that sum to zero)

Problem: Given an array of integers, find all unique triplets in the array that give the sum of zero.

Python Solution:

def three_sum(arr):
    arr.sort()
    triplets = []
    n = len(arr)
    
    for i in range(n - 2):
        # Skip duplicates for the first element
        if i > 0 and arr[i] == arr[i - 1]:
            continue
            
        left, right = i + 1, n - 1
        
        while left < right:
            current_sum = arr[i] + arr[left] + arr[right]
            
            if current_sum < 0:
                left += 1
            elif current_sum > 0:
                right -= 1
            else:
                # Found a triplet
                triplets.append([arr[i], arr[left], arr[right]])
                
                # Skip duplicates for the second element
                while left < right and arr[left] == arr[left + 1]:
                    left += 1
                    
                # Skip duplicates for the third element
                while left < right and arr[right] == arr[right - 1]:
                    right -= 1
                    
                left += 1
                right -= 1
                
    return triplets

# Example usage
arr = [-1, 0, 1, 2, -1, -4]
print(three_sum(arr))  # Output: [[-1, -1, 2], [-1, 0, 1]]

JavaScript Solution:

function threeSum(arr) {
    arr.sort((a, b) => a - b);
    const triplets = [];
    const n = arr.length;
    
    for (let i = 0; i < n - 2; i++) {
        // Skip duplicates for the first element
        if (i > 0 && arr[i] === arr[i - 1]) {
            continue;
        }
        
        let left = i + 1;
        let right = n - 1;
        
        while (left < right) {
            const currentSum = arr[i] + arr[left] + arr[right];
            
            if (currentSum < 0) {
                left++;
            } else if (currentSum > 0) {
                right--;
            } else {
                // Found a triplet
                triplets.push([arr[i], arr[left], arr[right]]);
                
                // Skip duplicates for the second element
                while (left < right && arr[left] === arr[left + 1]) {
                    left++;
                }
                
                // Skip duplicates for the third element
                while (left < right && arr[right] === arr[right - 1]) {
                    right--;
                }
                
                left++;
                right--;
            }
        }
    }
    
    return triplets;
}

// Example usage
const arr = [-1, 0, 1, 2, -1, -4];
console.log(threeSum(arr));  // Output: [[-1, -1, 2], [-1, 0, 1]]

Time Complexity: O(n²)
Space Complexity: O(1) (excluding the output array)

Real-world Applications

The Two Pointers pattern is used in various real-world applications:

1. Database Query Optimization

In database systems, two-pointer techniques are used for join operations, especially merge joins on sorted data.

-- Conceptual example of a merge join in SQL
SELECT * 
FROM Table1 t1
JOIN Table2 t2 ON t1.id = t2.id
-- The database engine might use a two-pointer approach if both tables are sorted by id

2. Network Routing

In network packet processing, two-pointer approaches help in efficiently comparing packet headers and payloads.

3. Image Processing

In image processing, techniques like seam carving use two pointers to efficiently traverse and modify image data.

# Simplified seam carving algorithm
def find_vertical_seam(energy_matrix):
    height, width = len(energy_matrix), len(energy_matrix[0])
    dp = [row[:] for row in energy_matrix]
    
    # Fill the dp table
    for i in range(1, height):
        for j in range(width):
            # Use left and right pointers to find minimum energy path
            left = max(0, j - 1)
            right = min(width - 1, j + 1)
            dp[i][j] += min(dp[i-1][k] for k in range(left, right + 1))
    
    # Find the minimum energy path
    return min(range(width), key=lambda j: dp[height-1][j])

4. Text Processing

In text editors and diff tools, two-pointer techniques help in comparing and merging text files.

# Simplified diff algorithm
def find_differences(text1, text2):
    i, j = 0, 0
    differences = []
    
    while i < len(text1) and j < len(text2):
        if text1[i] == text2[j]:
            i += 1
            j += 1
        else:
            # Found a difference
            start_i, start_j = i, j
            
            # Find the next matching point
            while i < len(text1) and j < len(text2) and text1[i] != text2[j]:
                i += 1
                j += 1
                
            differences.append((start_i, i, start_j, j))
    
    return differences

Common Bugs and Debugging Tips

1. Off-by-One Errors

Problem: Incorrectly initializing or terminating pointer positions.

Example:

# Incorrect implementation
def two_sum_buggy(arr, target):
    left, right = 0, len(arr)  # Bug: right should be len(arr) - 1
    
    while left < right:
        current_sum = arr[left] + arr[right]  # Will cause IndexError
        # ...

Debugging Tips:

  • Double-check array bounds (0 to len(arr) - 1)
  • Verify loop conditions (< vs <=)
  • Test with small arrays to catch boundary issues
  • Use print statements to track pointer positions

2. Infinite Loops

Problem: Pointers not moving or moving in a way that never satisfies the termination condition.

Example:

// Incorrect implementation
function removeDuplicatesBuggy(arr) {
    let slow = 0;
    
    for (let fast = 1; fast < arr.length; fast++) {
        if (arr[fast] !== arr[slow]) {
            // Bug: forgot to increment slow
            arr[slow] = arr[fast];
        }
    }
    
    return slow + 1;  // Will always return 1
}

Debugging Tips:

  • Ensure pointers are always moving in each iteration
  • Add a safety counter to break out of potentially infinite loops during debugging
  • Trace through the algorithm with a small example

3. Incorrect Pointer Movement

Problem: Moving pointers in the wrong direction or with incorrect logic.

Example:

# Incorrect implementation
def sorted_squares_buggy(arr):
    n = len(arr)
    result = [0] * n
    left, right = 0, n - 1
    
    for i in range(n):  # Bug: should fill from the end
        if abs(arr[left]) > abs(arr[right]):
            result[i] = arr[left] ** 2
            left += 1
        else:
            result[i] = arr[right] ** 2
            right -= 1
            
    return result  # Will not be sorted correctly

Debugging Tips:

  • Visualize the algorithm with a small example
  • Use invariants to ensure the algorithm maintains its properties
  • Check if the pointers are moving in the expected direction

4. Not Handling Duplicates

Problem: Failing to handle duplicate elements correctly.

Example:

// Incorrect implementation
function threeSumBuggy(arr) {
    arr.sort((a, b) => a - b);
    const triplets = [];
    const n = arr.length;
    
    for (let i = 0; i < n - 2; i++) {
        // Bug: no duplicate handling for the first element
        let left = i + 1;
        let right = n - 1;
        
        while (left < right) {
            // ...
            // Bug: no duplicate handling for left and right pointers
        }
    }
    
    return triplets;  // Will contain duplicates
}

Debugging Tips:

  • Add explicit duplicate handling for all pointers
  • Test with inputs containing duplicates
  • Use a set or other data structure to track seen values if needed

Interactive Learning Resources

Practice Problems

Visualizers

Interactive Code Playground

Learning Path Guidance

Prerequisites

Before diving into Two Pointers problems, you should understand:

  • Basic array operations and manipulation
  • Time and space complexity analysis
  • Basic sorting algorithms
  • Simple search algorithms

Suggested Learning Sequence

  1. Start with: Simple problems like Two Sum and Remove Duplicates
  2. Then learn: Medium difficulty problems like 3Sum and Squaring a Sorted Array
  3. Next explore: Fast and slow pointer problems like Linked List Cycle
  4. Advanced topics: Multi-pointer problems and complex variations

What to Learn Next

After mastering Two Pointers, consider learning:

How to Identify Two Pointers Problems

Look for these clues in the problem statement:

  1. The input is a sorted array, linked list, or string
  2. You need to find a set of elements that fulfill certain constraints
  3. The problem asks for pairs, triplets, or subarrays
  4. The solution requires in-place operations with O(1) extra space
  5. Keywords like "find a pair," "find triplets," "subarray with sum," or "palindrome"

Tips and Tricks

  1. Always check if sorting is required: Many two-pointer problems require a sorted array. If the input isn't sorted, consider sorting it first.

  2. Handle edge cases: Always check for empty arrays, single-element arrays, or other edge cases.

  3. Avoid duplicate results: In problems like 3Sum, be careful to skip duplicate elements to avoid duplicate results.

  4. Consider bidirectional pointers: For problems involving pairs or triplets with a specific sum, using pointers from both ends is often more efficient.

  5. Use fast and slow pointers for cycle detection: This is particularly useful for linked list problems.

  6. Optimize space complexity: Two pointers often allow you to solve problems with O(1) extra space instead of using additional data structures.

Common Pitfalls

  1. Off-by-one errors: Be careful with pointer initialization and termination conditions.

  2. Infinite loops: Ensure that your pointers are always moving towards each other or in the right direction.

  3. Not handling duplicates properly: This can lead to duplicate results or incorrect answers.

  4. Forgetting to sort: Many two-pointer algorithms require the input to be sorted first.

  5. Incorrect pointer movement: Make sure you're moving the pointers in the right direction based on the problem's requirements.

Testing Strategy

Here's how to thoroughly test your Two Pointers implementations:

def test_two_sum():
    # Test basic functionality
    assert two_sum([1, 2, 3, 4, 6, 8, 9], 10) == [1, 6]  # 2 + 8 = 10
    
    # Test edge cases
    assert two_sum([1, 2], 3) == [0, 1]  # Smallest valid array
    assert two_sum([1, 2, 3], 7) == [-1, -1]  # No solution
    assert two_sum([], 5) == [-1, -1]  # Empty array
    
    # Test with negative numbers
    assert two_sum([-3, -2, -1, 0, 1, 2, 3], 0) == [2, 4]  # -1 + 1 = 0
    
    # Test with duplicates
    assert two_sum([1, 2, 3, 3, 4], 6) == [1, 4]  # 2 + 4 = 6
    
    print("All two_sum tests passed!")

def test_remove_duplicates():
    # Test basic functionality
    arr1 = [1, 1, 2, 2, 3, 4, 4, 5]
    length1 = remove_duplicates(arr1)
    assert length1 == 5
    assert arr1[:length1] == [1, 2, 3, 4, 5]
    
    # Test edge cases
    arr2 = []
    length2 = remove_duplicates(arr2)
    assert length2 == 0
    
    arr3 = [1]
    length3 = remove_duplicates(arr3)
    assert length3 == 1
    assert arr3[:length3] == [1]
    
    # Test with all duplicates
    arr4 = [1, 1, 1, 1]
    length4 = remove_duplicates(arr4)
    assert length4 == 1
    assert arr4[:length4] == [1]
    
    print("All remove_duplicates tests passed!")

# Run tests
test_two_sum()
test_remove_duplicates()

Discussion Questions

  1. How would you modify the Two Sum algorithm to return all pairs that sum to the target, not just one?
  2. What are the trade-offs between using a hash map approach versus a two-pointer approach for the Two Sum problem?
  3. How would you handle the 3Sum problem if the array contained duplicates and you needed to avoid duplicate triplets?
  4. Can you think of a problem where using three pointers would be more efficient than two?
  5. How would you adapt the two-pointer technique for linked lists where random access is not available?