Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Identifying Direct Application

What Makes a Problem a “Direct Application”?

The two-pointer technique can be applied directly when the problem asks you to do something with pairs of elements — one from each end — while moving both pointers inward until they meet. No clever transformation needed. The template fits as-is.

The mental checklist:

✅ We need to look at two positions simultaneously ✅ One position starts near the beginning, one near the end ✅ Both move inward with each iteration ✅ The work done at each step is simple (swap, compare, copy…)

If those four boxes are checked, you’re looking at a direct application.


The Canonical Example: Reverse an Array In-Place

Problem statement: Given an array arr, reverse it in-place. Do not create and return a new array — modify the original.

Input:  arr = [1, 2, 3, 4, 5]
Output: arr is modified to [5, 4, 3, 2, 1]
---
config:
  theme: base
  themeVariables:
    primaryColor: "#dbeafe"
    primaryBorderColor: "#3b82f6"
    primaryTextColor: "#1e3a5f"
    lineColor: "#64748b"
    secondaryColor: "#ede9fe"
    tertiaryColor: "#fef9c3"
---
block-beta
  columns 5
  A["1"] B["2"] C["3"] D["4"] E["5"]
  F["5"] G["4"] H["3"] I["2"] J["1"]

Reverse the array in-place — the original array (top row) becomes the reversed array (bottom row) without allocating new memory.


Brute Force: Two Passes + Temp Array

The naive approach copies elements in reverse into a temporary array, then copies back:

  1. Walk arr backwards and fill temp forwards
  2. Walk temp forwards and copy back into arr
---
config:
  theme: base
  themeVariables:
    primaryColor: "#dbeafe"
    primaryBorderColor: "#3b82f6"
    primaryTextColor: "#1e3a5f"
    lineColor: "#64748b"
    secondaryColor: "#ede9fe"
    tertiaryColor: "#fef9c3"
---
flowchart TB
  subgraph ORIG["Original arr"]
    direction LR
    A["1"] --- B["2"] --- C["3"] --- D["4"] --- E["5"]
  end
  subgraph TEMP["temp  (copy arr backwards)"]
    direction LR
    T0["5"] --- T1["4"] --- T2["3"] --- T3["2"] --- T4["1"]
  end
  subgraph BACK["arr  (copy temp back)"]
    direction LR
    R0["5"] --- R1["4"] --- R2["3"] --- R3["2"] --- R4["1"]
  end
  ORIG -->|"pass 1: backwards copy"| TEMP
  TEMP -->|"pass 2: forwards copy"| BACK

Brute-force reversal — two full passes and O(n) extra space for the temp array.

from typing import List

class BruteForce:
    def reverse(self, arr: List[int]) -> None:
        n = len(arr)
        temp = [0] * n

        # Pass 1: copy arr backwards into temp
        for i in range(n - 1, -1, -1):
            temp[n - 1 - i] = arr[i]

        # Pass 2: copy temp back into arr
        for i in range(n):
            arr[i] = temp[i]


arr = [1, 2, 3, 4, 5]
BruteForce().reverse(arr)
print(arr)   # [5, 4, 3, 2, 1]

This works, but it uses O(n) extra space and touches every element twice. We can do better.


Two-Pointer Solution: One Pass, Zero Extra Space

Key insight: to reverse an array, we just need to swap equidistant elements from both ends — arr[0] ↔ arr[n-1], arr[1] ↔ arr[n-2], and so on. Each swap needs exactly two positions: one from the left, one from the right. That’s the two-pointer template.

---
config:
  theme: base
  themeVariables:
    primaryColor: "#dbeafe"
    primaryBorderColor: "#3b82f6"
    primaryTextColor: "#1e3a5f"
    lineColor: "#64748b"
    secondaryColor: "#ede9fe"
    tertiaryColor: "#fef9c3"
