Two Pointers & Sliding Windows
A family of array idioms that collapse an obvious scan into a single pass by maintaining an invariant as indices move. We meet two pointers (converging on a sorted array, and a fast/slow pair for in-place rewriting) and the sliding window (fixed and variable size, amortized ).
╌╌╌╌
We open a new module on sequences (arrays and strings), and the first thing to learn is a way of thinking that recurs everywhere in it. A great many array questions have an obvious brute-force answer that examines every pair or every subarray: two nested loops, work. The techniques in this lesson share a single trick for collapsing that quadratic scan into a single linear pass. Instead of re-examining the array from scratch at each step, we maintain a small amount of state (one or two indices, a window) together with an invariant that this state always tells us something true. Each step advances an index and patches the invariant in , so the whole pass is .1 The art is choosing the invariant. The loop-invariant discipline from our first algorithm, establish it, maintain it, read off the answer at termination, is the reasoning we use to prove these correct.2
Two pointers, opposite ends
The cleanest instance lives on a sorted array. Suppose is sorted ascending and we want indices with for a target . Brute force tries all pairs. Instead, place one pointer at each end, and , and let them converge:
At each step we look at . If we are done. If , then paired with anything still available is too small. Since is the largest sum involving in the window and it already fell short, cannot be part of any solution, and we discard it by advancing . The case is symmetric: is too large to pair with anything remaining, so we drop it by decrementing .
The invariant has a picture worth keeping in mind. At every moment the array is split into three zones: a left zone of indices already discarded because each was proven too small to pair with anything that remained, a right zone discarded as too large, and the live window in between. The lemma says every discard is safe, so by induction the window always contains both ends of every surviving candidate pair. When the pointers meet, the window holds fewer than two indices, and we may conclude that no solution exists at all — not merely that we failed to find one. The brute-force scan needs comparisons to establish that certificate of absence; the invariant gives it in .
Here is the full run on with target :
- : . Even the smallest available partner overshoots with , so index joins no pair: .
- : . Now falls short even with the largest remaining partner: .
- : , so .
- : , so .
- : . Done.
The pointers start apart and each step closes the gap by one, so the loop runs at most times: time, space. This is Two Sum II on a sorted input, and the reason sorting first () can beat a hash table when the array is already sorted or space is tight.
The comparison with the hash-table solution is worth making precise. On an unsorted array, one pass with a map answers Two Sum in expected time: for each , look up among the elements already inserted. That beats sort-then-scan's , but it spends extra space and gives expected rather than worst-case time. When the input arrives sorted, the pointers win outright: worst case, space, no hashing. One caveat if you sort yourself: sorting scrambles the original positions, so a problem that wants indices back forces you to sort (value, index) pairs first.
Two edge cases matter. The condition is , strict, because an element cannot pair with itself; when the pointers meet, the invariant says the remaining window holds no pair, so report failure. And each miss must move exactly one pointer, the one the proof licenses. Moving when (or both pointers at once) discards indices the invariant has said nothing about, and solutions can be lost. Duplicates need no special handling for existence; to enumerate all distinct pairs, step past runs of equal values after each hit.
The same converging-pointers move solves Container With Most Water: with heights , the area between walls and is . We start at the widest pair and always advance the pointer at the shorter wall.
One pass, . Here is the full run on :
- : width , height , area . , so advance .
- : width , height , area . , so retreat .
- : width , height , area . Advance .
- : width , height , area . Advance .
- : width , height , area . Advance ; the pointers meet. Maximum: , between walls and .
The candidate areas do not climb monotonically — step 4 drops to after the maximum has already been seen. The scan promises only that the true optimum is among the candidates it evaluates, which is what the claim guarantees: no discarded wall could have anchored anything better. When , both proofs apply and either move is safe; advancing by convention keeps the code branch-free.
Two pointers, same direction (fast/slow)
A second flavor sends both pointers the same way at different speeds. The canonical use is rewriting an array in place: a write pointer trails a read pointer , and only advances when is an element we want to keep. To remove duplicates from a sorted array:
- 1
- 2for to do
- 3if then
- 4
- 5
- 6return
The read pointer scans every element once and the write pointer never overtakes it, so the routine is time and extra space; it overwrites the input rather than allocating output. Trace it on :
- . At : , a duplicate; skip.
- : , so write and set . The array now reads .
- : , so write , .
- : ; skip. Return : the prefix is the answer, and everything past it is garbage the caller ignores.
The figure below freezes the run just before the step: the earlier write already replaced with , so the kept prefix reads even though the cells to its right still hold stale values.
Two details deserve care. The pseudocode assumes (it initializes , silently keeping ); guard the empty array separately. And the filter is stable: kept elements retain their relative order, because the write pointer copies them in read order. The same skeleton solves remove element (keep everything not equal to a given ) and move zeroes (keep the nonzeros, then zero-fill from to the end) with a one-line change to the keep test. The trailing-write pattern also underlies in-place partition (the core of quicksort and quickselect), where a write pointer marks the boundary between elements already placed below the pivot and the rest; that variant swaps rather than copies, and gives up stability in exchange.
Sliding windows
A window is a contiguous range that we slide rightward across the array while keeping it consistent with some property. Two regimes appear.
Fixed size . To compute, say, every window's sum, we do not re-add elements each time. We add the entering element and subtract the leaving one: when the window advances from to , update . On with : the first window sums to ; sliding to gives ; then gives ; then gives . Recomputing each window from scratch costs additions; the incremental version pays for the first window and two operations per slide, . (For floating-point data the running sum accumulates rounding error over many slides; recompute it from scratch periodically if that matters.)
Variable size. Here the window grows and shrinks to stay feasible. The pattern: advance to expand the window greedily; whenever the window violates its constraint, advance to shrink it until the constraint holds again. The double loop looks , but it is not:
The accounting behind the lemma is worth spelling out, because the same
argument recurs across this module. Count pointer increments instead of loop
iterations. The outer loop increments exactly times. Every pass
through the inner while increments ; since never decreases and never
exceeds , the inner loop runs at most times summed over the entire
outer loop, no matter how unevenly those shrinks cluster. One outer step may
trigger five shrinks and the next ten steps none; the total is still bounded
by pointer moves, each with bookkeeping attached. Equivalently,
charge each element two coins: one spent when brings it into the window,
one when evicts it. coins pay for everything.1
Worked: smallest subarray with sum . Given positive integers and a target , find the shortest contiguous subarray whose sum is at least (Minimum Size Subarray Sum). Keep a running window sum; expand to grow it, and the moment the sum reaches , shrink from to find the tightest window ending at .
- 1
- 2for to do
- 3
- 4while do
- 5
- 6
- 7
- 8return
Because all entries are positive, the window sum is monotone in width, so once it
drops below no further shrinking helps — the while exits and moves on.
Trace it on with , watching the running sum:
- : sums , , — all below , the window just grows to .
- : sum . Record length . Shrink: drop , sum , ; below , stop.
- : sum . Record length (no improvement). Shrink: drop , sum , ; still , record length . Shrink again: drop , sum , ; stop.
- : sum . Record length (tie). Shrink: drop , sum , ; record length . Shrink: drop , sum , ; stop.
The answer is , the window . Every recorded window is the tightest one ending at its , and the true optimum ends somewhere, so the minimum over all recordings is correct.
Edge cases: if no window ever reaches , stays and we return by convention. If some single element , the shrink loop tightens the window to length the moment passes it, so the algorithm needs no special case for it.
The positivity assumption is essential. Take
and : the scan reaches with sum
, records length , shrinks once to sum , and stops — final answer
. But alone is a valid window of length . The shrink
loop quit early because dropping would have raised the sum back to ,
and the once the sum dips below , shrinking further never helps
claim is
false with negative entries. With mixed signs, use prefix sums plus a
monotonic structure (a cousin of the monotonic stack)
instead of a window.
Worked: longest substring without repeats. For Longest Substring Without
Repeating Characters, the window must contain no duplicate character. We keep a
map last[c] of the most recent index of each character. Expand ; if
was last seen at a position , that occurrence is inside the window, so we
jump to one past it. The window is always duplicate-free, and we
track its maximum length.
- 1
- 2for to do
- 3if and then
- 4
- 5
- 6
- 7return
Each character is visited once by , and only moves forward, so this is time and space for an alphabet of size .
On the string abcabcbb:
- :
a,b,care all new; the window is , . - :
awas last seen at index , so ; the window isbca. - :
blast at , so :cab. :clast at , so :abc. - :
blast at , so jumps to :cb. :blast at , so :b.
never improves past (abc), and the jump at shows
why the map beats shrinking one step at a time: moves straight past every
index that cannot start a duplicate-free window.
The guard is the step most often botched. The map is
never cleaned, so it accumulates stale entries for characters that have long
since left the window, and acting on one moves backward. On abba:
at the second b sends to ; at , a has
— that occurrence sits outside the window
and is no conflict. With the guard, the window = ba is correct.
Without it, would retreat to and window
= bba contains a
duplicate, breaking the invariant and inflating the answer.
An equivalent formulation keeps a count map and shrinks one step at a time
(while the entering character's count exceeds , decrement the count of
and advance ). It runs in the same amortized and generalizes
more smoothly to constraints like at most distinct characters
, where
there is no single index to jump to.
Choosing the tool
The techniques overlap enough that recognizing which one fits is most of the work. A checklist for the pointer-and-window family:
- Sorted input, question about a pair (sum, difference, closest to a target): converging pointers from the ends. If the input is unsorted and order does not matter, sort first () or use a hash map ( time and space).
- One-pass, in-place rewrite (dedup, filter, compact): fast/slow pointers. Stable, extra space.
- Optimize over contiguous subarrays, feasibility monotone in window width (all-positive sums, distinct-character counts): variable-size sliding window, amortized . The monotonicity is the license; check it before trusting the shrink loop.
- Sliding-window maximum or minimum fits none of the above (max is not invertible the way sums are) and needs the monotonic deque, covered in the next lesson.
- Contiguous subarrays with negative entries, or exact-sum counting, break the window's monotonicity outright. Prefix sums handle these: see the companion lesson.
Amortization, stream processing, and sweep lines
The amortized argument for the variable window — each index enters and
leaves once — is the same potential bookkeeping Tarjan formalized for data
structures (Tarjan, Amortized Computational Complexity, SIAM J. Alg. Disc.
Meth., 1985): assign each element a small constant of credit
when it enters,
spend it when it leaves, and the total spend bounds the whole run regardless of
how unevenly the work clusters. The two-coin accounting in this lesson is that
theorem in miniature.
Two-pointer and window scans also underpin stream processing. A fixed
window of width over an unbounded stream — a moving average, a rate limiter
counting events in the last seconds — reduces to the incremental add-the-
entrant, subtract-the-departer update, and it is how time-series databases and
monitoring systems (Prometheus's range queries, for instance) compute windowed
aggregates without rescanning history. The distinction the lesson draws between
invertible aggregates (sum, count — a departing element can be subtracted)
and non-invertible ones (max, min — a departing maximum cannot be
subtracted back out) is the
line between what a plain running total handles and what needs the monotonic
deque of the next lesson; the same split reappears in database window
functions, where SUM() OVER slides cheaply but MAX() OVER does not.
Finally, the converging-pointer move on a sorted array is the one-dimensional
base case of a recurring geometric pattern: 3Sum and k-Sum fix outer
indices and run the two-pointer scan inside, and the same advance the provably useless end
logic drives sweep-line algorithms in computational geometry
(Preparata & Shamos, Computational Geometry, 1985), where a pointer sweeps a
sorted event list and never backtracks.
Takeaways
- These idioms all replace a pair-or-subarray scan with a single pass by maintaining an invariant as indices advance and patching it in per step.3
- Two pointers from opposite ends solve sorted-array pair problems (Two Sum II, Container With Most Water): the move that discards an index provably discards no solution, giving time, space.
- Fast/slow pointers (a trailing write behind a read) rewrite an array in place, dedup or partition, in time and extra space.
- A sliding window expands and shrinks to keep a property; since each index enters and leaves the window once, the nested loop is amortized .
- The window needs its feasibility monotone in width — positivity for sums, a duplicate test for distinct characters. When negative entries break that monotonicity, the tool changes.
This continues in Prefix Sums & Difference Arrays, which restores range-sum queries and subarray counting when the window's positivity assumption no longer holds.
Footnotes
- Erickson, Ch. — Arrays and Amortization: the amortized argument that a two-pointer window, though nested, does total work because each index is enqueued and dequeued once. ↩ ↩2 ↩3
- CLRS, Ch. 2 — Getting Started (§2.1): the loop-invariant method (initialization, maintenance, termination) used here to prove each pointer scheme correct. ↩
- Skiena, § — Sorting & array techniques: two-pointer and windowing idioms on sorted arrays as the linear-time alternative to a quadratic scan. ↩
╌╌ END ╌╌