Skip to content
Work Practice Products Coding interviews Machine learning coding Quant developer HFT course Blog Method Why me About Book a call

Coding · Two pointers

Pointers from both ends

Lesson · 11 min read · Two pointers · Pointers from both ends

One pointer at each end, and after every comparison one of the two ends is thrown away for good. The whole skill is saying why that's safe.

What the two ends give you

The version of two pointers you'll meet first puts lo on the first element and hi on the last, and moves one of them inward on every step until they meet. There's not much code to it. What makes it correct is an argument you have to be able to say out loud, and I'd say it in the interview even if nobody asks, because it's the part they're actually marking.

The argument always has the same shape. Look at the pair (lo, hi) and decide that one of the two ends can't be part of any better answer with anything still left between them. Then that end goes and the range shrinks by one. After at most n − 1 steps there's nothing left, and you've ruled out every pair without ever looking at most of them.

Opposite ends

scan(a)

  1. Set lo = 0 and hi = n − 1.
  2. While lo < hi: look at a[lo] and a[hi] and update the answer.
  3. Decide which end cannot do better with anything inside the range, and move that pointer inward.

Where the biggest values live

Squaring a sorted array is a good first example because it doesn't look like a pair problem at all. Take [−6, −2, 0, 1, 4]. Once you square them the order breaks, since −6 becomes the biggest. But the largest square has to come from one of the two ends, because those are the values furthest from zero on each side. So we compare the two ends, write the bigger square into the last free slot of the output and move that pointer in. We fill the answer from the back, and there's no sort.

The thing to notice is that we're asking which end wins, not which pair works. Plenty of two pointer problems are like that, and the discard argument is the same one: the end that loses has nothing left to offer.

The squares of [-6, -2, 0, 1, 4] in order, filled from the back.

  1. Step 1 / 6
    nums
    -60i-21021344j
    out
    ·0·1·2·3·4
    i0j4

    i and j at the ends; the result is filled from its last slot.

  2. Step 2 / 6
    nums
    -60i-21021344j
    out
    ·0·1·2·3364
    i0j4k4

    |-6| > |4|: write -6² = 36 at out[4], move i right.

  3. Step 3 / 6
    nums
    -60-21i021344j
    out
    ·0·1·2163364
    i1j4k3

    |-2| ≤ |4|: write 4² = 16 at out[3], move j left.

  4. Step 4 / 6
    nums
    -60-21i0213j44
    out
    ·0·142163364
    i1j3k2

    |-2| > |1|: write -2² = 4 at out[2], move i right.

  5. Step 5 / 6
    nums
    -60-2102i13j44
    out
    ·01142163364
    i2j3k1

    |0| ≤ |1|: write 1² = 1 at out[1], move j left.

  6. Step 6 / 6
    nums
    -60-2102i/j1344
    out
    001142163364
    i2j2k0

    |0| ≤ |0|: write 0² = 0 at out[0], move j left.

Pairing the heaviest with the lightest

Here's one where the input isn't sorted when you get it and you sort it yourself first. Each boat holds at most two people and a weight limit, and we want the fewest boats. After sorting, look at the heaviest person still waiting. If even the lightest person can't share with them, nobody can, so they go alone. If the lightest can share, put them together, because the lightest person is the easiest one to fit with anyone else later and we lose nothing by using them up now.

TimeO(n log n)
SpaceO(1) extra

The sort costs more than the scan. If the weights are small integers a counting sort brings it down to O(n).

Boats for weights [5, 1, 4, 2, 6, 3] with a limit of 7.

  1. Step 1 / 5
    sorted
    10i2132435465j
    boats0

    Sorted: 1, 2, 3, 4, 5, 6. Limit 7.

  2. Step 2 / 5
    sorted
    10i2132435465j
    boats1

    1 + 6 = 7 ≤ 7: they share boat 1.

  3. Step 3 / 5
    sorted
    1021i324354j65
    boats2

    2 + 5 = 7 ≤ 7: they share boat 2.

  4. Step 4 / 5
    sorted
    102132i43j5465
    boats3

    3 + 4 = 7 ≤ 7: they share boat 3.

  5. Step 5 / 5
    sorted
    102132435465
    boats3

    Everyone is on a boat: 3 boats.

When the input is not sorted at all

It's easy to come away thinking two pointers needs sorted input, and that's not quite right. In the container with the most water the heights are in any order, and the scan still works, because the shorter of the two walls caps every container it could make with a wall further in and all of those are narrower too. So that's the test I'd use. If you can prove that one end has had its best chance already, you can move it, sorted or not. If you can't prove it, you probably want a hash map instead.

Rain water trapped between walls goes one step further. Each pointer carries the tallest wall it has seen so far, and the side whose maximum is lower is the side whose water level is already known. It's the same idea with a bit more state.

Problems in this module