← All Posts
DSA Series · Arrays · Two Pointers · Competition Patterns

Competition Patterns

This final post in the two pointers series covers advanced problems that combine multiple pointer techniques. These are the patterns you will encounter most often in coding interviews and competitive programming.

Prerequisites: You should be comfortable with both opposite-end pointers and same-direction pointers before reading this page.

Problem 1: Three Sum

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

Example: [-1, 0, 1, 2, -1, -4] produces [[-1, -1, 2], [-1, 0, 1]].

The Strategy: Fix One, Two-Pointer the Rest

Sort the array first. Then for each element nums[i], run the opposite-end two-pointer technique on the subarray nums[i+1..n-1] looking for pairs that sum to -nums[i].

The outer loop runs O(n) times and each two-pointer scan takes O(n), giving O(n2) total. This is a significant improvement over the O(n3) brute force.

Skip Duplicate Values

After sorting, identical values are adjacent. To avoid duplicate triplets:

  • In the outer loop, skip nums[i] if it equals nums[i-1].
  • In the inner two-pointer loop, after finding a valid triplet, skip duplicate left and right values before continuing.

Step-Through Animation: Three Sum

Sorted array: [-4, -1, -1, 0, 1, 2]. Target sum = 0. The fixed element (blue, circled) is set by the outer loop. Left (green) and Right (orange) scan the remainder.

-4 -1 -1 0 1 2 0 1 2 3 4 5 L R
Fixed=-4 (i=0), L=1, R=5. Sum=-4+(-1)+2=-3. Click Step.

C++ Solution

vector<vector<int>> threeSum(vector<int>& nums) {
    sort(nums.begin(), nums.end());
    vector<vector<int>> result;
    int n = nums.size();

    for (int i = 0; i < n - 2; i++) {
        // Skip duplicate fixed elements
        if (i > 0 && nums[i] == nums[i - 1]) continue;
        // Early termination: smallest triple too large
        if (nums[i] + nums[i + 1] + nums[i + 2] > 0) break;
        // Skip: even largest triple too small
        if (nums[i] + nums[n - 2] + nums[n - 1] < 0) continue;

        int left = i + 1, right = n - 1;
        int target = -nums[i];

        while (left < right) {
            int sum = nums[left] + nums[right];
            if (sum == target) {
                result.push_back({nums[i], nums[left], nums[right]});
                // Skip duplicates
                while (left < right && nums[left] == nums[left + 1]) left++;
                while (left < right && nums[right] == nums[right - 1]) right--;
                left++;
                right--;
            } else if (sum < target) {
                left++;
            } else {
                right--;
            }
        }
    }
    return result;
}

Time O(n2), Space O(1)

Sorting is O(n log n). The nested loop is O(n2). Output space depends on the number of triplets but is not counted as auxiliary space. This is optimal since any three-sum algorithm must examine at least O(n2) pairs in the worst case.

Four Sum Extension

The same idea extends to 4Sum: fix two elements with nested loops, then two-pointer the rest. This gives O(n3). In general, k-Sum can be solved in O(nk-1) by fixing k-2 elements and running the two-pointer scan.

// 4Sum: O(n^3)
for (int i = 0; i < n - 3; i++) {
    if (i > 0 && nums[i] == nums[i-1]) continue;
    for (int j = i + 1; j < n - 2; j++) {
        if (j > i + 1 && nums[j] == nums[j-1]) continue;
        int left = j + 1, right = n - 1;
        long long target = (long long)goal - nums[i] - nums[j];
        // ... standard two-pointer scan for target
    }
}

Problem 2: Dutch National Flag (Sort Colors)

Problem: Given an array containing only 0s, 1s, and 2s, sort it in-place in a single pass. This is LeetCode 75: Sort Colors.

Example: [2, 0, 1, 2, 0, 1] becomes [0, 0, 1, 1, 2, 2].

Three Pointers, One Pass

This problem uses three pointers, making it a natural extension of two-pointer partitioning. The idea, credited to Edsger Dijkstra:

The array is partitioned into four zones:

Three Cases for arr[mid]

  • arr[mid] == 0: Swap arr[low] and arr[mid]. Advance both low and mid.
  • arr[mid] == 1: Already in the right zone. Just advance mid.
  • arr[mid] == 2: Swap arr[mid] and arr[high]. Decrease high. Do NOT advance mid (the swapped value needs to be examined).

Step-Through Animation: Dutch National Flag

Array: [1, 2, 0, 1, 0, 2]. Three pointers: Low (green), Mid (blue), High (orange).

1 2 0 1 0 2 0 1 2 3 4 5 Low Mid High
Low=0, Mid=0, High=5. arr[Mid]=1, advance Mid. Click Step.

C++ Solution

void sortColors(vector<int>& nums) {
    int low = 0, mid = 0, high = nums.size() - 1;
    while (mid <= high) {
        if (nums[mid] == 0) {
            swap(nums[low], nums[mid]);
            low++;
            mid++;
        } else if (nums[mid] == 1) {
            mid++;
        } else { // nums[mid] == 2
            swap(nums[mid], nums[high]);
            high--;
            // do NOT advance mid
        }
    }
}
Common Mistake: When arr[mid] == 2, you must NOT advance mid after swapping. The element swapped from the high position has not been examined yet and could be a 0 that needs to go to the front.

