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.
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 = 6A 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
- Build the first window by adding the first
k = 3values one at a time.0 + 2 = 2. - Add
1:2 + 1 = 3. This is the only window you add up in full. - Add
5: the first window sums to8. Startbestthere, not at0, because every sum could be negative. - Slide:
1enters on the right and2leaves on the left.8 + 1 - 2 = 7, with no need to re-add the middle.beststays8. 3enters and1leaves:7 + 3 - 1 = 9. That beats8, sobest = 9.2enters and5leaves:9 + 2 - 5 = 6.beststays9.rightreached the end. Every window after the first cost one addition and one subtraction, so the scan is O(n). The biggest sum is9, from[5, 1, 3].
Windows come in two shapes:
- Fixed size. The window always holds
kelements. Build the first one, then each step addsnums[right]and dropsnums[right - k]. The template below is this shape. - Variable size. Move
rightforward one step at a time to grow the window. When the window breaks the rule (a repeated letter, a sum over the limit), moveleftforward 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
kdistinct characters". - A fixed size is given: every
kconsecutive elements, or every window of lengthk. - 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
| Operation | Time | Space |
|---|---|---|
| Recompute every window of size k | O(n · k) | O(1) |
| Fixed-size sliding window | O(n) | O(1) |
| Variable-size sliding window | O(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)); // -3package 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.
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 SolutionHide 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
righttakes in one character per step.pisn't ininWindow, so add it. The windowphas length1, sobest = 1.wis new, so add it.pwhas length1 - 0 + 1 = 2, sobest = 2.wis already ininWindow, at index1. The window holds a repeat, and growing it can't fix that, so moveleftuntil the earlierwis gone.- Drop
pand moveleftto1. The earlierwis still inside, so keep going. - Drop
wand moveleftto2. The repeat is gone, sowcomes in:whas length1, andbeststays2. kis new, so add it.wkhas length3 - 2 + 1 = 2, andbeststays2.eis new, so add it.wkehas length4 - 2 + 1 = 3, sobest = 3.- Another
w, and the earlier one is still inside at index2. Same fix: shrink from the left until it's gone. - Drop
wand moveleftto3. The repeat is gone, sowcomes in:kewhas length3, andbeststays3. rightreached the end andleftonly ever moved forward, so each character entered once and left at most once: O(n). The longest run without a repeat has length3.
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("")); // 0package 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
}
}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 SolutionHide 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 ofcthe window has to take in. It starts at the counts oftand goes negative when the window holds spares.missingis the total number of characters still owed. Taking inconly lowers it whenneed[c]was positive, because spares don't help. Droppingconly raises it whenneed[c]climbs back above 0.- The window covers
texactly whenmissingis 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
bestat 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]holdsright - left + 1elements. - 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
missinginstead.
Practice
More problems to try on your own.