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

  1. Two indices instead of two loops
  2. Pair sum in a sorted array
  3. Traced: find two values adding to 10
  4. Why it never misses the answer
  5. Same-direction pointers
  6. 3Sum: fix one, two-pointer the rest
  7. Container with most water
  8. Three-way partition (Dutch national flag)
  9. The templates side by side
  10. Practice questions (7)
  11. Make it stick
  12. 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

step 1 / 13
Set up
nums is sorted

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);
Variables
nums[1, 3, 4, 6, 8, 11]
target10
Console
(nothing printed yet)
Each step discards one value for good. At most n − 1 steps, so O(n). · interactive
nums =
1
0
3
1
l
4
2
r
6
3
8
4
11
5
↑ index (position), always starting at 0
Where the pointers stop: 4 + 6 = 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.

Two Sum without sorting

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.

JavaScript
// 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")); // false

j 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].

JavaScript
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.

JavaScript
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
Dry run · maxArea([1, 8, 6, 2, 5, 4, 8, 3, 7]) — first steps
lrheightsareabestmove
081, 71 × 8 = 88l (1 is shorter)
188, 77 × 7 = 4949r (7 is shorter)
178, 33 × 6 = 1849r
168, 88 × 5 = 4049r (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 low is 0;
  • everything after high is 2;
  • everything between low and mid is 1;
  • mid is the item we are checking now.
JavaScript
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 ]
Dry run · sortColors([2, 0, 2, 1, 1, 0])
nums[mid]ActionArray afterlowmidhigh
2swap with high[0, 0, 2, 1, 1, 2]004
0swap with low[0, 0, 2, 1, 1, 2]114
0swap with low[0, 0, 2, 1, 1, 2]224
2swap with high[0, 0, 1, 1, 2, 2]223
1move mid[0, 0, 1, 1, 2, 2]233
1move mid[0, 0, 1, 1, 2, 2]243

mid has passed high, so every item is in its place.

The templates side by side

FormStartMove ruleTypical problems
Opposite endsl = 0, r = n − 1Move the side that cannot be part of a better answerPair sum, 3Sum, container, palindrome
Same directionboth at 0 (or 1)Fast pointer every step; slow pointer only when something is kept or matchedRemove duplicates, move zeros, subsequence
Three-waylow = mid = 0, high = n − 1Swap to the front or back depending on the groupSort 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.

Q1Two Sum II (sorted input)Medium

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.

Example 1
Input: numbers = [2, 7, 11, 15], target = 9
Output: [1, 2]
Explanation: 2 + 7 = 9. Positions are 1-based in this problem.
Example 2
Input: numbers = [-1, 0], target = -1
Output: [1, 2]
Explanation: −1 + 0 = −1.
Stuck? Open one hint at a time
Hint 1
The array is sorted, and the extra space must be O(1).
Hint 2
Put pointers at both ends. Move the one that cannot be in the answer.
Show all 2 solutions with explanations
Approach 1Map of seen values
The idea

For each value, check whether you have already seen the value target - value.

JavaScript
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 ]
How it works

O(n) time, but O(n) space. It also does not use the fact that the input is sorted.

Approach 2Two pointers
The idea

This is the algorithm we traced in the lesson.

JavaScript
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 ]
How it works

O(n) time, O(1) space. This is what the problem asks for.

Which approach should you use?

If a problem says “sorted” and “O(1) space”, think of two pointers. (LeetCode 167.)

Q2Reverse only the vowelsEasy

Reverse only the vowels of the string s (upper or lower case). Every other character must stay where it is.

Example 1
Input: "IceCreAm"
Output: "AceCreIm"
Explanation: The vowels I, e, e, A become A, e, e, I. Other letters stay.
Example 2
Input: "leetcode"
Output: "leotcede"
Explanation: Vowels e, e, o, e reversed are e, o, e, e.
Stuck? Open one hint at a time
Hint 1
Use pointers from both ends. Move each pointer until it sits on a vowel, then swap the two vowels.
Show the solution with explanations
Approach 1Two pointers that skip non-vowels
The idea

Work on an array of characters. Skip the consonants (letters that are not vowels) from each side. Swap each pair of vowels.

JavaScript
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")); // leotcede
How it works