---
flowchart TB
  subgraph Step0["Start: left=0, right=4"]
    direction LR
    A0(["left"]) --> S0A["1"] --- S0B["2"] --- S0C["3"] --- S0D["4"] --- S0E["5"]
    S0E --> B0(["right"])
  end
  subgraph Step1["Swap arr[0]↔arr[4], left=1, right=3"]
    direction LR
    A1(["left"]) --> S1A["5"] --- S1B["2"] --- S1C["3"] --- S1D["4"] --- S1E["1"]
    S1D --> B1(["right"])
  end
  subgraph Step2["Swap arr[1]↔arr[3], left=2, right=2"]
    direction LR
    A2(["left"]) --> S2A["5"] --- S2B["4"] --- S2C["3"] --- S2D["2"] --- S2E["1"]
    S2C --> B2(["right"])
  end
  subgraph Step3["left = right = 2  →  loop ends"]
    DONE(["✓  arr = [5, 4, 3, 2, 1]"])
  end

  Step0 -->|"swap + move"| Step1
  Step1 -->|"swap + move"| Step2
  Step2 -->|"left ≥ right"| Step3

Two-pointer reversal on [1, 2, 3, 4, 5] — two swaps close the gap from both ends; the middle element needs no swap.

from typing import List

class Solution:
    def reverse(self, arr: List[int]) -> None:
        left  = 0              # Start at the leftmost element
        right = len(arr) - 1   # Start at the rightmost element

        # Stop when pointers meet or cross:
        #   - Odd-length array: left == right at the middle element — it's already in place, no swap needed
        #   - Even-length array: left > right means all pairs have been swapped
        while left < right:
            # Swap the two ends — these elements are equidistant from center
            # and belong in each other's positions after reversal
            arr[left], arr[right] = arr[right], arr[left]

            left  += 1  # This position is finalized — move inward
            right -= 1  # This position is finalized — move inward
        # Invariant at exit: every pair (0, n-1), (1, n-2), ... has been swapped.
        # The array is fully reversed in-place.


arr = [1, 2, 3, 4, 5]
Solution().reverse(arr)
print(arr)   # [5, 4, 3, 2, 1]

arr2 = [1, 2, 3, 4]
Solution().reverse(arr2)
print(arr2)  # [4, 3, 2, 1]

One pass. No extra memory. The two-pointer template applied directly.

Trace — arr = [1, 2, 3, 4, 5]
arr = [1, 2, 3, 4, 5]   left = 0,  right = 4

Step 1 │ left=0 (1),  right=4 (5) │ 0 < 4 → swap │ [5, 2, 3, 4, 1] │ left=1, right=3
Step 2 │ left=1 (2),  right=3 (4) │ 1 < 3 → swap │ [5, 4, 3, 2, 1] │ left=2, right=2
Step 3 │ left=2 (3),  right=2 (3) │ 2 == 2 → left < right is false → loop exits

Result: [5, 4, 3, 2, 1] ✓

Note: The middle element (3) never needed a swap — it's equidistant from both ends
      and sits in its correct reversed position automatically.

Even-length check — arr = [1, 2, 3, 4]:
Step 1 │ left=0 (1),  right=3 (4) │ 0 < 3 → swap │ [4, 2, 3, 1] │ left=1, right=2
Step 2 │ left=1 (2),  right=2 (3) │ 1 < 2 → swap │ [4, 3, 2, 1] │ left=2, right=1
Step 3 │ left=2,      right=1     │ 2 > 1 → loop exits (pointers crossed)

Result: [4, 3, 2, 1] ✓

Note: For even-length arrays the pointers cross (left > right) rather than meet —
      all pairs are handled before that happens.

Fitting the Template

Let’s verify this problem matches all four checkboxes:

CheckpointThis Problem
Two positions at once?arr[left] and arr[right]
One near start, one near end?left=0, right=n-1
Both move inward?left++, right-- each iteration
Simple work per step?✅ One swap

Checkpoint 1 — Why “two positions at once”?

WHAT: A reversal requires swapping pairs of elements. A swap is an inherently two-element operation — you can’t swap a single element with nothing.

