Sliding Window

Track one contiguous stretch of an array or string as it slides, updating it in O(1) per step instead of rescanning every subarray.

IntermediateRead first: Two Pointers, Arrays & Hash Maps

How It Works

Many problems ask about a contiguous stretch of an array or string: the biggest sum of k neighbors, the longest substring with no repeats, the shortest piece that covers a set of letters. Brute force tries every start and every end, which is O(n²) windows before you even look inside them. A sliding window keeps a single window [left, right] and updates what it knows as the edges move. Add the element that enters on the right, remove the one that leaves on the left, and never rescan the middle.

k = 3

 [2, 1, 5, 1, 3, 2]
  ^-----^              2 + 1 + 5 = 8
     ^-----^           8 - 2 + 1 = 7
        ^-----^        7 - 1 + 3 = 9   best
           ^-----^     9 - 5 + 2 = 6
Each slide drops one element and adds one, so every window after the first costs O(1).

A Fixed Window, Step by Step

Step 1 of 7

nums

  • Entering the window
  • Window
  • Left the window
  • Best window

Build the first window by adding the first k = 3 values one at a time. 0 + 2 = 2.

All 7 steps as text
  1. Build the first window by adding the first k = 3 values one at a time. 0 + 2 = 2.
  2. Add 1: 2 + 1 = 3. This is the only window you add up in full.
  3. Add 5: the first window sums to 8. Start best there, not at 0, because every sum could be negative.
  4. Slide: 1 enters on the right and 2 leaves on the left. 8 + 1 - 2 = 7, with no need to re-add the middle. best stays 8.
  5. 3 enters and 1 leaves: 7 + 3 - 1 = 9. That beats 8, so best = 9.
  6. 2 enters and 5 leaves: 9 + 2 - 5 = 6. best stays 9.
  7. right reached the end. Every window after the first cost one addition and one subtraction, so the scan is O(n). The biggest sum is 9, from [5, 1, 3].

Windows come in two shapes:

  • Fixed size. The window always holds k elements. Build the first one, then each step adds nums[right] and drops nums[right - k]. The template below is this shape.
  • Variable size. Move right forward one step at a time to grow the window. When the window breaks the rule (a repeated letter, a sum over the limit), move left forward until it's valid again, then record the answer. When the question wants the shortest valid window, flip it: grow until the window is valid, then shrink it while it stays valid.

The variable window relies on one property: validity only changes in one direction as the window grows. A window with a repeated letter still has that repeat however far you extend it, so once it breaks, the only fix is to move left, and no window starting further left is worth trying again. Since left and right both only move forward, each index enters the window once and leaves once. The inner shrinking loop doesn't make it O(n²): the whole scan is O(n).

When to Reach for It

  • The question asks about a contiguous subarray or substring, not a subsequence.
  • It wants the longest, shortest or best window that follows a rule, such as "at most k distinct characters".
  • A fixed size is given: every k consecutive elements, or every window of length k.
  • The values are non-negative, so growing a window never makes its sum smaller.
  • Brute force would recompute something over every range, but you could update it as one element enters and another leaves.

Complexity

OperationTimeSpace
Recompute every window of size kO(n · k)O(1)
Fixed-size sliding windowO(n)O(1)
Variable-size sliding windowO(n)O(m) for counts of m distinct values

The Template

Max Sum of k Consecutive Elements

Return the largest sum of k consecutive elements. Sum the first k values to build the first window. Then slide it one step at a time: nums[right] enters and nums[right - k] leaves, so the new sum is one addition and one subtraction away from the old one.

Start best at the first window's sum, not at 0. If every number is negative, the answer is negative too, and a best of 0 would never be beaten.

Time O(n)Space O(1)

def max_sum_window(nums: list[int], k: int) -> int:
    """Largest sum of any `k` consecutive elements of `nums` (1 <= k <= len(nums))."""
    window_sum = 0
    for i in range(k):
        window_sum += nums[i]  # The first window, summed directly.
    best = window_sum  # Not 0: every window's sum could be negative.
    for right in range(k, len(nums)):
        # Slide one step: nums[right] enters and nums[right - k] leaves.
        window_sum += nums[right] - nums[right - k]
        best = max(best, window_sum)
    return best


