DSA Fundamentals
Arrays and Hashing
Stacks and Queues
Trees and Graphs
Dynamic Programming
Advanced Data Structures
Two Pointers on Sorted Arrays
Imagine you have a sorted array and need to find a pair of elements that satisfy some condition - say, two numbers that sum to a target. The brute-force approach checks every pair: pick one element, scan the rest. That is O(n^2). Can we do better?
Yes, and the key insight is monotonicity. In a sorted array, the elements increase from left to right. This creates a powerful property: if you place one pointer at the start and another at the end, the sum of the two pointed-to elements tells you exactly which pointer to move.
- If the sum is too small, the left element is too low. Moving the left pointer rightward increases the sum.
- If the sum is too large, the right element is too high. Moving the right pointer leftward decreases the sum.
- If the sum matches the target, you found your answer.
Each step eliminates at least one candidate position. Since there are n positions total, the algorithm terminates in at most n steps - O(n) time.
Two pointers converging for pair sum
Why does this work? Consider the sorted array [1, 3, 5, 7, 9] with target 10. Start with left = 0 (value 1) and right = 4 (value 9). The sum is 10 - found it. But suppose the target were 8. The sum 1 + 9 = 10 is too large, so move right to index 3 (value 7). Now 1 + 7 = 8 - found it. Each decision is forced by the sort order: moving left increases the sum, moving right decreases it. There is no ambiguity.
The two pointer technique only works when the array is sorted (or has a monotonic property). If the array is unsorted, you need a hash map instead. Always check: does the problem give you sorted input, or can you sort it first?
The general template looks like this:
This template is the foundation for every problem in this lesson. The variations come from what you compute at each step (sum, area, distance), what you do when you find a match (return immediately, collect results, skip duplicates), and how many pointers you use (two for pair problems, two plus an outer loop for triplet problems).
Why not use a hash map? For unsorted arrays, a hash map solves two-sum in O(n) time and O(n) space. But when the array is already sorted, two pointers achieve O(n) time with O(1) space. No extra data structure needed. If the problem hands you sorted input, two pointers is almost always the right approach.
The classic application of two pointers is Two Sum II: given a sorted array numbers and a target, find two numbers that add up to the target and return their 1-indexed positions. The array is guaranteed to have exactly one solution.
This is a direct application of the template from the previous section. The only twist is 1-indexed output - add 1 to both indices before returning.
Let us trace through an example. Given numbers = [2, 7, 11, 15] and target = 9:
| Step | left | right | numbers[left] | numbers[right] | Sum | Action |
| 1 | 0 | 3 | 2 | 15 | 17 | Too large, move right |
| 2 | 0 | 2 | 2 | 11 | 13 | Too large, move right |
| 3 | 0 | 1 | 2 | 7 | 9 | Match! Return [1, 2] |
Three steps to find the answer in a 4-element array. The brute force would check up to 6 pairs.
Why does the algorithm never skip the correct pair? This is the correctness argument that interviewers care about. Suppose the answer is at indices (i, j) where i \< j. At some point during execution, either left = i or right = j (or both). If left = i and right > j, then numbers[left] + numbers[right] > target (because numbers[right] >= numbers[j] and numbers[left] + numbers[j] = target). So right moves leftward toward j. Similarly, if right = j and left \< i, the sum is too small and left moves toward i. The pointers converge on the answer from both sides.
In interviews, stating the correctness argument - why the algorithm never skips the answer - is what separates a good solution from a great one. Practice explaining: if the sum is too large, every pair with the current right and a larger left is also too large, so we can safely discard the current right.
Edge cases to consider:
- Negative numbers: The array
[-3, -1, 0, 2, 4]with target 1 works identically. The sort order guarantees monotonicity regardless of sign. - Duplicate values:
[1, 1, 3, 5]with target 2 returns[1, 2]. The algorithm handles duplicates naturally because it compares sums, not values. - Minimum array size: The array
[-1, 0]with target -1 has left = 0, right = 1, sum = -1, match on the first step.
The time complexity is O(n) because each pointer moves at most n - 1 positions. The space complexity is O(1) - only two integer variables.
The 3Sum problem asks: given an array of integers, find all unique triplets that sum to zero. This is harder than two sum for two reasons - you need to find all solutions (not just one), and you must avoid duplicate triplets.
The key insight is reduction: a 3-element problem reduces to a 2-element problem. Fix one element with an outer loop, then use two pointers on the remaining subarray to find pairs that sum to the negation of the fixed element.
First, sort the array. This enables two pointers on the inner search and makes duplicate skipping straightforward.
Three sum: fix one, two-pointer the rest
Let us trace through nums = [-1, 0, 1, 2, -1, -4]:
Step 1: Sort. The array becomes [-4, -1, -1, 0, 1, 2].
Step 2: Outer loop, i = 0, nums[i] = -4. Need pairs summing to 4 in [-1, -1, 0, 1, 2]. Left = 1 (value -1), right = 5 (value 2). Sum = -1 + 2 = 1 < 4, move left. Left = 2 (value -1), sum = -1 + 2 = 1 < 4, move left. Left = 3 (value 0), sum = 0 + 2 = 2 < 4, move left. Left = 4 (value 1), sum = 1 + 2 = 3 < 4, move left. Left = 5, left meets right, no pair found.
Step 3: Outer loop, i = 1, nums[i] = -1. Need pairs summing to 1 in [-1, 0, 1, 2]. Left = 2 (value -1), right = 5 (value 2). Sum = -1 + 2 = 1 = target. Found triplet [-1, -1, 2]. Advance both pointers. Left = 3 (value 0), right = 4 (value 1). Sum = 0 + 1 = 1 = target. Found triplet [-1, 0, 1]. Advance both pointers. Left = 4, right = 3, done.
Step 4: Outer loop, i = 2, nums[i] = -1. Same as i = 1 - skip because nums[2] == nums[1].
Step 5: Outer loop, i = 3, nums[i] = 0. Need pairs summing to 0 in [1, 2]. Sum = 3 > 0, move right. Done, no pair found.
Result: [[-1, -1, 2], [-1, 0, 1]].
Handling duplicates
Duplicate avoidance happens at three points:
Duplicate skipping in three sum
- Outer loop: if
nums[i] == nums[i - 1], skip (we already processed this value as the fixed element). - Left pointer: after finding a triplet, advance left past all duplicates of
nums[left]. - Right pointer: after finding a triplet, advance right past all duplicates of
nums[right].
Time complexity: Sorting is O(n log n). The outer loop runs n times, and each inner two-pointer scan is O(n). Total: O(n^2). This is optimal - you cannot do better than O(n^2) for 3Sum (this has been proven in theoretical computer science).
Space complexity: O(1) extra space (excluding the output list), since sorting is done in-place and two pointers use constant space.
Generalizing to k-sum
The same reduction applies to 4Sum, 5Sum, or any k-Sum. Fix one element, reduce to (k-1)-Sum. The base case is 2Sum, solved with two pointers. The time complexity is O(n^(k-1)): each level of recursion adds a factor of n from its loop, and the innermost 2Sum is O(n).
The 3Sum pattern - sort, fix one, two-pointer the rest - appears in many interview problems. Whenever you see a problem asking for combinations of k elements satisfying a condition, think: can I fix elements one at a time and reduce to two pointers?
This problem uses two pointers in a different way. Instead of finding elements that sum to a target, you are maximizing an area.
Given an array height where height[i] represents a vertical line at position i, find two lines that together with the x-axis form a container that holds the most water. The area between lines at positions l and r is:
The water level is limited by the shorter line (water would spill over it), and the width is the distance between the two lines.
Container with most water
The greedy insight: Start with the widest container (left = 0, right = n - 1). At each step, move the pointer pointing to the shorter line inward. Why? Because the area is constrained by the shorter line. If you move the taller line inward, the width decreases and the height cannot increase beyond the shorter line - so the area can only decrease or stay the same. But if you move the shorter line inward, the width decreases but the height might increase enough to compensate, potentially yielding a larger area.
Let us prove this more carefully. Suppose height[l] \< height[r]. The current area is height[l] * (r - l). If we move r leftward to r - 1:
- Width decreases to
r - l - 1 - New height is
min(height[l], height[r-1]), which is at mostheight[l](sinceheight[l]is the shorter side) - New area is at most
height[l] * (r - l - 1), which is strictly less thanheight[l] * (r - l)
So keeping the shorter line and moving the taller line can never improve the area. The shorter line is the bottleneck. The only hope for improvement is replacing the shorter line with a potentially taller one.
Trace through height = [1, 8, 6, 2, 5, 4, 8, 3, 7]:
| Step | left | right | height[l] | height[r] | Width | Area | Best | Move |
| 1 | 0 | 8 | 1 | 7 | 8 | 8 | 8 | left (shorter) |
| 2 | 1 | 8 | 8 | 7 | 7 | 49 | 49 | right (shorter) |
| 3 | 1 | 7 | 8 | 3 | 6 | 18 | 49 | right (shorter) |
| 4 | 1 | 6 | 8 | 8 | 5 | 40 | 49 | right (equal, either works) |
| 5 | 1 | 5 | 8 | 4 | 4 | 16 | 49 | right (shorter) |
| 6 | 1 | 4 | 8 | 5 | 3 | 15 | 49 | right (shorter) |
| 7 | 1 | 3 | 8 | 2 | 2 | 4 | 49 | right (shorter) |
| 8 | 1 | 2 | 8 | 6 | 1 | 6 | 49 | right (shorter) |
The maximum area is 49, formed by lines at indices 1 (height 8) and 8 (height 7).
Time complexity: O(n) - each step moves one pointer, total moves bounded by n.
Space complexity: O(1) - just three variables (left, right, best).
What if both heights are equal? When height[left] == height[right], moving either pointer is valid. Neither side can improve the area with the current opposing line, so both are equally "bottlenecked." The algorithm moves one arbitrarily (in the code above, it moves right). The correctness proof still holds: the optimal pair will be found regardless of which direction you choose when heights are tied.
Practice
Loading problem...
Loading problem...
Loading problem...