Two pointers, sliding window and prefix sums
Replace a nested loop with two indices that only move forward, keep a window under a constraint, and answer range queries in constant time.
Two pointers on a sorted array
from typing import Sequence
def pair_sum(xs: Sequence[int], target: int) -> tuple[int, int] | None:
"""The input must be sorted: each comparison removes one candidate."""
lo, hi = 0, len(xs) - 1
while lo < hi:
s = xs[lo] + xs[hi]
if s == target:
return lo, hi
if s < target:
lo += 1 # the smallest value cannot be part of any answer
else:
hi -= 1 # the largest value cannot be part of any answer
return None
def dedupe_sorted(xs: list[int]) -> int:
"""In-place deduplication returning the new length (the classic 26 problem)."""
if not xs:
return 0
write = 1
for read in range(1, len(xs)):
if xs[read] != xs[write - 1]:
xs[write] = xs[read]
write += 1
return write
def container_most_water(heights: Sequence[int]) -> int:
"""Move the shorter wall inward: it is the only one that can help."""
lo, hi = 0, len(heights) - 1
best = 0
while lo < hi:
best = max(best, min(heights[lo], heights[hi]) * (hi - lo))
if heights[lo] <= heights[hi]:
lo += 1
else:
hi -= 1
return best
def three_sum(nums: list[int]) -> list[list[int]]:
"""Sort, fix one index, then two-pointer the remainder."""
nums.sort()
out: list[list[int]] = []
for i in range(len(nums) - 2):
if i > 0 and nums[i] == nums[i - 1]:
continue # skip duplicate anchors
if nums[i] > 0:
break # sorted: no later triple can sum to zero
lo, hi = i + 1, len(nums) - 1
while lo < hi:
s = nums[i] + nums[lo] + nums[hi]
if s == 0:
out.append([nums[i], nums[lo], nums[hi]])
lo += 1
hi -= 1
while lo < hi and nums[lo] == nums[lo - 1]:
lo += 1 # skip duplicate partners
while lo < hi and nums[hi] == nums[hi + 1]:
hi -= 1
elif s < 0:
lo += 1
else:
hi -= 1
return out| Pattern | Invariant | Complexity |
|---|---|---|
| Opposite ends | Discard one end per comparison | O(n) after an O(n log n) sort |
| Read and write | Everything before write is final | O(n), in place |
| Fast and slow | The fast pointer runs ahead by a fixed amount | O(n): cycle detection, middle of a list |
| Merge | One pointer per sorted input | O(n + m) |
| N-sum | Fix k-2 indices, two-pointer the rest | O(n^(k-1)) |
The key insight of the opposite-ends pattern is that each comparison eliminates one element for good. If you cannot justify that elimination, you have not found the invariant and the approach will be wrong on some input.
Fixed and variable sliding windows
from typing import Sequence
def max_sum_fixed(xs: Sequence[int], k: int) -> int:
"""A fixed window: add the entering element, subtract the leaving one."""
if k <= 0 or k > len(xs):
raise ValueError("k out of range")
window = sum(xs[:k])
best = window
for i in range(k, len(xs)):
window += xs[i] - xs[i - k]
best = max(best, window)
return best
def min_window_at_least(xs: Sequence[int], target: int) -> int:
"""The smallest window whose sum is at least target. All values positive."""
left = 0
running = 0
best = len(xs) + 1
for right, x in enumerate(xs):
running += x
while running >= target: # shrink while the constraint holds
best = min(best, right - left + 1)
running -= xs[left]
left += 1
return 0 if best > len(xs) else best
def longest_window_with_distinct(s: str) -> int:
"""The variable window with a map of the last position of each character."""
last: dict[str, int] = {}
left = 0
best = 0
for right, ch in enumerate(s):
if ch in last and last[ch] >= left:
left = last[ch] + 1 # jump the left edge past the repeat
last[ch] = right
best = max(best, right - left + 1)
return best
def character_replacement_demo(s: str, k: int) -> int:
"""Sliding window whose validity depends on a maintained histogram."""
counts: dict[str, int] = {}
left = 0
best = 0
for right, ch in enumerate(s):
counts[ch] = counts.get(ch, 0) + 1
most = max(counts.values())
# left pointer advances when the window cannot be fixed with k changes
if (right - left + 1) - most > k:
counts[s[left]] -= 1
left += 1
best = max(best, right - left + 1)
return best
if __name__ == "__main__":
print(max_sum_fixed([2, 1, 5, 1, 3, 2], 3)) # 9
print(min_window_at_least([2, 3, 1, 2, 4, 3], 7)) # 2
print(longest_window_with_distinct("abcabcbb")) # 3- Each pointer moves forward only, so the total work is
O(n)even though there are two loops. The innerwhileamortises: it can run at most n times in total across the whole scan. - A variable window needs a monotone condition: shrinking must never make an invalid window valid again, or the two-pointer argument breaks. Counting distinct characters is monotone; a window sum with negative values is not.
- Maintain the aggregate incrementally. Recomputing
sum(xs[left:right])each step reintroduces theO(n)inner cost. - With a histogram, updating the maximum naively per step is
O(alphabet). For a small fixed alphabet that is a constant, which is why the character-replacement solution stays linear.
Prefix sums and difference arrays
from typing import Sequence
class PrefixSum:
"""Static range-sum queries in O(1) after O(n) preprocessing."""
__slots__ = ("_pre",)
def __init__(self, xs: Sequence[int]) -> None:
pre = [0] * (len(xs) + 1)
for i, x in enumerate(xs):
pre[i + 1] = pre[i] + x
self._pre = pre
def query(self, lo: int, hi: int) -> int:
"""Sum of xs[lo:hi], half open."""
return self._pre[hi] - self._pre[lo]
def difference_array_demo(updates: list[tuple[int, int, int]], n: int) -> list[int]:
"""Apply many range additions in O(1) each, then rebuild in O(n)."""
diff = [0] * (n + 1)
for lo, hi, delta in updates:
diff[lo] += delta
diff[hi + 1] -= delta # the effect stops after hi
out = [0] * n
running = 0
for i in range(n):
running += diff[i]
out[i] = running
return out
def count_subarrays_sum_k(nums: Sequence[int], k: int) -> int:
"""Prefix sums plus a hash map: O(n), works with negative numbers."""
counts: dict[int, int] = {0: 1}
running = 0
total = 0
for x in nums:
running += x
total += counts.get(running - k, 0)
counts[running] = counts.get(running, 0) + 1
return total
def max_subarray_kadane(nums: Sequence[int]) -> int:
"""Not a prefix sum, but the same incremental-accumulator idea."""
best = current = nums[0]
for x in nums[1:]:
current = max(x, current + x) # restart here, or extend
best = max(best, current)
return best
if __name__ == "__main__":
ps = PrefixSum([3, 1, 4, 1, 5])
print(ps.query(1, 4)) # 1+4+1 = 6
print(difference_array_demo([(0, 2, 5), (1, 3, 2)], 5)) # [5, 7, 7, 2, 0]
print(count_subarrays_sum_k([1, 1, 1], 2)) # 2
print(max_subarray_kadane([-2, 1, -3, 4, -1, 2, 1, -5, 4])) # 6💡
A prefix sum answers a question about a range in constant time and costs one pass to build. The moment a problem says "sum of a subarray", "range query" or "count subarrays with property", check whether a prefix sum plus a hash map replaces a nested loop.
FAQ
When does two pointers fail?
When moving a pointer does not provably eliminate candidates. That is why the input is sorted first, or why the window condition must be monotone. With negative numbers in a sliding window sum, shrinking can both raise and lower the sum, so the pattern does not apply.
Prefix sum or a segment tree?
A prefix sum only works when the array does not change. The moment you have point updates interleaved with range queries, you need a Fenwick tree for
O(log n) updates, or a segment tree when the operation is not invertible.Related
Frequency maps, sets and hashing techniques Sorting and searching
Last refreshed 2026-09-18.