if __name__ == "__main__":
    print(max_sum_window([2, 1, 5, 1, 3, 2], 3))  # 9
    print(max_sum_window([1, 12, -5, -6, 50, 3], 4))  # 51
    print(max_sum_window([-3, -1, -2], 2))  # -3
// Largest sum of any `k` consecutive elements of `nums` (1 <= k <= nums.length).
export function maxSumWindow(nums: number[], k: number): number {
  let windowSum = 0;
  for (let i = 0; i < k; i++) windowSum += nums[i]; // The first window, summed directly.
  let best = windowSum; // Not 0: every window's sum could be negative.
  for (let right = k; right < nums.length; right++) {
    // Slide one step: nums[right] enters and nums[right - k] leaves.
    windowSum += nums[right] - nums[right - k];
    best = Math.max(best, windowSum);
  }
  return best;
}

console.log(maxSumWindow([2, 1, 5, 1, 3, 2], 3)); // 9
console.log(maxSumWindow([1, 12, -5, -6, 50, 3], 4)); // 51
console.log(maxSumWindow([-3, -1, -2], 2)); // -3
package main

import "fmt"

// maxSumWindow returns the largest sum of any k consecutive elements of nums
// (1 <= k <= len(nums)).
func maxSumWindow(nums []int, k int) int {
	windowSum := 0
	for i := 0; i < k; i++ {
		windowSum += nums[i] // The first window, summed directly.
	}
	best := windowSum // Not 0: every window's sum could be negative.
	for right := k; right < len(nums); right++ {
		// Slide one step: nums[right] enters and nums[right - k] leaves.
		windowSum += nums[right] - nums[right-k]
		best = max(best, windowSum)
	}
	return best
}

func main() {
	fmt.Println(maxSumWindow([]int{2, 1, 5, 1, 3, 2}, 3))     // 9
	fmt.Println(maxSumWindow([]int{1, 12, -5, -6, 50, 3}, 4)) // 51
	fmt.Println(maxSumWindow([]int{-3, -1, -2}, 2))           // -3
}
using System;

public class Solution
{
    // Largest sum of any k consecutive elements of nums (1 <= k <= nums.Length).
    public static int MaxSumWindow(int[] nums, int k)
    {
        int windowSum = 0;
        for (int i = 0; i < k; i++) windowSum += nums[i]; // The first window, summed directly.
        int best = windowSum; // Not 0: every window's sum could be negative.
        for (int right = k; right < nums.Length; right++)
        {
            // Slide one step: nums[right] enters and nums[right - k] leaves.
            windowSum += nums[right] - nums[right - k];
            best = Math.Max(best, windowSum);
        }
        return best;
    }

    public static void Main()
    {
        Console.WriteLine(MaxSumWindow(new[] { 2, 1, 5, 1, 3, 2 }, 3)); // 9
        Console.WriteLine(MaxSumWindow(new[] { 1, 12, -5, -6, 50, 3 }, 4)); // 51
        Console.WriteLine(MaxSumWindow(new[] { -3, -1, -2 }, 2)); // -3
    }
}
public class Solution {
    // Largest sum of any k consecutive elements of nums (1 <= k <= nums.length).
    static int maxSumWindow(int[] nums, int k) {
        int windowSum = 0;
        for (int i = 0; i < k; i++) windowSum += nums[i]; // The first window, summed directly.
        int best = windowSum; // Not 0: every window's sum could be negative.
        for (int right = k; right < nums.length; right++) {
            // Slide one step: nums[right] enters and nums[right - k] leaves.
            windowSum += nums[right] - nums[right - k];
            best = Math.max(best, windowSum);
        }
        return best;
    }

    public static void main(String[] args) {
        System.out.println(maxSumWindow(new int[] {2, 1, 5, 1, 3, 2}, 3)); // 9
        System.out.println(maxSumWindow(new int[] {1, 12, -5, -6, 50, 3}, 4)); // 51
        System.out.println(maxSumWindow(new int[] {-3, -1, -2}, 2)); // -3
    }
}

