Skip to content
ΣDSA Patterns
Menu
Language

Sliding Window

Guide 2 of 6 · Path 2 of 6

Interactive

Mental model

A worked animation for this problem. Scrub steps or press space to pause; re-tell the invariant out loud.

Step 1 of 9
a
b
c
a
b
b

best = 0

Start with an empty window. Invariant: all characters unique.

Demo preview: These solutions pass the automated test suite but have not been human-reviewed. Treat trade-offs and prose as draft.

Minimum Window Substring

Problem (restated)

Given strings s and t, return the minimum-length substring of s that covers every character of t (with at least the same multiplicities). If no such window exists, return the empty string. Any minimum window is acceptable if several share the same length.

Intuition

This is the min-window shape of sliding window, not the “longest valid” shape:

  1. Expand right until the window is valid (covers t).
  2. While it stays valid, shrink left and track the best (shortest) window.
  3. When shrinking breaks validity, expand again.

The invariant is “window contains all required counts from t.”

Approaches

Sliding window with need/have counts

Tested only
Time O(|s| + |t|)Space O(Σ)

Idea. Build need frequencies from t and a missing (or have/needTypes) counter. Move right, decrement need for that char when useful. When missing == 0, the window is valid: record s[left..right], then advance left and restore need until invalid.

Walkthrough. s = "ADOBECODEBANC", t = "ABC".

step window (idea) notes
expand to first full cover ADOBEC first valid, length 6
shrink while valid still need a full cover track best
later expand/shrink BANC better length 4 → answer

Trade-offs. Still O(|s|) once each end moves at most once. Bookkeeping is the hard part: confuse “need” vs “window count” and you under-shrink or accept incomplete covers. Fixed-size alphabet arrays beat hash maps for ASCII.

Solution
export function minWindow(s: string, t: string): string {
  if (!t) return "";
  const need = new Map<string, number>();
  for (const ch of t) need.set(ch, (need.get(ch) ?? 0) + 1);
  let missing = need.size;
  let left = 0;
  let bestL = 0, bestLen = Infinity;
  const window = new Map<string, number>();
  for (let right = 0; right < s.length; right++) {
    const ch = s[right]!;
    window.set(ch, (window.get(ch) ?? 0) + 1);
    if (need.has(ch) && window.get(ch) === need.get(ch)) missing--;
    while (missing === 0) {
      if (right - left + 1 < bestLen) {
        bestLen = right - left + 1;
        bestL = left;
      }
      const leftCh = s[left]!;
      window.set(leftCh, (window.get(leftCh) ?? 0) - 1);
      if (need.has(leftCh) && (window.get(leftCh) ?? 0) < need.get(leftCh)!) missing++;
      left++;
    }
  }
  return bestLen === Infinity ? "" : s.slice(bestL, bestL + bestLen);
}
export function minWindow(s: string, t: string): string {
  if (!t) return "";
  const need = new Map<string, number>();
  for (const ch of t) need.set(ch, (need.get(ch) ?? 0) + 1);
  let missing = need.size;
  let left = 0;
  let bestL = 0, bestLen = Infinity;
  const window = new Map<string, number>();
  for (let right = 0; right < s.length; right++) {
    const ch = s[right]!;
    window.set(ch, (window.get(ch) ?? 0) + 1);
    if (need.has(ch) && window.get(ch) === need.get(ch)) missing--;
    while (missing === 0) {
      if (right - left + 1 < bestLen) {
        bestLen = right - left + 1;
        bestL = left;
      }
      const leftCh = s[left]!;
      window.set(leftCh, (window.get(leftCh) ?? 0) - 1);
      if (need.has(leftCh) && (window.get(leftCh) ?? 0) < need.get(leftCh)!) missing++;
      left++;
    }
  }
  return bestLen === Infinity ? "" : s.slice(bestL, bestL + bestLen);
}

Template connection

Opposite of the default max-window template: there you shrink while invalid and always update length when valid. Here you shrink while valid and only then update the minimum. See the sliding-window template header for both shapes.

Common bugs

Reflection