Two Pointers, Sliding Windows & Prefix Sums
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), the sliding window (fixed and variable size, amortized ), and prefix sums, which answer any range-sum in and count subarrays summing to .
╌╌╌╌
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, a running sum) 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 exactly 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 pointers start apart and each step closes the gap by one, so the loop runs at most times: time, space. This is exactly 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 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. Moving the taller one can only shrink the width without ever raising the binding height, so it can never improve on the current best. The shorter wall, by contrast, might be replaced by a taller one. One pass, .
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. The same trailing-write pattern underlies in-place partition (the core of quicksort and quickselect): a write pointer marks the boundary between elements already placed below the pivot and the rest.
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 . Each slide is , so all windows cost total.
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:
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.
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 .
Prefix sums
The sliding window assumed we could update a sum incrementally. Prefix sums generalize this to answer any range-sum query in after a linear precompute. Define
so has length and is built in one pass: . Then the sum of any subarray telescopes:
The highlighted entries are and ; their difference is exactly . One subtraction, no rescanning.
Counting subarrays with sum . Prefix sums turn a subarray scan into for Subarray Sum Equals K. A subarray has sum iff , i.e. . So as we sweep a running prefix left to right, the number of valid left endpoints is the number of earlier prefixes equal to . Keep a hash map of prefix-value frequencies seen so far:
- 1
- 2empty prefix seen once
- 3for to do
- 4
- 50 if absent
- 6
- 7return
Seeding freq with accounts for subarrays that start at index
(those need ). One pass, time and space. Note this works for
any integers, positive or negative, unlike the sliding window, which needed
positivity for monotonicity.
Difference arrays, the dual. To apply many range updates "add to every " and only then read the array, invert the relationship. Keep a difference array and for each update do and , two touches. A single prefix-sum pass over at the end materializes the final array, since . Thus range-adds cost instead of .
Prefix sums extend to two dimensions as well: precompute , and any axis-aligned rectangle sum is recovered by inclusion–exclusion with four lookups in .
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 .
- Prefix sums answer any range sum as in , and a hash map of prefix frequencies counts subarrays with sum in , even with negative numbers.
- The difference-array dual turns range-adds into , and prefix sums generalize to 2-D rectangle queries in .
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
- 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 ╌╌