Worked Problems

Each solution stays folded until you open it, so you can try the problem first. Every one is a complete program in Python, TypeScript, Go, C# and Java, run and checked against its expected output.

Longest Substring Without Repeating Characters

MEDIUMLeetCode 3

Problem

Given a string s, find the length of the longest substring (a contiguous run of characters) in which no character appears twice.

Example: "abcabcbb" returns 3. The longest runs without a repeat are "abc", "bca" and "cab".

Show Solution

Approach

This is the variable-size window. Move right one character at a time and keep the characters currently in the window in a set. When s[right] is already in the set, the window holds a repeat, and every longer window that keeps both copies would too. So move left forward, removing characters as you go, until the earlier copy is gone. Then add s[right] and record the window's length, right - left + 1.

s = "pwwkew"

[p] w  w  k  e  w    length 1
[p  w] w  k  e  w    length 2
 p  w [w] k  e  w    drop p and w, length 1
 p  w [w  k] e  w    length 2
 p  w [w  k  e] w    length 3
 p  w  w [k  e  w]   drop w, length 3
right moves every step. left only moves to push out a repeat, so each character enters and leaves the window once.

A Variable Window, Step by Step

Step 1 of 10

s

inWindow

  • Entering the window
  • Window
  • Repeated character
  • Left the window
  • Answer

right takes in one character per step. p isn't in inWindow, so add it. The window p has length 1, so best = 1.

All 10 steps as text
  1. right takes in one character per step. p isn't in inWindow, so add it. The window p has length 1, so best = 1.
  2. w is new, so add it. pw has length 1 - 0 + 1 = 2, so best = 2.
  3. w is already in inWindow, at index 1. The window holds a repeat, and growing it can't fix that, so move left until the earlier w is gone.
  4. Drop p and move left to 1. The earlier w is still inside, so keep going.
  5. Drop w and move left to 2. The repeat is gone, so w comes in: w has length 1, and best stays 2.
  6. k is new, so add it. wk has length 3 - 2 + 1 = 2, and best stays 2.
  7. e is new, so add it. wke has length 4 - 2 + 1 = 3, so best = 3.
  8. Another w, and the earlier one is still inside at index 2. Same fix: shrink from the left until it's gone.
  9. Drop w and move left to 3. The repeat is gone, so w comes in: kew has length 3, and best stays 3.
  10. right reached the end and left only ever moved forward, so each character entered once and left at most once: O(n). The longest run without a repeat has length 3.

Time O(n)Space O(m), m = distinct characters

def length_of_longest_substring(s: str) -> int:
    """Length of the longest substring of `s` with no repeated character."""
    in_window: set[str] = set()
    left = 0
    best = 0
    for right, c in enumerate(s):
        # A repeat breaks the window, and growing it can't fix that.
        # Drop characters from the left until the earlier copy of c is gone.
        while c in in_window:
            in_window.remove(s[left])
            left += 1
        in_window.add(c)
        best = max(best, right - left + 1)
    return best


if __name__ == "__main__":
    print(length_of_longest_substring("abcabcbb"))  # 3
    print(length_of_longest_substring("bbbbb"))  # 1
    print(length_of_longest_substring("pwwkew"))  # 3
    print(length_of_longest_substring(""))  # 0
// Length of the longest substring of `s` with no repeated character.
export function lengthOfLongestSubstring(s: string): number {
  const inWindow = new Set<string>();
  let left = 0;
  let best = 0;
  for (let right = 0; right < s.length; right++) {
    const c = s[right];
    // A repeat breaks the window, and growing it can't fix that.
    // Drop characters from the left until the earlier copy of c is gone.
    while (inWindow.has(c)) {
      inWindow.delete(s[left]);
      left++;
    }
    inWindow.add(c);
    best = Math.max(best, right - left + 1);
  }
  return best;
}

console.log(lengthOfLongestSubstring("abcabcbb")); // 3
console.log(lengthOfLongestSubstring("bbbbb")); // 1
console.log(lengthOfLongestSubstring("pwwkew")); // 3
console.log(lengthOfLongestSubstring("")); // 0
package main

