CodeOath
← All posts
Data Structures & Algorithms43 min total · 16 parts

DSA Patterns for Coding Interviews: The Techniques Behind Almost Every Problem

Part 3 of 16 · ~4 min

Two Pointers & Sliding Window

The two-pointer technique replaces a nested loop — usually O(n²) — with two indices that move through the data in a single coordinated pass, usually O(n). It comes in two genuinely distinct shapes, and mixing them up is the most common way people fumble it under pressure.

Opposite-ends two pointers

The signal: a sorted array (or something equivalent to one), looking for a pair — or a small fixed number of elements — that satisfies a sum or comparison condition.

function hasPairWithSum(sortedArr, target) {
  let left = 0, right = sortedArr.length - 1;
  while (left < right) {
    const sum = sortedArr[left] + sortedArr[right];
    if (sum === target) return true;
    if (sum < target) left++;    // sum too small — only INCREASING left can raise it
    else right--;                // sum too big — only DECREASING right can lower it
  }
  return false;
}

The reason this is safe — not just fast — is worth being able to explain out loud: because the array is sorted, if sortedArr[left] + sortedArr[right] is already too small, pairing sortedArr[left] with anything at an index less than right would only make the sum smaller still, so sortedArr[left] can never be part of a valid pair with anything to the left of right, and it's safe to move on from it entirely. Each pointer only ever moves in one direction and never backtracks, so the total work across the whole run is proportional to n, not .

Same-direction two pointers

The signal: in-place array modification — "remove duplicates," "partition around a value" — or two indices moving at different speeds through the same structure, like linked-list cycle detection.

function removeDuplicatesSorted(arr) {
  if (arr.length === 0) return 0;
  let slow = 0;                          // slow marks the end of the already-deduplicated region
  for (let fast = 1; fast < arr.length; fast++) {
    if (arr[fast] !== arr[slow]) {
      slow++;
      arr[slow] = arr[fast];
    }
  }
  return slow + 1;                       // length of the deduplicated prefix
}

fast explores ahead while slow only advances when it finds something worth keeping — that's the shape underneath most in-place partitioning problems, and it's also the shape behind Floyd's cycle detection on a linked list, where slow moves one node at a time and fast moves two: if there's a cycle, fast eventually laps slow from behind instead of ever reaching the end.

Sliding window — fixed size

The signal: "a subarray of size exactly k" — a contiguous window whose length never changes.

function maxSumFixedWindow(arr, k) {
  let windowSum = 0;
  for (let i = 0; i < k; i++) windowSum += arr[i];   // build the first window once: O(k)

  let maxSum = windowSum;
  for (let i = k; i < arr.length; i++) {
    windowSum += arr[i] - arr[i - k];    // slide: add the new right edge, drop the old left edge
    maxSum = Math.max(maxSum, windowSum);
  }
  return maxSum;
}

Recomputing each k-length sum from scratch would cost O(n·k). Updating the running sum by one addition and one subtraction per step brings the whole scan down to O(n) — the window "slides" without ever re-examining an element it's already accounted for.

Sliding window — variable size

The signal: "longest/shortest substring or subarray such that...," where the window's size adjusts based on a condition rather than being fixed up front.

function longestSubstringNoRepeats(s) {
  const seen = new Set();
  let left = 0, longest = 0;

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

right expands the window one character at a time; the inner while only shrinks it when the window has become invalid. It looks like a nested loop, but left never moves backward and never revisits a position, so across the whole run it advances at most n times total — the combined cost of the outer and inner loops together is still O(n), by the same amortized argument as the monotonic stack later in this piece.

The signal that tells you which applies: two pointers usually shows up on a sorted structure where you're hunting for a pair, or doing an in-place partition. A sliding window shows up on a contiguous run — a substring or subarray — where the question is "longest," "shortest," "contains," or "at most/exactly K of something," and the window's boundary moves based on a condition rather than a fixed sorted order.

Practice this on Code Lab: Two Pointers and Sliding Window