Two Pointers and the Sliding Window

July 8, 2026 · 3 min read

A huge fraction of array and string problems that look like they need nested loops actually don't. Two related patterns — two pointers and the sliding window — collapse that quadratic work into a single pass by never throwing away information you already computed. Once you recognize the shape, you'll see them everywhere.

The problem with the naive approach

Say you want the maximum sum of any 3 consecutive elements in an array. The obvious solution sums every window from scratch:

function maxSum(arr, k) {
  let best = -Infinity;
  for (let i = 0; i + k <= arr.length; i++) {
    let sum = 0;
    for (let j = i; j < i + k; j++) sum += arr[j]; // re-adds k elements
    best = Math.max(best, sum);
  }
  return best;
}

That inner loop re-adds k numbers for every position — O(n·k) work. But look at what happens when the window moves one step right: it loses its leftmost element and gains one on the right. Everything in the middle is identical. We're recomputing what we already knew.

The sliding window

Keep a running sum. When the window slides, subtract the element leaving and add the one entering — two operations, regardless of window size:

2
0
1
1
5
2
1
3
3
4
2
5
window sum = 8best = 8

Build the first window of size 3: [2, 1, 5] = 8. That is our best so far.

0 / 4
function maxSum(arr, k) {
  let sum = 0;
  for (let i = 0; i < k; i++) sum += arr[i]; // first window
  let best = sum;

  for (let end = k; end < arr.length; end++) {
    sum += arr[end] - arr[end - k]; // add new, drop old
    best = Math.max(best, sum);
  }
  return best;
}

One pass, O(n) time, O(1) space. The window carries its state forward instead of rebuilding it.

Fixed vs. dynamic windows

The example above is a fixed-size window. The more powerful variant is the dynamic window, where the right edge expands to include more and the left edge contracts when a constraint breaks. The classic is longest substring without repeating characters:

function longestUnique(s) {
  const seen = new Set();
  let left = 0, best = 0;

  for (let right = 0; right < s.length; right++) {
    while (seen.has(s[right])) {
      seen.delete(s[left]); // shrink from the left until valid again
      left++;
    }
    seen.add(s[right]);
    best = Math.max(best, right - left + 1);
  }
  return best;
}

Each character is added once and removed at most once, so even with the inner while this is still O(n) — the pointers only ever move forward.

Two pointers

The sliding window is really a special case of the broader two pointers idea: maintain two indices and move them based on the data instead of brute-forcing every pair. The other common flavor is converging pointers on a sorted array — one from each end, walking toward the middle. Finding a pair that sums to a target:

function twoSumSorted(arr, target) {
  let lo = 0, hi = arr.length - 1;
  while (lo < hi) {
    const sum = arr[lo] + arr[hi];
    if (sum === target) return [lo, hi];
    if (sum < target) lo++;  // need more → move the low end up
    else hi--;               // need less → move the high end down
  }
  return null;
}

Because the array is sorted, each comparison lets us rule out an entire row or column of the pair space — O(n) instead of the O() of checking all pairs.

How to spot them

Signal in the problemReach for
"subarray / substring of size k" or "contiguous"Sliding window
"longest / shortest window satisfying a condition"Dynamic sliding window
Sorted array, find a pair / tripletConverging two pointers
In-place partition, dedupe, or reverseTwo pointers (read + write)

Wrap-up

The common thread is don't recompute what hasn't changed. A sliding window updates its state incrementally as it moves; two converging pointers use sorted order to discard half the possibilities each step. Both trade a nested loop for a single forward sweep — the difference between code that scales and code that times out.