import "fmt"

// lengthOfLongestSubstring returns the length of the longest substring of s
// with no repeated character.
func lengthOfLongestSubstring(s string) int {
	inWindow := map[byte]bool{}
	left := 0
	best := 0
	for right := 0; right < len(s); right++ {
		c := s[right]
		// A repeat breaks the window, and growing it can't fix that.
		// Drop characters from the left until the earlier copy of c is gone.
		for inWindow[c] {
			delete(inWindow, s[left])
			left++
		}
		inWindow[c] = true
		best = max(best, right-left+1)
	}
	return best
}

func main() {
	fmt.Println(lengthOfLongestSubstring("abcabcbb")) // 3
	fmt.Println(lengthOfLongestSubstring("bbbbb"))    // 1
	fmt.Println(lengthOfLongestSubstring("pwwkew"))   // 3
	fmt.Println(lengthOfLongestSubstring(""))         // 0
}
using System;
using System.Collections.Generic;

public class Solution
{
    // Length of the longest substring of s with no repeated character.
    public static int LengthOfLongestSubstring(string s)
    {
        var inWindow = new HashSet<char>();
        int left = 0, best = 0;
        for (int right = 0; right < s.Length; right++)
        {
            char c = s[right];
            // A repeat breaks the window, and growing it can't fix that.
            // Drop characters from the left until the earlier copy of c is gone.
            while (inWindow.Contains(c))
            {
                inWindow.Remove(s[left]);
                left++;
            }
            inWindow.Add(c);
            best = Math.Max(best, right - left + 1);
        }
        return best;
    }

    public static void Main()
    {
        Console.WriteLine(LengthOfLongestSubstring("abcabcbb")); // 3
        Console.WriteLine(LengthOfLongestSubstring("bbbbb")); // 1
        Console.WriteLine(LengthOfLongestSubstring("pwwkew")); // 3
        Console.WriteLine(LengthOfLongestSubstring("")); // 0
    }
}
import java.util.HashSet;
import java.util.Set;

public class Solution {
    // Length of the longest substring of s with no repeated character.
    static int lengthOfLongestSubstring(String s) {
        Set<Character> inWindow = new HashSet<>();
        int left = 0, best = 0;
        for (int right = 0; right < s.length(); right++) {
            char c = s.charAt(right);
            // A repeat breaks the window, and growing it can't fix that.
            // Drop characters from the left until the earlier copy of c is gone.
            while (inWindow.contains(c)) {
                inWindow.remove(s.charAt(left));
                left++;
            }
            inWindow.add(c);
            best = Math.max(best, right - left + 1);
        }
        return best;
    }

    public static void main(String[] args) {
        System.out.println(lengthOfLongestSubstring("abcabcbb")); // 3
        System.out.println(lengthOfLongestSubstring("bbbbb")); // 1
        System.out.println(lengthOfLongestSubstring("pwwkew")); // 3
        System.out.println(lengthOfLongestSubstring("")); // 0
    }
}

Minimum Window Substring

HARDLeetCode 76

Problem

Given strings s and t, return the shortest substring of s that contains every character of t, counting repeats: if t has two as, the window needs two as. Return an empty string if no such window exists. Neither string is empty.

Example: s = "ADOBECODEBANC" and t = "ABC" returns "BANC".

Show Solution

Approach

This is the variable window turned around. Grow right until the window covers all of t. Then shrink from the left while it still covers `t`, recording each size, because a shorter valid window is a better answer. As soon as a needed character drops out, go back to growing.

Comparing two count maps on every step would cost O(alphabet) per step. Keep two running values instead:

  • need[c] is how many more copies of c the window has to take in. It starts at the counts of t and goes negative when the window holds spares.
  • missing is the total number of characters still owed. Taking in c only lowers it when need[c] was positive, because spares don't help. Dropping c only raises it when need[c] climbs back above 0.
  • The window covers t exactly when missing is 0, which is an O(1) check.

Each index of s enters the window once and leaves once, plus one pass over t to build the counts.

Time O(|s| + |t|)Space O(m), m = distinct characters

