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

Understanding the Two Pointer Pattern

Why Single-Direction Traversal Isn’t Always Enough

When you work with arrays, a single loop moving left to right is your default tool. It handles most problems cleanly. But some problems have a property that a single direction completely ignores: both ends of the array matter at the same time.

Think about checking if a word is a palindrome. You need to compare the first character with the last, the second with the second-to-last — you’re always looking at two positions simultaneously, one from each end. A single forward loop forces you to either:

  • Use a second nested loop (O(n²) — very slow), or
  • Store results and do two passes (extra space)

The two-pointer technique solves this elegantly: use two variables, left and right, as indices that start at opposite ends and march toward each other in a single pass.


The Core Idea

Two pointers (left and right) start at opposite ends of the array and converge toward the middle. At each step, you do some work using both arr[left] and arr[right], then move one or both pointers inward.

---
config:
  theme: base
  themeVariables:
    primaryColor: "#dbeafe"
    primaryBorderColor: "#3b82f6"
    primaryTextColor: "#1e3a5f"
    lineColor: "#64748b"
    secondaryColor: "#ede9fe"
    tertiaryColor: "#fef9c3"
---
flowchart LR
  L(["left = 0\n→"]) -->|"starts here"| A0

  subgraph ARR["Array"]
    direction LR
    A0["arr[0]"] --- A1["arr[1]"] --- A2["arr[2]"] --- MID["···"] --- An2["arr[n-3]"] --- An1["arr[n-2]"] --- An["arr[n-1]"]
  end

  An -->|"starts here"| R(["← right = n-1"])
  MID -.->|"loop ends when\nleft ≥ right"| STOP(["✓ done"])

The two-pointer traversal — left starts at index 0 and advances right; right starts at index n−1 and retreats left. They meet in the middle.

The loop terminates when left >= right. At that point, every pair of equidistant positions has been visited exactly once — which is why the algorithm runs in O(n) time with O(1) extra space.


How the Pointers Move

Here is the full picture of a single traversal on an array of size 7:

---
config:
  theme: base
  themeVariables:
    primaryColor: "#dbeafe"
    primaryBorderColor: "#3b82f6"
    primaryTextColor: "#1e3a5f"
    lineColor: "#64748b"
    secondaryColor: "#ede9fe"
    tertiaryColor: "#fef9c3"
---
flowchart TB
  subgraph S0["Initial state"]
    direction LR
    L0(["left=0"]) --> I0["A"] --- I1["B"] --- I2["C"] --- I3["D"] --- I4["E"] --- I5["F"] --- I6["G"]
    I6 --> R0(["right=6"])
  end
  subgraph S1["After iteration 1  (A & G processed, left++, right--)"]
    direction LR
    L1(["left=1"]) --> J1["B"] --- J2["C"] --- J3["D"] --- J4["E"] --- J5["F"]
    J5 --> R1(["right=5"])
  end
  subgraph S2["After iteration 2  (B & F processed, left++, right--)"]
    direction LR
    L2(["left=2"]) --> K2["C"] --- K3["D"] --- K4["E"]
    K4 --> R2(["right=4"])
  end
  subgraph S3["After iteration 3  (C & E processed, left++, right--)"]
    direction LR
    L3(["left=3"]) --> M3["D"]
    M3 --> R3(["right=3"])
  end
  subgraph S4["left = right = 3  →  loop ends"]
    DONE(["✓ centre element D handled separately if needed"])
  end

  S0 --> S1 --> S2 --> S3 --> S4

Iteration-by-iteration view of the two-pointer traversal on a 7-element array — each step processes one pair of equidistant elements and closes the gap by one on each side.


The Generic Algorithm

The two-pointer pattern follows this skeleton for every problem that uses it directly:

Step 1. Initialise left = 0, right = n − 1 (or whatever starting positions the problem requires, as long as left < right).

Step 2. Loop while left < right:

  • Step 2.1 — do some work on arr[left] and arr[right]
  • Step 2.2 — move left forward by some number of steps (if the problem requires it)
  • Step 2.3 — move right backward by some number of steps (if the problem requires it)

Step 3. Return the result.

The specific “work” and “step size” in steps 2.1–2.3 change per problem. Everything else stays the same.


Generic Implementation

from typing import List

class Solution:
    def two_pointer(self, arr: List[int]) -> None:
        left = 0
        right = len(arr) - 1

        while left < right:
            left_val  = arr[left]
            right_val = arr[right]

            # Problem-specific work goes here
            # e.g. swap, compare, accumulate…

            # Move left forward if the problem calls for it
            if self.should_move_left(left_val, right_val):
                left += self.left_step(left_val, right_val)

            # Move right backward if the problem calls for it
            if self.should_move_right(left_val, right_val):
                right -= self.right_step(left_val, right_val)

    def should_move_left(self, lv: int, rv: int) -> bool:
        return True   # almost always True — move left every step

    def should_move_right(self, lv: int, rv: int) -> bool:
        return True   # almost always True — move right every step

    def left_step(self, lv: int, rv: int) -> int:
        return 1      # most problems step by 1

    def right_step(self, lv: int, rv: int) -> int:
        return 1      # most problems step by 1


# Try it out — replace this body with a real problem later
arr = [1, 2, 3, 4, 5, 6, 7]
Solution().two_pointer(arr)
print("Done — customise the template above to solve a real problem!")

Complexity Analysis

ComplexityReasoning
TimeO(n)left and right together visit every index exactly once. No element is processed twice.
SpaceO(1)Only two integer variables (left and right) are needed regardless of input size.

This is true for every problem that directly applies the two-pointer technique — both best and worst case are O(n) time, O(1) space.

The power of this pattern is that it reduces problems which naively need O(n²) nested loops to a single O(n) pass.


Three Ways to Apply Two Pointers

Not every two-pointer problem is identical. Problems in this pattern fall into three categories:

---
config:
  theme: base
  themeVariables:
    primaryColor: "#dbeafe"
    primaryBorderColor: "#3b82f6"
    primaryTextColor: "#1e3a5f"
    lineColor: "#64748b"
    secondaryColor: "#ede9fe"
    tertiaryColor: "#fef9c3"
---
flowchart TB
  Root["Two-Pointer Pattern Problems"]
  D["Direct Application\nTwo pointers applied as-is\n(e.g. reverse, palindrome check)"]
  R["Reduction\nProblem reduced to an\nequivalent two-pointer problem"]
  S["Subproblems\nOne step of the solution\nuses two pointers internally"]

  Root --> D
  Root --> R
  Root --> S

Three categories of two-pointer pattern problems — we start with Direct Application, which is the simplest and most common.

In the next lessons, we work through the Direct Application category in depth — problems where the two-pointer template above applies almost verbatim.