<style> .reveal-up, .reveal-left, .reveal-right, .reveal-stagger > * { opacity: 1 !important; transform: none !important; } </style>
  1. Learn
  2. DSA
  3. Hashing and problem-solving techniques
All lessons in this course

Lesson 21 of 50 13 minIntermediate

Two pointers

Walk two indexes through an array, from both ends or in the same direction, to solve pair, partition and in-place problems in O(n) with no extra memory.

A two-pointer algorithm keeps two indexes into the data and moves them according to simple rules. Each step moves at least one pointer, and neither ever moves back, so the whole thing is O(n). You already met one: the merge step of merge sort walks one pointer through each half.

There are two main shapes:

  • Opposite ends: one pointer starts at the left, one at the right, and they move towards each other. Used on sorted data and for reversing or mirroring.
  • Same direction (often called slow and fast, or read and write): both start on the left; one scans every item, the other moves only when something is kept.
Interactive

Two pointers

Target
10L
31
42
63
84
95
116
147R

L = left, R = right. Faded values have been ruled out.

Sorted list, target 13. Start with left at the smallest value and right at the largest.

Opposite ends: a pair with a given sum

In a sorted array, look at the smallest and the largest value. If their sum is too small, the smallest value is useless: even with the largest partner it does not reach the target, so move left forward. If the sum is too big, the largest value is useless, so move right back. Each step rules out one value for good.

PairWithSum.cs
static (int, int)? PairWithSum(int[] sorted, int target){    int left = 0, right = sorted.Length - 1;    while (left < right)    {        int sum = sorted[left] + sorted[right];        if (sum == target) return (left, right);        if (sum < target) left++;      // need a bigger sum        else right--;                  // need a smaller sum    }    return null;} int[] a = { 1, 3, 4, 6, 8, 9, 11, 14 };Console.WriteLine(PairWithSum(a, 13));Console.WriteLine(PairWithSum(a, 21)?.ToString() ?? "none");
Output
(2, 5)
none

Compare with the dictionary version of the last lesson: both are O(n) time, but two pointers needs O(1) extra memory. The price is that the data must be sorted. If it is not, sorting first costs O(n log n).

Same direction: remove repeats in place

In a sorted array, repeats sit next to each other. A read pointer visits every item. A write pointer marks where the next value to keep goes. When read finds a new value, copy it to write and advance write. Nothing is shifted, and no second array is needed.

RemoveDuplicates.cs
static int RemoveDuplicates(int[] a){    if (a.Length == 0) return 0;    int write = 1;                          // a[0] is always kept    for (int read = 1; read < a.Length; read++)        if (a[read] != a[write - 1])        // a new value            a[write++] = a[read];    return write;                           // number of values kept} int[] spans = { 2, 2, 3, 3, 3, 5, 8, 8, 9 };int n = RemoveDuplicates(spans);Console.WriteLine(n);Console.WriteLine(string.Join(", ", spans[..n]));
Output
5
2, 3, 5, 8, 9

The same read/write shape filters an array in place for any condition: “keep only beams longer than 6 m”, “move all zeros to the end”. It is how List<T>.RemoveAll works internally, which is why RemoveAll is O(n) while calling Remove in a loop is O(n²).

Opposite ends: mirroring

Comparing or swapping the first and last item, then moving inwards, reverses an array or checks if text reads the same both ways. The pointers can also skip items, here spaces and punctuation:

Palindrome.cs
static bool IsPalindrome(string s){    int i = 0, j = s.Length - 1;    while (i < j)    {        if (!char.IsLetterOrDigit(s[i])) { i++; continue; }        if (!char.IsLetterOrDigit(s[j])) { j--; continue; }        if (char.ToLowerInvariant(s[i]) != char.ToLowerInvariant(s[j])) return false;        i++; j--;    }    return true;} Console.WriteLine(IsPalindrome("Never odd or even"));Console.WriteLine(IsPalindrome("Steel beam"));
Output
True
False

When does two pointers apply?

Clue in the problemShape
Sorted array, pair or triple with a sum or differenceOpposite ends
Reverse, mirror, palindromeOpposite ends
Remove, filter or compact in placeSame direction (read / write)
Merge two sorted listsOne pointer per list
Linked list: middle node, detect a cycleSlow and fast (fast moves 2 steps)
Quick check

In PairWithSum, the sum is less than the target. Why is it safe to discard sorted[left] for ever?

Quick check

What is the extra memory used by RemoveDuplicates?

Practice

Given a sorted array that may contain negative numbers, return a new array of the squares, also sorted. For { -7, -3, 2, 3, 11 } the answer is 4, 9, 9, 49, 121. Do it in O(n), without sorting.

Show a hint

The biggest square is at one of the two ends (a big negative or a big positive). Compare the squares at left and right, write the bigger one at the end of the result, and move that pointer inwards. Fill the result from the back.

Show the solution
Solution.cs
static int[] SortedSquares(int[] a){    var result = new int[a.Length];    int left = 0, right = a.Length - 1;    for (int k = a.Length - 1; k >= 0; k--)    {        int l = a[left] * a[left], r = a[right] * a[right];        if (l > r) { result[k] = l; left++; }        else { result[k] = r; right--; }    }    return result;} Console.WriteLine(string.Join(", ", SortedSquares(new[] { -7, -3, 2, 3, 11 })));
Output
4, 9, 9, 49, 121

Remember

  • Two indexes that only move forward (or inward) give O(n) time and O(1) extra memory.
  • Opposite ends on sorted data: move the pointer that can no longer be part of an answer.
  • Read/write pointers filter or remove repeats in place; this is how RemoveAll is O(n).
  • Compared with a hash table, two pointers saves memory but usually needs sorted data.
0 of 50 lessons completed