from collections import Counter


def min_window(s: str, t: str) -> str:
    """Shortest substring of `s` that contains every character of `t`, repeats
    included, or "" if there isn't one."""
    # need[c] is how many more copies of c the window has to take in.
    # It goes negative when the window holds spares.
    need = Counter(t)
    missing = len(t)  # Characters of t the window still lacks.
    best_start = 0
    best_length = len(s) + 1  # Longer than any real window.
    left = 0
    for right, c in enumerate(s):
        if need[c] > 0:
            missing -= 1  # This copy was still needed.
        need[c] -= 1
        # The window covers t. Shrink it while it still does, keeping the smallest.
        while missing == 0:
            if right - left + 1 < best_length:
                best_start = left
                best_length = right - left + 1
            dropped = s[left]
            need[dropped] += 1
            if need[dropped] > 0:
                missing += 1  # Dropped a copy the window needed.
            left += 1
    return "" if best_length > len(s) else s[best_start : best_start + best_length]


if __name__ == "__main__":
    print(repr(min_window("ADOBECODEBANC", "ABC")))  # 'BANC'
    print(repr(min_window("a", "aa")))  # ''
    print(repr(min_window("a", "a")))  # 'a'
// Shortest substring of `s` that contains every character of `t`, repeats
// included, or "" if there isn't one.
export function minWindow(s: string, t: string): string {
  // need.get(c) is how many more copies of c the window has to take in.
  // It goes negative when the window holds spares.
  const need = new Map<string, number>();
  for (const c of t) need.set(c, (need.get(c) ?? 0) + 1);
  let missing = t.length; // Characters of t the window still lacks.
  let bestStart = 0;
  let bestLength = s.length + 1; // Longer than any real window.
  let left = 0;
  for (let right = 0; right < s.length; right++) {
    const c = s[right];
    const count = need.get(c) ?? 0;
    if (count > 0) missing--; // This copy was still needed.
    need.set(c, count - 1);
    // The window covers t. Shrink it while it still does, keeping the smallest.
    while (missing === 0) {
      if (right - left + 1 < bestLength) {
        bestStart = left;
        bestLength = right - left + 1;
      }
      const dropped = s[left];
      const dropCount = (need.get(dropped) ?? 0) + 1;
      need.set(dropped, dropCount);
      if (dropCount > 0) missing++; // Dropped a copy the window needed.
      left++;
    }
  }
  return bestLength > s.length ? "" : s.slice(bestStart, bestStart + bestLength);
}

console.log(JSON.stringify(minWindow("ADOBECODEBANC", "ABC"))); // "BANC"
console.log(JSON.stringify(minWindow("a", "aa"))); // ""
console.log(JSON.stringify(minWindow("a", "a"))); // "a"
package main

import "fmt"

// minWindow returns the shortest substring of s that contains every character
// of t, repeats included, or "" if there isn't one.
func minWindow(s, t string) string {
	// need[c] is how many more copies of c the window has to take in.
	// It goes negative when the window holds spares.
	need := map[byte]int{}
	for i := 0; i < len(t); i++ {
		need[t[i]]++
	}
	missing := len(t) // Characters of t the window still lacks.
	bestStart := 0
	bestLength := len(s) + 1 // Longer than any real window.
	left := 0
	for right := 0; right < len(s); right++ {
		c := s[right]
		if need[c] > 0 {
			missing-- // This copy was still needed.
		}
		need[c]--
		// The window covers t. Shrink it while it still does, keeping the smallest.
		for missing == 0 {
			if right-left+1 < bestLength {
				bestStart = left
				bestLength = right - left + 1
			}
			dropped := s[left]
			need[dropped]++
			if need[dropped] > 0 {
				missing++ // Dropped a copy the window needed.
			}
			left++
		}
	}
	if bestLength > len(s) {
		return ""
	}
	return s[bestStart : bestStart+bestLength]
}

func main() {
	fmt.Printf("%q\n", minWindow("ADOBECODEBANC", "ABC")) // "BANC"
	fmt.Printf("%q\n", minWindow("a", "aa"))              // ""
	fmt.Printf("%q\n", minWindow("a", "a"))               // "a"
}
using System;
using System.Collections.Generic;