WHY it means two pointers fit: Every step of the algorithm needs arr[left] and arr[right] simultaneously. One pointer alone can’t do the job — it would need to “remember” the element it picked up and go look for where to put it, which is exactly what the brute force does (it stores the whole array in temp).

What breaks with a single pointer: Walk forward with one pointer and try to reverse in-place — when you overwrite arr[0] with arr[4], the original arr[0] is gone. You’d need to save it somewhere first. That “somewhere” is the temp array. Two pointers sidestep the need entirely: they swap atomically (a, b = b, a), so nothing is lost.

Rule of thumb: If the problem needs to operate on two elements that “belong together” (a pair, a palindrome check, a sum), two simultaneous pointers are the natural fit.


Checkpoint 2 — Why “one near start, one near end”?

WHAT: left starts at index 0 (the first element), right starts at index n-1 (the last element).

WHY this specific placement: After reversal, element at index 0 must land at index n-1 and vice versa. Element at index 1 must land at n-2. The pattern is: element at distance d from the left end swaps with element at distance d from the right end.

Placing one pointer at each end directly aligns with this structure — at every step, left and right are pointing at exactly the pair that needs to swap next.

What breaks with a different placement: If both pointers started from the left (like in many sliding window problems), you’d never naturally reach the element at n-1 first. You’d be doing something fundamentally different — not a reversal.

Concrete check: [1, 2, 3, 4, 5]

  • left=0, right=4: 1 and 5 are the farthest pair — swap first ✓
  • left=1, right=3: 2 and 4 are next closest pair — swap second ✓
  • left=2, right=2: single middle element — no swap needed ✓

Checkpoint 3 — Why “both move inward”?

WHAT: After each swap, both left increments by 1 and right decrements by 1.

WHY inward movement: After swapping arr[left] and arr[right], those two positions are finalized — they now hold the correct values for a reversed array. The unsolved subproblem is the inner portion: arr[left+1 .. right-1]. Moving both pointers inward shrinks the problem by 2 each step and focuses attention on exactly what’s left to solve.

Why the stop condition is left < right (not left <= right):

  • When left == right (odd-length arrays): both pointers are at the same middle element. That element is already in the correct position — swapping it with itself is a no-op. We stop before that unnecessary step.
  • When left > right (even-length arrays): the pointers have crossed, all pairs have been handled. No element remains.

What breaks if you stop at left <= right: For odd-length arrays like [1, 2, 3, 4, 5], when left = right = 2, you’d execute arr[2], arr[2] = arr[2], arr[2] — harmless but wasteful. For the stop condition to be correct, left < right is sufficient and exact.


Checkpoint 4 — Why “simple work per step”?

WHAT: At each step, we do exactly one swap: arr[left], arr[right] = arr[right], arr[left].

WHY “simple” matters: The two-pointer direct application template is powerful precisely because the loop body is O(1) — a fixed amount of work per step. Combined with O(n) steps (n/2 swaps), the total is O(n) time. The simplicity of the per-step work is what keeps the whole algorithm lean.

HOW to recognize “simple work” in other problems: The work per step should not require nested loops, searching, or recursion. It should be a direct operation on the two elements currently pointed at — compare, swap, copy, check equality. If the per-step work itself requires a loop, you may be looking at a subproblem pattern instead (section 05).

What this rules out: If you found yourself needing to scan the entire remaining array on each step, that’s O(n²) and the direct-application template no longer applies cleanly.


That’s a direct application — all four checkboxes confirmed.


Problems in This Category

The following lessons each apply the two-pointer technique in exactly this direct way. Each is a small variation on the same theme:

ProblemTwo-pointer work per step
Flip CharactersSwap characters from both ends
Palindrome CheckerCompare characters from both ends
Vowel ExchangeFind and swap vowels from both ends
Reverse WordsReverse each word’s characters with inner two pointers
Reverse SegmentsReverse a sub-range with two pointers
Reverse Word OrderReverse entire string, then reverse each word

Each is a small twist on the same pattern — same skeleton, different work in the loop body.