Skip to content
DSA patterns 7 min read

The Two-Pointers Pattern: Template & When to Use It

The two-pointers pattern uses two index variables that move across a sequence — either toward each other from both ends or together in the same direction — to solve in O(n) time problems that would otherwise need a nested O(n²) loop. It trades the second loop for a smarter walk, usually with O(1) extra space. This page recaps when to reach for it and gives a template you can adapt.

For the full topic treatment, see the two-pointers technique.

The signal: when to recognize it

Reach for two pointers when you see any of these in the problem statement:

  • The input is a sorted array or string and you must find a pair, triplet, or range with some property (a target sum, a difference, a closest match).
  • You must reverse, rotate, or partition in place with O(1) extra space (e.g. move zeros, remove duplicates, Dutch-national-flag).
  • You compare a sequence with itself from both ends — palindrome checks.
  • A brute-force pair scan is O(n²) and the data is sorted (or you’re allowed to sort it).

For beginners: “Two pointers” just means two int indices. The skill is the rule for moving them so that each move eliminates possibilities you’ll never need to revisit.

The two flavors

Converging pointers start at opposite ends (lo = 0, hi = n-1) and move inward. Fast/lead and slow/write pointers start together and move in the same direction, with one advancing the scan and the other marking where the next valid element goes.

Template: converging pointers

This is the canonical “pair sum on a sorted array,” the shape most converging two-pointer problems take. Adjust the comparison to fit your property.

#include <vector>
// Converging two pointers: does a sorted array contain a pair summing to target?
bool hasPairWithSum(const std::vector<int>& a, int target) {
    int lo = 0, hi = (int)a.size() - 1;
    while (lo < hi) {
        int sum = a[lo] + a[hi];
        if (sum == target) return true;
        if (sum < target) lo++;   // too small: raise the low end
        else hi--;                // too big: lower the high end
    }
    return false;
}

Template: same-direction (read/write) pointers

A write pointer marks the next slot for a “kept” element while a read pointer scans everything. This is the in-place pattern behind move zeros, remove duplicates, and partition. Here it moves all zeros to the end while preserving order.

#include <vector>
// Same-direction pointers: move all zeros to the end, in place, stable.
void moveZeros(std::vector<int>& a) {
    int write = 0;
    for (int read = 0; read < (int)a.size(); read++)
        if (a[read] != 0) a[write++] = a[read];   // keep non-zeros packed left
    while (write < (int)a.size()) a[write++] = 0;  // fill the rest with zeros
}

Complexity

Both flavors are O(n) time — each pointer makes at most n moves and never backtracks — and O(1) extra space. The converging flavor usually requires the input to be sorted first; if you have to sort it, the overall cost becomes O(n log n).

Common pitfalls

  • Infinite loops: always make sure every branch moves a pointer.
  • Off-by-one on the bound: use while (lo < hi) for pairs (you don’t want a number paired with itself), but while (lo <= hi) if a single middle element is valid.
  • Forgetting the array must be sorted for the converging-sum flavor — the move rule is only correct on sorted data.

Going deeper: The 3-sum problem fixes one index, then runs the converging template on the rest — a two-pointer loop nested inside one outer loop gives O(n²) instead of the naive O(n³).

Practice

Drill this in the searching exercises and the array exercises. Then compare it with the closely related sliding window pattern, which also walks two indices but maintains a window between them.

Last updated June 25, 2026
Was this helpful?