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.
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 equalsnums[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.
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:
low: boundary of the 0-region. Everything beforelowis 0.mid: current element being examined. Scans left to right.high: boundary of the 2-region. Everything afterhighis 2.
The array is partitioned into four zones:
[0, low): all 0s (red zone, finalized)[low, mid): all 1s (white zone, finalized)[mid, high]: unprocessed elements(high, n-1]: all 2s (blue zone, finalized)
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).
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
}
}
}
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:
- Maintain
leftMax(tallest bar seen from the left) andrightMax(tallest bar seen from the right). - Move the pointer on the side with the smaller max height.
- The water at the current position is guaranteed to be
currentMax - height[current]because the other side is at least as tall.
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.
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;
}
| Approach | Time | Space |
|---|---|---|
| Brute force (per-element max) | O(n2) | O(1) |
| Prefix max arrays | O(n) | O(n) |
| Stack-based | O(n) | O(n) |
| Two pointers | O(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
| Problem | Difficulty | Pattern |
|---|---|---|
| 3Sum (LC 15) | Medium | Fix one + opposite-end |
| 3Sum Closest (LC 16) | Medium | Fix one + opposite-end, track min diff |
| 4Sum (LC 18) | Medium | Fix two + opposite-end |
| Sort Colors (LC 75) | Medium | Dutch National Flag (3 pointers) |
| Trapping Rain Water (LC 42) | Hard | Opposite-end with max tracking |
| Container With Most Water (LC 11) | Medium | Opposite-end, move shorter side |
| Partition Labels (LC 763) | Medium | Greedy boundary expansion |
| Minimum Window Substring (LC 76) | Hard | Sliding window (expand/shrink) |
Series Summary
Across this four-part series, we covered the full spectrum of two-pointer techniques:
- Opposite Ends: Converge from both extremes. Best for sorted pair searches, palindromes, and container problems. O(n) time, O(1) space.
- Same Direction: Both pointers move forward. The read/write pattern handles in-place filtering and compaction. O(n) time, O(1) space.
- Fast/Slow: One pointer moves at double speed. Detects cycles and finds midpoints. O(n) time, O(1) space.
- Competition Patterns: Combine the basics into harder problems. k-Sum nests two pointers in loops. Dutch National Flag uses three pointers. Trapping rain water uses max-tracking from both sides.
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.