O(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.

Which approach should you use?

It has the same shape as a palindrome check, plus a “skip” step. (LeetCode 345.)

Q3Is subsequenceEasy

Return whether s is a subsequence of t.

Example 1
Input: s = "abc", t = "ahbgdc"
Output: true
Explanation: a, b, c appear in t in that order.
Example 2
Input: s = "axc", t = "ahbgdc"
Output: false
Explanation: There is no x in t.
Example 3
Input: s = "", t = "abc"
Output: true
Explanation: Edge case: the empty string is a subsequence of everything.
Stuck? Open one hint at a time
Hint 1
Use one pointer in each string. Move the pointer in s only when the letters match.
Show the solution with explanations
Approach 1Same-direction pointers
The idea

Walk through t. Each time the letter in t matches the next letter that s needs, move forward in s.

JavaScript
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"));       // true
How it works

O(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.

Which approach should you use?

It is short and common. (LeetCode 392.)

Q43SumMedium

Return all unique triples [a, b, c] of values from nums (taken from three different positions) where a + b + c = 0.

Example 1
Input: [-1, 0, 1, 2, -1, -4]
Output: [[-1, -1, 2], [-1, 0, 1]]
Explanation: These are the only two different triples that add up to 0.
Example 2
Input: [0, 0, 0, 0]
Output: [[0, 0, 0]]
Explanation: Only one unique triple, even though many index choices give it.
Stuck? Open one hint at a time
Hint 1
Sort first. Fix the first value. Then find pairs in the rest that add up to its negative (for 4, look for pairs that add up to −4).
Hint 2
Skip equal neighbouring values, so you do not repeat a triple.
Show all 2 solutions with explanations
Approach 1Three loops + a Map of keys
The idea

Try every triple. Sort each triple and turn it into a string key. Store it in a Map, so repeated triples replace each other.

JavaScript
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 ] ]
How it works

O(n³). It is too slow for 3,000 values, but you can use it to check your faster answer.

Approach 2Sort + two pointers
The idea

This is the method from the lesson.

JavaScript
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 ] ]
How it works

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.

Which approach should you use?

This is one of the most asked medium questions. Practise the lines that skip duplicates until you can write them without thinking. (LeetCode 15.)

Q53Sum closestMedium

Return the sum of three values from nums that is nearest to target.

Example 1
Input: nums = [-1, 2, 1, -4], target = 1
Output: 2
Explanation: −1 + 2 + 1 = 2 is the closest possible sum to 1.
Example 2
Input: nums = [0, 0, 0], target = 1
Output: 0
Explanation: Only one triple.
Stuck? Open one hint at a time
Hint 1
The structure is the same as 3Sum. But you do not look for an exact match. Instead, keep the sum that is nearest to the target.
Show the solution with explanations
Approach 1Sort + two pointers, track the closest
The idea

Move the pointers exactly as in pair sum, towards the target. Keep the best (nearest) sum you have seen.

JavaScript
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));      // 0
How it works

O(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.

Which approach should you use?

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.)

Q6Container with most waterMedium

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.

Example 1
Input: [1, 8, 6, 2, 5, 4, 8, 3, 7]
Output: 49
Explanation: Lines at indices 1 (height 8) and 8 (height 7): 7 × 7 = 49.
Example 2
Input: [1, 1]
Output: 1
Explanation: 1 × 1.
Stuck? Open one hint at a time
Hint 1
The brute force tries every pair, which is O(n²).
Hint 2
Start with the widest pair. If you move the taller line inwards, you can never get more water. So move the shorter one.
Show all 2 solutions with explanations
Approach 1Every pair
The idea

Two loops over all pairs.

JavaScript
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])); // 49
How it works

O(n²). It is too slow for 105 lines.

Approach 2Two pointers, move the shorter line
The idea

This is the method from the lesson.

JavaScript
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]));                      // 1
How it works

O(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.

Which approach should you use?

Interviewers usually ask you to explain the pointer move. Practise saying the reason out loud. (LeetCode 11.)

Q7Boats to save peopleMedium

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.

Example 1
Input: people = [3, 2, 2, 1], limit = 3
Output: 3
Explanation: Boats: (1, 2), (2), (3).
Example 2
Input: people = [3, 5, 3, 4], limit = 5
Output: 4
Explanation: No two people fit together, so everyone needs a boat.
Stuck? Open one hint at a time
Hint 1
Each boat holds at most two people. Sort the weights.
Hint 2
Always send the heaviest person who is left. Can the lightest person who is left go in the same boat?
Show the solution with explanations
Approach 1Sort + two pointers
The idea

The heaviest person (r) always takes a boat now. Add the lightest person (l) to the same boat if the two fit together.

JavaScript
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
How it works
Dry run · sorted [1, 2, 2, 3], limit 3
l (weight)r (weight)pairfits?boats
131 + 3 = 4no → 3 goes alone1
121 + 2 = 3yes → both go2
22same personalone3

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.

Which approach should you use?

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

Make it stick — close this page, then
  1. Write the sorted pair-sum loop and explain why moving l when the sum is too small is safe.
  2. Explain how 3Sum avoids duplicate triples.
  3. Say which line moves in “container with most water”, and why.
  4. 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