public class Solution
{
    // Shortest substring of s that contains every character of t, repeats
    // included, or "" if there isn't one.
    public static string MinWindow(string s, string t)
    {
        // need[c] is how many more copies of c the window has to take in.
        // It goes negative when the window holds spares.
        var need = new Dictionary<char, int>();
        foreach (char c in t) need[c] = need.GetValueOrDefault(c) + 1;
        int missing = t.Length; // Characters of t the window still lacks.
        int bestStart = 0;
        int bestLength = s.Length + 1; // Longer than any real window.
        int left = 0;
        for (int right = 0; right < s.Length; right++)
        {
            char c = s[right];
            if (need.GetValueOrDefault(c) > 0) missing--; // This copy was still needed.
            need[c] = need.GetValueOrDefault(c) - 1;
            // The window covers t. Shrink it while it still does, keeping the smallest.
            while (missing == 0)
            {
                if (right - left + 1 < bestLength)
                {
                    bestStart = left;
                    bestLength = right - left + 1;
                }
                char dropped = s[left];
                need[dropped]++;
                if (need[dropped] > 0) missing++; // Dropped a copy the window needed.
                left++;
            }
        }
        return bestLength > s.Length ? "" : s.Substring(bestStart, bestLength);
    }

    public static void Main()
    {
        Console.WriteLine(Quote(MinWindow("ADOBECODEBANC", "ABC"))); // "BANC"
        Console.WriteLine(Quote(MinWindow("a", "aa"))); // ""
        Console.WriteLine(Quote(MinWindow("a", "a"))); // "a"
    }

    static string Quote(string text) => "\"" + text + "\"";
}
import java.util.HashMap;
import java.util.Map;

public class Solution {
    // Shortest substring of s that contains every character of t, repeats
    // included, or "" if there isn't one.
    static String minWindow(String s, String t) {
        // need.get(c) is how many more copies of c the window has to take in.
        // It goes negative when the window holds spares.
        Map<Character, Integer> need = new HashMap<>();
        for (int i = 0; i < t.length(); i++) need.merge(t.charAt(i), 1, Integer::sum);
        int missing = t.length(); // Characters of t the window still lacks.
        int bestStart = 0;
        int bestLength = s.length() + 1; // Longer than any real window.
        int left = 0;
        for (int right = 0; right < s.length(); right++) {
            char c = s.charAt(right);
            if (need.getOrDefault(c, 0) > 0) missing--; // This copy was still needed.
            need.merge(c, -1, Integer::sum);
            // The window covers t. Shrink it while it still does, keeping the smallest.
            while (missing == 0) {
                if (right - left + 1 < bestLength) {
                    bestStart = left;
                    bestLength = right - left + 1;
                }
                char dropped = s.charAt(left);
                need.merge(dropped, 1, Integer::sum);
                if (need.get(dropped) > 0) missing++; // Dropped a copy the window needed.
                left++;
            }
        }
        return bestLength > s.length() ? "" : s.substring(bestStart, bestStart + bestLength);
    }

    public static void main(String[] args) {
        System.out.println(quote(minWindow("ADOBECODEBANC", "ABC"))); // "BANC"
        System.out.println(quote(minWindow("a", "aa"))); // ""
        System.out.println(quote(minWindow("a", "a"))); // "a"
    }

    static String quote(String text) {
        return "\"" + text + "\"";
    }
}

Common Pitfalls

  • Starting best at 0 in a max-sum window. If every value is negative, the real answer is below 0.
  • Shrinking a window to control a sum when the array has negative numbers. The sum no longer moves in one direction, so you skip valid windows. Use prefix sums with a hash map instead.
  • Getting the length wrong. The window [left, right] holds right - left + 1 elements.
  • Recording the answer in the wrong place. A longest-window problem records after the window is valid again. A shortest-window problem records inside the shrink loop, while the window is still valid.
  • Checking the whole window on every step, for example by comparing two count maps, which quietly brings back the slow version. Keep a running summary such as missing instead.

Practice

More problems to try on your own.