Time O(n), Space O(1)

Each element is visited and swapped at most twice. The three pointers together cover the entire array exactly once. No extra storage is needed.

Problem 3: Trapping Rain Water

Problem: Given an array of non-negative integers representing an elevation map, compute how much water it can trap after rain.

Example: [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1] traps 6 units of water.

The Two-Pointer Approach

The key insight for trapping rain water: the water level at any position is determined by min(maxLeft, maxRight) - height[i], where maxLeft and maxRight are the tallest bars to the left and right of position i.

Instead of precomputing left-max and right-max arrays (which takes O(n) space), we can use two pointers to compute the answer in O(1) space:

Why Move the Smaller Side

If leftMax < rightMax, then the water level at the left pointer is determined by leftMax (regardless of the exact rightMax, since rightMax is at least as large). So we can safely compute the water at the left position and advance. The same logic applies when rightMax is smaller.

Step-Through Animation: Trapping Rain Water

Heights: [3, 0, 2, 0, 4]. Left pointer (green) and Right pointer (orange) track max heights from each side.

3 0 2 0 4 0 1 2 3 4 L R
L=0, R=4. leftMax=3, rightMax=4. leftMax smaller, process L. Click Step.

C++ Solution

int trap(vector<int>& height) {
    int left = 0, right = height.size() - 1;
    int leftMax = 0, rightMax = 0;
    int water = 0;

    while (left < right) {
        if (height[left] < height[right]) {
            if (height[left] >= leftMax) {
                leftMax = height[left];
            } else {
                water += leftMax - height[left];
            }
            left++;
        } else {
            if (height[right] >= rightMax) {
                rightMax = height[right];
            } else {
                water += rightMax - height[right];
            }
            right--;
        }
    }
    return water;
}
ApproachTimeSpace
Brute force (per-element max)O(n2)O(1)
Prefix max arraysO(n)O(n)
Stack-basedO(n)O(n)
Two pointersO(n)O(1)

Pattern Recognition Guide

Here is a decision framework for choosing the right two-pointer pattern in competition settings:

Step 1: Is the input sorted (or can you sort it)?

Yes: Consider opposite-end pointers for pair/sum problems, or same-direction for dedup/partition.

No, but sorting is OK: Sort first, then apply two pointers. Common for k-Sum, Two Sum variants.

No, and order must be preserved: Consider same-direction read/write pointers for in-place transformations.

Step 2: How many elements are in the answer?

2 elements (pair): Direct two-pointer scan. O(n).

3 elements (triplet): Fix one, two-pointer the rest. O(n2).

4 elements (quadruplet): Fix two, two-pointer the rest. O(n3).

Subarray/range: Sliding window (a close relative of two pointers).

Step 3: What is the constraint on space?

O(1) required: Two pointers is often the only option. Hash maps and prefix arrays are out.

O(n) allowed: Compare two-pointer with hash map approaches. Hash maps may be simpler for unsorted data.

More Competition Problems

Partition Labels (LC 763)

A string must be split into as many parts as possible such that each letter appears in at most one part. Greedy with a right-boundary pointer that expands based on the last occurrence of each character.

vector<int> partitionLabels(string s) {
    int last[26] = {};
    for (int i = 0; i < s.size(); i++)
        last[s[i] - 'a'] = i;
    vector<int> result;
    int start = 0, end = 0;
    for (int i = 0; i < s.size(); i++) {
        end = max(end, last[s[i] - 'a']);
        if (i == end) {
            result.push_back(end - start + 1);
            start = i + 1;
        }
    }
    return result;
}

Minimum Window Substring (LC 76)

Find the smallest substring of s containing all characters of t. This is a sliding window problem (same-direction pointers with a shrink/expand pattern). The left pointer shrinks the window when valid, the right pointer expands it when invalid.

Longest Mountain in Array (LC 845)

Find the longest subarray that first increases then decreases. Two-pointer: use one pointer for the start of a potential mountain, another to scan the peak and descent.

Practice Problems

ProblemDifficultyPattern
3Sum (LC 15)MediumFix one + opposite-end
3Sum Closest (LC 16)MediumFix one + opposite-end, track min diff
4Sum (LC 18)MediumFix two + opposite-end
Sort Colors (LC 75)MediumDutch National Flag (3 pointers)
Trapping Rain Water (LC 42)HardOpposite-end with max tracking
Container With Most Water (LC 11)MediumOpposite-end, move shorter side
Partition Labels (LC 763)MediumGreedy boundary expansion
Minimum Window Substring (LC 76)HardSliding window (expand/shrink)

Series Summary

Across this four-part series, we covered the full spectrum of two-pointer techniques:

The unifying principle: by maintaining two (or three) strategic positions and moving them according to problem-specific rules, you replace nested loops with linear scans. The key is always understanding which pointer to move and why.