Part 4 · Lesson 21 · 26 min read
Two Pointers
Two indices moving towards each other or in the same direction — pair sums, reversing, removing duplicates, 3Sum.
On this page
- Two indices instead of two loops
- Pair sum in a sorted array
- Traced: find two values adding to 10
- Why it never misses the answer
- Same-direction pointers
- 3Sum: fix one, two-pointer the rest
- Container with most water
- Three-way partition (Dutch national flag)
- The templates side by side
- Practice questions (7)
- Make it stick
- What's next
Two indices instead of two loops
Many array problems look at pairs of items. The obvious solution is a loop inside a loop, which costs O(n²) steps. The two-pointer technique uses two indices (called pointers) instead of two loops. Each pointer moves through the array only once, so the total is O(n). It comes in two forms:
- From both ends. The pointers start at the two ends and move towards each other. You usually use this on a sorted array.
- In the same direction. One pointer stays ahead of the other. This is the read/write pattern from Lesson 19.
The main skill is knowing which pointer to move. You must also be able to explain why that move cannot skip the answer.
Pair sum in a sorted array
Find two values in a sorted array (smallest first) that add up to a target. Put l (left) at the start and r (right) at the end. If the sum is too small, the only way to make it bigger is to move l to the right. If the sum is too big, move r to the left.
Traced: find two values adding to 10
Smallest on the left, largest on the right.
▶const nums = [1, 3, 4, 6, 8, 11];2const target = 10;3let l = 0, r = nums.length - 1;4while (l < r) {5 const sum = nums[l] + nums[r];6 if (sum === target) break;7 if (sum < target) l++;8 else r--;9}10console.log(l, r);| nums | [1, 3, 4, 6, 8, 11] |
| target | 10 |
Why it never misses the answer
Interviewers often ask this question. Say nums[l] + nums[r] is too small. Because the array is sorted, every other partner for nums[l] is at most nums[r]. So every pair that uses nums[l] is also too small. This means nums[l] cannot be part of the answer, and moving l loses nothing. The “too big” case is the same idea from the other side. Each move removes one value that we can prove is not needed.
Sometimes the array is not sorted and you must return the original indices. Sorting would change the positions. In that case, the Map approach from Lesson 26 is better. It takes O(n) time and needs no sorting.
Same-direction pointers
In the second form, both pointers move forward, but at different speeds. Lesson 19 used it to remove duplicates and to move zeros. Here is another classic problem. A subsequence of a string is made by taking some of its letters in the same order. The letters do not have to be next to each other. For example, “ace” is a subsequence of “abcde”. The task is to check whether one string is a subsequence of another.
// Is s a subsequence of t? (Letters of s appear in t in order, not necessarily together.)function isSubsequence(s, t) { let i = 0; // position in s for (let j = 0; j < t.length && i < s.length; j++) { if (s[i] === t[j]) i++; // matched one more letter of s } return i === s.length;}console.log(isSubsequence("ace", "abcde")); // trueconsole.log(isSubsequence("aec", "abcde")); // falsej moves at every step. i moves only when it finds a match. This takes O(n + m) steps (n and m are the two string lengths). You do not have to try every way of picking letters. The sliding windows in Lessons 22 and 23 also use pointers that move in the same direction.
3Sum: fix one, two-pointer the rest
Find all unique triples (groups of three values) that add up to 0. Three nested loops cost O(n³). Do this instead. First sort the array. Then fix the first value nums[i]. Then use the pair-sum two pointers on the rest of the array, to find two values that add up to -nums[i].
function threeSum(nums) { nums.sort((a, b) => a - b); const out = []; for (let i = 0; i < nums.length - 2; i++) { if (i > 0 && nums[i] === nums[i - 1]) continue; // same first value: skip let l = i + 1, r = nums.length - 1; while (l < r) { const sum = nums[i] + nums[l] + nums[r]; if (sum < 0) l++; else if (sum > 0) r--; else { out.push([nums[i], nums[l], nums[r]]); l++; r--; while (l < r && nums[l] === nums[l - 1]) l++; // skip repeated second values } } } return out;}console.log(threeSum([-1, 0, 1, 2, -1, -4]));// [ [ -1, -1, 2 ], [ -1, 0, 1 ] ]Sorting costs O(n log n). Then you run one O(n) two-pointer pass for each of the n first values. The total is O(n²). To avoid repeated triples, skip a value that is equal to the one you just used. Do this for i and for l. The sort makes this easy, because it puts equal values next to each other.
Container with most water
A vertical line of a different height stands at each index. Choose two lines. The water they can hold is min(height[l], height[r]) × (r − l). (The water level is set by the shorter line, and the width is the distance between the lines.) Start with the widest pair. Then move the shorter line inwards. Keeping it cannot help. Any narrower container that still uses it is limited by the same short height, and it is also narrower.
function maxArea(height) { let l = 0, r = height.length - 1, best = 0; while (l < r) { const area = Math.min(height[l], height[r]) * (r - l); best = Math.max(best, area); if (height[l] < height[r]) l++; // the shorter line limits the area: move it else r--; } return best;}console.log(maxArea([1, 8, 6, 2, 5, 4, 8, 3, 7])); // 49| l | r | heights | area | best | move |
|---|---|---|---|---|---|
| 0 | 8 | 1, 7 | 1 × 8 = 8 | 8 | l (1 is shorter) |
| 1 | 8 | 8, 7 | 7 × 7 = 49 | 49 | r (7 is shorter) |
| 1 | 7 | 8, 3 | 3 × 6 = 18 | 49 | r |
| 1 | 6 | 8, 8 | 8 × 5 = 40 | 49 | r (equal: either) |
The best container, 49, is found on the second step; the rest of the walk only confirms it.
Three-way partition (Dutch national flag)
Lesson 18 partitioned (split) an array around a pivot into two groups. With three pointers you can split an array into three groups in one pass. A common example is sorting an array that has only 0s, 1s and 2s. It is called the Dutch national flag problem because the flag has three colour bands. The meaning of the pointers:
- everything before
lowis 0; - everything after
highis 2; - everything between
lowandmidis 1; midis the item we are checking now.
function sortColors(nums) { let low = 0, mid = 0, high = nums.length - 1; while (mid <= high) { if (nums[mid] === 0) { [nums[low], nums[mid]] = [nums[mid], nums[low]]; low++; mid++; } else if (nums[mid] === 1) { mid++; } else { [nums[mid], nums[high]] = [nums[high], nums[mid]]; high--; // do not move mid: the new value is unchecked } } return nums;}console.log(sortColors([2, 0, 2, 1, 1, 0])); // [ 0, 0, 1, 1, 2, 2 ]| nums[mid] | Action | Array after | low | mid | high |
|---|---|---|---|---|---|
| 2 | swap with high | [0, 0, 2, 1, 1, 2] | 0 | 0 | 4 |
| 0 | swap with low | [0, 0, 2, 1, 1, 2] | 1 | 1 | 4 |
| 0 | swap with low | [0, 0, 2, 1, 1, 2] | 2 | 2 | 4 |
| 2 | swap with high | [0, 0, 1, 1, 2, 2] | 2 | 2 | 3 |
| 1 | move mid | [0, 0, 1, 1, 2, 2] | 2 | 3 | 3 |
| 1 | move mid | [0, 0, 1, 1, 2, 2] | 2 | 4 | 3 |
mid has passed high, so every item is in its place.
The templates side by side
| Form | Start | Move rule | Typical problems |
|---|---|---|---|
| Opposite ends | l = 0, r = n − 1 | Move the side that cannot be part of a better answer | Pair sum, 3Sum, container, palindrome |
| Same direction | both at 0 (or 1) | Fast pointer every step; slow pointer only when something is kept or matched | Remove duplicates, move zeros, subsequence |
| Three-way | low = mid = 0, high = n − 1 | Swap to the front or back depending on the group | Sort colors, partition |
Practice questions
For every two-pointer answer, be ready to say in one sentence why the pointer you move cannot be part of a better answer.
You get a sorted array and a target. Return the 1-based positions (the first item is position 1) of the two values that add up to the target. Use O(1) extra space.
Hint 1
Hint 2
Show all 2 solutions with explanations
For each value, check whether you have already seen the value target - value.
function twoSum(numbers, target) { const seen = new Map(); for (let i = 0; i < numbers.length; i++) { const j = seen.get(target - numbers[i]); if (j !== undefined) return [j + 1, i + 1]; seen.set(numbers[i], i); }}console.log(twoSum([2, 7, 11, 15], 9)); // [ 1, 2 ]O(n) time, but O(n) space. It also does not use the fact that the input is sorted.
This is the algorithm we traced in the lesson.
function twoSum(numbers, target) { let l = 0, r = numbers.length - 1; while (l < r) { const sum = numbers[l] + numbers[r]; if (sum === target) return [l + 1, r + 1]; if (sum < target) l++; else r--; }}console.log(twoSum([2, 7, 11, 15], 9)); // [ 1, 2 ]console.log(twoSum([-1, 0], -1)); // [ 1, 2 ]O(n) time, O(1) space. This is what the problem asks for.
If a problem says “sorted” and “O(1) space”, think of two pointers. (LeetCode 167.)
Reverse only the vowels of the string s (upper or lower case). Every other character must stay where it is.
Hint 1
Show the solution with explanations
Work on an array of characters. Skip the consonants (letters that are not vowels) from each side. Swap each pair of vowels.
function reverseVowels(s) { const vowels = new Set("aeiouAEIOU"); const a = [...s]; let l = 0, r = a.length - 1; while (l < r) { if (!vowels.has(a[l])) l++; else if (!vowels.has(a[r])) r--; else { [a[l], a[r]] = [a[r], a[l]]; l++; r--; } } return a.join("");}console.log(reverseVowels("IceCreAm")); // AceCreImconsole.log(reverseVowels("leetcode")); // leotcedeO(n) time. In JavaScript you cannot change a string in place (Lesson 9). So we copy the letters into an array and join them at the end.
It has the same shape as a palindrome check, plus a “skip” step. (LeetCode 345.)
Return whether s is a subsequence of t.
Hint 1
Show the solution with explanations
Walk through t. Each time the letter in t matches the next letter that s needs, move forward in s.
function isSubsequence(s, t) { let i = 0; for (let j = 0; j < t.length && i < s.length; j++) { if (s[i] === t[j]) i++; } return i === s.length;}console.log(isSubsequence("abc", "ahbgdc")); // trueconsole.log(isSubsequence("axc", "ahbgdc")); // falseconsole.log(isSubsequence("", "abc")); // trueO(n + m) time, O(1) space. It is always safe to take the first match. That leaves the most letters of t for the rest of s.
It is short and common. (LeetCode 392.)
Return all unique triples [a, b, c] of values from nums (taken from three different positions) where a + b + c = 0.
Hint 1
Hint 2
Show all 2 solutions with explanations
Try every triple. Sort each triple and turn it into a string key. Store it in a Map, so repeated triples replace each other.
function threeSum(nums) { const found = new Map(); for (let i = 0; i < nums.length; i++) for (let j = i + 1; j < nums.length; j++) for (let k = j + 1; k < nums.length; k++) if (nums[i] + nums[j] + nums[k] === 0) { const t = [nums[i], nums[j], nums[k]].sort((a, b) => a - b); found.set(t.join(","), t); } return [...found.values()];}console.log(threeSum([0, 0, 0, 0])); // [ [ 0, 0, 0 ] ]O(n³). It is too slow for 3,000 values, but you can use it to check your faster answer.
This is the method from the lesson.
function threeSum(nums) { nums.sort((a, b) => a - b); const out = []; for (let i = 0; i < nums.length - 2; i++) { if (nums[i] > 0) break; // three positives cannot sum to 0 if (i > 0 && nums[i] === nums[i - 1]) continue; let l = i + 1, r = nums.length - 1; while (l < r) { const sum = nums[i] + nums[l] + nums[r]; if (sum < 0) l++; else if (sum > 0) r--; else { out.push([nums[i], nums[l], nums[r]]); l++; r--; while (l < r && nums[l] === nums[l - 1]) l++; } } } return out;}console.log(threeSum([-1, 0, 1, 2, -1, -4])); // [ [ -1, -1, 2 ], [ -1, 0, 1 ] ]console.log(threeSum([0, 0, 0, 0])); // [ [ 0, 0, 0 ] ]O(n²) time, O(1) extra space (not counting the output and the sort). The early break is a small speed-up. It is possible only because the array is sorted.
This is one of the most asked medium questions. Practise the lines that skip duplicates until you can write them without thinking. (LeetCode 15.)
Return the sum of three values from nums that is nearest to target.
Hint 1
Show the solution with explanations
Move the pointers exactly as in pair sum, towards the target. Keep the best (nearest) sum you have seen.
function threeSumClosest(nums, target) { nums.sort((a, b) => a - b); let best = nums[0] + nums[1] + nums[2]; for (let i = 0; i < nums.length - 2; i++) { let l = i + 1, r = nums.length - 1; while (l < r) { const sum = nums[i] + nums[l] + nums[r]; if (Math.abs(sum - target) < Math.abs(best - target)) best = sum; if (sum < target) l++; else if (sum > target) r--; else return sum; // exact match: cannot do better } } return best;}console.log(threeSumClosest([-1, 2, 1, -4], 1)); // 2console.log(threeSumClosest([0, 0, 0], 1)); // 0O(n²). Start best with the sum of a real triple, not with 0 or Infinity. If you start with 0, you can return a wrong answer when every real sum is far from the target.
Many problems are small changes to a template like this one. Find the template first. Then change only the parts that are different. (LeetCode 16.)
You get the heights of vertical lines. Choose two lines that, together with the x-axis (the flat ground), hold the most water. Return that amount.
Hint 1
Hint 2
Show all 2 solutions with explanations
Two loops over all pairs.
function maxArea(h) { let best = 0; for (let i = 0; i < h.length; i++) for (let j = i + 1; j < h.length; j++) best = Math.max(best, Math.min(h[i], h[j]) * (j - i)); return best;}console.log(maxArea([1, 8, 6, 2, 5, 4, 8, 3, 7])); // 49O(n²). It is too slow for 105 lines.
This is the method from the lesson.
function maxArea(h) { let l = 0, r = h.length - 1, best = 0; while (l < r) { best = Math.max(best, Math.min(h[l], h[r]) * (r - l)); if (h[l] < h[r]) l++; else r--; } return best;}console.log(maxArea([1, 8, 6, 2, 5, 4, 8, 3, 7])); // 49console.log(maxArea([1, 1])); // 1O(n) time, O(1) space. Why it is safe: the shorter line has already been tried with the widest partner it can have. Every other container that uses it is narrower and no taller.
Interviewers usually ask you to explain the pointer move. Practise saying the reason out loud. (LeetCode 11.)
Each boat carries at most two people, and their total weight must be at most limit. Return the smallest number of boats that can carry everyone.
Hint 1
Hint 2
Show the solution with explanations
The heaviest person (r) always takes a boat now. Add the lightest person (l) to the same boat if the two fit together.
function numRescueBoats(people, limit) { people.sort((a, b) => a - b); let l = 0, r = people.length - 1, boats = 0; while (l <= r) { if (people[l] + people[r] <= limit) l++; // lightest joins the heaviest r--; // heaviest always leaves boats++; } return boats;}console.log(numRescueBoats([3, 2, 2, 1], 3)); // 3console.log(numRescueBoats([3, 5, 3, 4], 5)); // 4| l (weight) | r (weight) | pair | fits? | boats |
|---|---|---|---|---|
| 1 | 3 | 1 + 3 = 4 | no → 3 goes alone | 1 |
| 1 | 2 | 1 + 2 = 3 | yes → both go | 2 |
| 2 | 2 | same person | alone | 3 |
The loop uses l <= r, so the last single person is counted. The l and r columns show the weights that the pointers point at.
The sort costs O(n log n) and the walk costs O(n). This problem mixes two pointers with a greedy choice. A greedy choice is the best-looking choice at each step. Lesson 47 explains when such choices are safe. (LeetCode 881.)
Make it stick
- Write the sorted pair-sum loop and explain why moving l when the sum is too small is safe.
- Explain how 3Sum avoids duplicate triples.
- Say which line moves in “container with most water”, and why.
- Dry-run the Dutch national flag on [1, 2, 0].
Revisit schedule: solve two of the questions again tomorrow, again in 3 days, and once more in a week — without looking at the answers. Each time it will feel easier. That is the memory forming.
What's next
Lesson 22 turns same-direction pointers into a sliding window. A sliding window is a block of k items that moves one step at a time. Each time it moves, you update its sum or count in O(1) (one quick step) and you do not need to count it again from the start.
Practise on LeetCode
Real interview problems that use exactly what this lesson teaches. Solve them in JavaScript after the practice questions above.
Published by Jainik · Last updated · How these lessons are made
Keep learning
Part of the study guide
Related series