Pointers and Sliding Windows
Move a boundary only when its invariant makes the move safe.
On this page
Two indices are not an algorithm by themselves. They become useful when a move eliminates impossible candidates, preserves a compacted prefix or updates one contiguous range without rescanning it.
The first question is not “can I use two pointers?” It is “what lets me move one without losing a valid answer?” Sorted order and monotone window validity provide different proofs. Do not transfer a movement rule between them without checking its conditions.
The proof comes before the pointers
| Structure | What is maintained | Why progress is safe |
|---|---|---|
| Opposite ends | Eligible pair interval | Sorted values bound every partner on one side |
| Read/write | Accepted output prefix | Write never overwrites unread input |
| Fixed window | Last k items and their state | Add entering item; remove leaving item |
| Variable window | Valid or newly valid contiguous range | Discarded starts cannot improve the objective later |
| Center expansion | Matched palindrome interior | Only adjacent matching characters extend this center |
| Fast/slow | Traversal state | A separate cycle/meeting proof; not ordinary window validity |
Eliminate impossible pairs in sorted order
For sorted nondecreasing a, compare a[left]+a[right]. If the sum is below target, every eligible partner at or below right is also too small for that left value. Move left forward. If it is above target, every eligible partner at or above left is too large for that right value. Move right backward.
def has_pair_with_sum(nums, target):
left, right = 0, len(nums) - 1
while left < right:
total = nums[left] + nums[right]
if total == target: return True
if total < target: left += 1
else: right -= 1
return FalseThe function returns existence, not indices. Return the two current indices instead of True if positions are required. left<right enforces distinct elements. On unsorted input, these bounds do not hold. Sorting can make them valid but costs work, changes order and loses original indices unless those indices travel with their values. Complement hashing → avoids this sorted-order requirement.
Why this boundary may move
Sorted [1,2,4,7,11,15], target 15. Only one boundary moves when its whole row or column of possible partners is ruled out.
4 + 11 = 15. Found distinct indices 2, 4.
This frame shows the compared boundaries. The next step applies the justified move.
Exact small input, not a speed benchmark. Step order follows the algorithm; playback is optional. Blue outlines mark active input positions, index labels remain visible.
Read/write pointers preserve a prefix
Read visits each original value; write is the next free output position. After handling the read position, the prefix before write contains exactly the accepted values in their required order. For sorted duplicate removal, adjacent equal values belong to one run.
def remove_duplicates(nums):
write = 0
for read in range(len(nums)):
if write == 0 or nums[read] != nums[write - 1]:
nums[write] = nums[read]
write += 1
return writeWriting at or behind read protects unread data. The function handles empty input and returns a logical length; it does not promise that the original container shrinks. On unsorted data it removes only adjacent repetitions, not every repeated value.
Fast/slow pointers solve a different family: linked-list cycle detection or a processed boundary. Relative motion on a cycle provides the meeting argument. Do not assume that moving at two speeds solves an arbitrary array relation.
Centers, merges and a fixed outer choice
A palindrome grows around either one character (odd length) or the gap between two characters (even length). Expand while both indices are in bounds and characters match. On stopping, [left+1,right) is the valid palindrome. Trying all centers takes O(n²) time in the worst case, not one globally linear scan.
Expand one odd or even palindrome center
def expand(s, left, right):
while left >= 0 and right < len(s) and s[left] == s[right]:
left -= 1
right += 1
return s[left + 1:right]Call with a valid odd center (i,i) or even center (i,i+1). The even center at the final position has right equal to the string length and returns an empty string. Python strings are indexed by Unicode code point; this does not define grapheme-cluster or normalized palindrome equality. Each returned slice allocates its characters.
To merge two ordered inputs, compare their current heads and consume the smaller eligible value. Each item is consumed once; output size matters. For a three-value sum, fix one sorted position and apply opposite-end reasoning to its suffix. Repeating a linear suffix scan gives O(n²) overall.
When producing unique value triples, record a valid triple before skipping equal runs. When producing index triples, skipping equal values may discard required outputs. State what makes two answers distinct.
The shorter-boundary move in maximum container area uses another bound: reducing width cannot help while keeping the current shorter height. Trapping water uses known boundary maxima. Neither follows solely from the sum comparison rule; a superficially similar pair of pointers can require a different invariant.
Reuse a fixed contiguous range
A fixed-size window is a contiguous range, not an arbitrary subsequence. For sums, add the new right value and remove the item falling out of the left boundary. Record only once k items exist. Validate or define an explicit no-window result.
def max_sum_of_size_k(nums, k):
if type(k) is not int or not 1 <= k <= len(nums):
raise ValueError('window size outside input')
total, best = 0, float('-inf')
for right, value in enumerate(nums):
total += value
if right >= k:
total -= nums[right - k]
if right >= k - 1:
best = max(best, total)
return bestThis implementation rejects an invalid window size rather than returning a numeric sentinel for no window. After adjustment during startup, the state describes at most k items; after startup it describes exactly k. Initializing best to zero would be wrong when every complete window has negative sum. Negative values do not invalidate this fixed-size additive update.
Maximum does not support removal by subtraction. A sliding maximum needs another structure, such as a monotonic deque that removes expired indices from the front and dominated candidates from the back. Counts require decrementing the outgoing value; a full recomputation defeats the reuse.
Longest and shortest use different loops
For the longest unique substring, expand right, then repeatedly remove from left while a duplicate exists. Record only after restoring uniqueness. Before expansion the window is unique, so only the entering character can newly violate that condition.
Compute the longest unique substring length
def longest_unique_substring(s):
counts = {}
left = best = 0
for right, char in enumerate(s):
counts[char] = counts.get(char, 0) + 1
while counts[char] > 1:
departing = s[left]
counts[departing] -= 1
left += 1
best = max(best, right - left + 1)
return bestZero-count keys may remain because this validity check reads the entering character's count, not dictionary size. The map can retain all characters seen so far. If validity instead uses the number of current distinct keys, remove a key when its count reaches zero.
For a shortest covering range, expand until all requirements are met, record that valid range, then shrink while it remains valid. The last removal can break validity; do not record an invalid range afterward.
| Objective | Shrink when | Record when |
|---|---|---|
| Longest valid range | Invalid | After validity is restored |
| Shortest covering range | Valid | Before a removal might break coverage |
Each removal needs a reason it can never become a better start for a later right boundary. A duplicate-containing range cannot become unique by appending more characters. Once a covering range at one start has been recorded, extending that same start only makes it longer.
Restore validity before recording length
Code-point sequence “abca”. Adding the second a makes a length-four invalid window. Remove the first a before accepting the new length-three range.
Window is unique. Record length 3; best is 3.
Counts in current window: a:1 · b:1 · c:1. Currently unique.
Exact small input, not a speed benchmark. Step order follows the algorithm; playback is optional. Blue outlines mark active input positions, index labels remain visible.
Track satisfied frequency requirements
For covering a target multiset, keep required counts, current counts and formed: the number of distinct required characters whose count has reached its requirement. Increase formed only when an added required count crosses from need−1 to need; decrease it when removal crosses from need to need−1. Surplus copies do not add another satisfied requirement.
For target “AAB”, {A:1,B:1} is not coverage. {A:2,B:1} is. A scalar avoids comparing whole dictionaries after every move, but its crossing logic must still preserve exact multiplicities.
Record coverage before removing
Synthetic code-point sequence “CAAABA”, target multiset “AAB”. Record “CAAAB”, then remove C and one surplus A without losing coverage. Record “AAB” at [2,4] before the next A removal breaks coverage. The later “ABA” ties its length; keep the earlier range.
Coverage holds. Record this range before removing its leftmost character.
| Character | Need | Current | Satisfied |
|---|---|---|---|
| A | 2 | 3 | yes |
| B | 1 | 1 | yes |
Satisfied requirements: 2/2. Covered: record before removing. Best recorded range: [0,4] = “CAAAB”, length 5.
Exact small input, not a speed benchmark. Step order follows the algorithm; playback is optional. Blue outlines mark active input positions, index labels remain visible.
A fixed-length anagram window can maintain requirement differences or a mismatch count. Delete zero-count keys if dictionary size represents distinct characters. A stale maximum frequency is safe in certain replacement-length algorithms only with that algorithm's proof; it is not an exact summary of every current window.
Exactly K through at-most counts
For at most K distinct values, shrink while too many distinct values remain. After restoring validity, all suffixes starting from left through right are valid, giving new ranges, where l and r are the restored boundaries. Sum that number across right endpoints. Define atMost(K)=0 for K<0.
Count ranges with at most K distinct values
def at_most_k_distinct(nums, k):
if type(k) is not int: raise ValueError('integer K required')
if k < 0: return 0
counts = {}
left = total = 0
for right, value in enumerate(nums):
counts[value] = counts.get(value, 0) + 1
while len(counts) > k:
departing = nums[left]
counts[departing] -= 1
if counts[departing] == 0: del counts[departing]
left += 1
total += right - left + 1
return totalValues must be hashable. Before adding to total, counts contains exactly the positive counts in the restored window. With K=0 the range is empty, so its contribution is zero. For nonnegative integer K, subtract at_most_k_distinct(nums, K−1) from at_most_k_distinct(nums, K) to count exactly K distinct values.
For [1,2,1], atMost(2)=6 and atMost(1)=3, so exactly two distinct values occur in 3 ranges. Empty input contributes zero nonempty ranges. The count can grow quadratically even though computing it takes linear pointer movement.
Where forward-only boundaries fail
“Contiguous” is a recognition signal, not enough proof. For a shortest range with sum at least S, nonnegative values make sum monotone under expansion and contraction. Arbitrary signed values destroy that argument.
Counterexample: [1,−1,5], S=5. A naive shrink-while-valid sum method reaches [1,−1,5] with sum 5 and length 3, removes 1 and sees sum 4, then stops. It misses [5], length 1, because removing the next −1 would restore validity. The usual monotone argument cannot justify discarding that possibility.
Signed exact-sum counts often use prefix sums and frequency lookup. Signed shortest-threshold ranges can use ordered prefix candidates in a monotonic deque. These are different invariants, not permission to reuse the nonnegative shrink rule.
State the window and the cost model
Choose one range convention: this reader uses inclusive [left,right] for windows and half-open [l,r) for prefix queries. Define the empty state before reading length formulas. Use while when several removals may be needed; one if does not generally restore validity.
For forward-only windows, each item enters once and leaves at most once. A nested shrinking loop therefore has O(n) total moves. It has linear total work only when state updates and checks have the assumed bounded cost. Sorting adds O(n log n); iterating all centers or outer fixed values has its own larger cost.
Test empty input, k outside its domain, equal values, no feasible range, an all-valid range, a requirement with repetitions and signed-sum counterexamples. Explain what lies outside the active interval and why no lost candidate can improve the answer.
Connected patterns
Python string and code-point semantics ↗
These traces are new exact instructional examples, not quoted implementation diagrams.
Stored keys and prefix state → · Monotonic candidate reasoning → · Cycle detection and relative motion →