Lessons
1 · Foundations (Think Like a Programmer)
2 · Data Structures and Recursion
- 05Arrays and Array-Based Problem Solving
- 06Strings: Characters, Immutability, and Efficient Manipulation
- 07Linked Lists: Nodes, Pointers, and Pointer Reasoning
- 08Stacks and Queues
- 09Hashing: HashMap, HashSet, and Frequency Patterns
- 10Recursion and Backtracking
- 11Trees and Binary Search Trees
- 12Heaps and Priority Queues
3 · Core Algorithms and Reusable Patterns
4 · Advanced Interview Topics
Lesson 15 · Core Algorithms and Reusable Patterns
Sliding Window: Efficient Subarray and Substring Problems
Fixed-size and variable-size windows over arrays and strings, with running sums, counters and frequency maps. Learn the expand-and-shrink template, why it runs in O(n), and when negative numbers break it.
For
Learners who know two pointers and hashing and want an efficient method for contiguous subarray and substring questions.
Level
Before you start
Articles 5, 6, 9 and 14: arrays, strings, hash maps and two pointers. A window is two pointers marking a contiguous range.
On this page
Key takeaways
- A sliding window keeps a running summary of a contiguous range. When the window moves, you update the summary instead of recomputing it from scratch.
- Fixed-size windows add the new element and remove the one that left. Variable-size windows expand the right end, and shrink the left end while a condition is broken or satisfied.
- Each pointer only moves forward, so the total work is O(n), even though a while loop sits inside a for loop.
- The technique needs a monotonic condition. For sums it requires non-negative numbers; negative values break the logic.
- Choose the window state to match the question: a sum, a count, a set of characters or a frequency map.
Look out of a train window. At every moment you see a frame of the landscape. As the train moves, one bit of scenery enters the frame at the front, and another bit leaves at the back. You do not re-photograph the whole view each second. You just update what changed.
That is the sliding window technique. For problems about contiguous ranges (subarrays and substrings), you keep a window [left, right] and a small summary of what is inside it: a running sum, a count, or a table of letter frequencies. When the window moves, you update the summary by adding the new element and removing the old one. Work that would be O(n) for each window becomes O(1), and the whole solution becomes O(n).
What you will learn
#- The difference between fixed-size and variable-size windows.
- The expand-and-shrink template.
- Why the technique is
O(n)even with a loop inside a loop. - How to solve the classic problems: maximum sum of size
k, smallest subarray with a given sum, longest substring without repeating characters, at mostkdistinct characters, and anagram search. - When sliding windows are valid, and when negative numbers break them.
Fixed-size windows
#In a fixed-size window, the length k is given. The window slides one step at a time: the new element enters on the right, and the element k positions back leaves on the left.
Problem: maximum sum of k consecutive elements
#Brute force. For each starting position, add up the next k elements: O(n × k).
Window. Compute the sum of the first k elements once. Then, for each next position, window = window + a[i] - a[i - k]. That is one addition and one subtraction per step.
Dry run on [2, 1, 5, 1, 3, 2] with k = 3:
Step | Window (positions) | Update | Sum |
|---|---|---|---|
start | 0 to 2 ( |
| 8 |
| 1 to 3 ( |
| 7 |
| 2 to 4 ( |
| 9 |
| 3 to 5 ( |
| 6 |
The maximum is 9, from 5, 1, 3.
// File: Main.javapublic class Main { // Brute force: O(n * k). static long maxSumBrute(int[] a, int k) { long best = Long.MIN_VALUE; for (int start = 0; start + k <= a.length; start++) { long sum = 0; for (int i = start; i < start + k; i++) { sum += a[i]; } best = Math.max(best, sum); } return best; } // Sliding window: O(n) time, O(1) space. static long maxSum(int[] a, int k) { if (k <= 0 || k > a.length) { throw new IllegalArgumentException("k must be between 1 and the array length"); } long window = 0; for (int i = 0; i < k; i++) { window += a[i]; // the first window } long best = window; for (int i = k; i < a.length; i++) { window += (long) a[i] - a[i - k]; // add the entering element, remove the leaving one best = Math.max(best, window); } return best; } public static void main(String[] args) { int[][] inputs = {{2, 1, 5, 1, 3, 2}, {1, 2, 3}, {-1, -2, -3, -4}, {Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE}, {4, 9, 1}}; int[] ks = {3, 3, 2, 2, 1}; for (int t = 0; t < inputs.length; t++) { System.out.println(maxSumBrute(inputs[t], ks[t]) + " " + maxSum(inputs[t], ks[t])); } try { maxSum(new int[] {1, 2}, 3); } catch (IllegalArgumentException e) { System.out.println("error: " + e.getMessage()); } }}9 96 6-3 -34294967294 42949672949 9error: k must be between 1 and the array lengthThe code checks the edge cases the problem leaves open: k larger than the array, and k of zero or less. It also uses long sums, because three copies of Integer.MAX_VALUE clearly do not fit in an int. The time is O(n), because every element is added once and subtracted once, and the space is O(1).
Variable-size windows: expand and shrink
#Often the window size is not given. You want the longest window that satisfies a condition, or the shortest one. The window grows and shrinks as you scan.
The standard template:
left = 0for right from 0 to n - 1: add a[right] to the window summary // expand while the window breaks the rule (or, for "shortest", while it meets the goal): remove a[left] from the window summary // shrink left = left + 1 update the best answer using right - left + 1- For "longest window that stays valid": expand always, shrink while the window is invalid, then record the length.
- For "shortest window that becomes valid": expand always, and while the window is valid, record its length and shrink.
Why is it O(n) with a while inside a for?
#Both left and right only ever move forward, and neither moves past n. The for loop moves right n times. The while loop moves left at most n times in total across the whole run, not per iteration. So the total number of pointer moves is at most 2n. This is the same amortised argument we used for the monotonic stack.
Problem: smallest subarray with sum at least a target
#Problem. Given an array of positive integers and a target, find the length of the shortest contiguous subarray whose sum is at least the target, or 0 if none exists. For [2, 3, 1, 2, 4, 3] and target 7, the answer is 2 (4, 3).
Expand right, adding to sum. While sum >= target, record the window length and shrink from the left, subtracting a[left].
Dry run on [2, 3, 1, 2, 4, 3] with target 7:
|
| While | Best length |
|---|---|---|---|
0 | 2 | no | none |
1 | 5 | no | none |
2 | 6 | no | none |
3 | 8 | record length 4 (positions 0 to 3), remove | 4 |
4 | 10 | record 4 (positions 1 to 4), remove | 3 |
5 | 9 | record 3 (positions 3 to 5), remove | 2 |
The answer is 2.
Why this requires positive numbers
#The logic "once the sum reaches the target, we can shrink, and shrinking makes the sum smaller" depends on every element being non-negative. Then adding an element never decreases the sum, and removing one never increases it, so the window behaves predictably: growing helps reach the target, shrinking moves away from it.
With negative numbers that breaks. Removing a negative element raises the sum. Adding a negative one lowers it. A window that is currently below the target might become valid after dropping a negative element on its left, but the algorithm only shrinks when the sum is already at least the target. Here is a tiny counterexample: [-2, 6] with target 6. The shortest valid subarray is [6], length 1. The window algorithm sums -2, then 4, never reaches 6, never shrinks, and reports "none".
// File: Main.javapublic class Main { // Positive numbers only. Returns 0 if there is no such subarray. static int minLengthWindow(int[] a, long target) { if (target <= 0) { throw new IllegalArgumentException("target must be positive"); } int best = Integer.MAX_VALUE; long sum = 0; int left = 0; for (int right = 0; right < a.length; right++) { sum += a[right]; while (sum >= target) { best = Math.min(best, right - left + 1); sum -= a[left]; left++; } } return best == Integer.MAX_VALUE ? 0 : best; } // Works for any numbers, but O(n^2). static int minLengthBrute(int[] a, long target) { int best = Integer.MAX_VALUE; for (int start = 0; start < a.length; start++) { long sum = 0; for (int end = start; end < a.length; end++) { sum += a[end]; if (sum >= target) { best = Math.min(best, end - start + 1); break; } } } return best == Integer.MAX_VALUE ? 0 : best; } public static void main(String[] args) { System.out.println(minLengthWindow(new int[] {2, 3, 1, 2, 4, 3}, 7) + " " + minLengthBrute(new int[] {2, 3, 1, 2, 4, 3}, 7)); System.out.println(minLengthWindow(new int[] {1, 1, 1, 1}, 11) + " " + minLengthWindow(new int[] {5}, 5) + " " + minLengthWindow(new int[] {}, 3)); System.out.println("with a negative number: window=" + minLengthWindow(new int[] {-2, 6}, 6) + " brute=" + minLengthBrute(new int[] {-2, 6}, 6)); }}2 20 1 0with a negative number: window=0 brute=1The last line shows the failure: the window answer 0 (no subarray) is wrong; the brute force correctly finds length 1. When numbers can be negative, sliding window sums are the wrong tool. Prefix sums with a hash map (next article) handle "sum equals k" with negatives, and a monotonic deque handles "sum at least k" with negatives, which is beyond this course.
Window state: sets, counts and frequency maps
#The window summary does not have to be a number. For substring problems you keep whatever tells you if the window is valid: a set, a count of distinct characters, or a table of letter frequencies.
Problem: longest substring without repeating characters
#Problem. Find the length of the longest substring in which all characters are different. For "abcabcbb" the answer is 3 ("abc").
Brute force. Check every substring: O(n^2) or worse.
Window. Expand right. If s[right] already appears in the window, the window is invalid, so move left just past the previous occurrence of that character. Keep a map from each character to the last index where we saw it.
The subtle point: the previous occurrence might lie to the left of the window, in which case it is stale and must be ignored. So we only move left if the previous index is >= left.
Dry run on "abba", where the stale case appears:
| Char | Previous index | Action |
| Window | Best |
|---|---|---|---|---|---|---|
0 |
| none | 0 |
| 1 | |
1 |
| none | 0 |
| 2 | |
2 |
| 1 (inside the window) |
| 2 |
| 2 |
3 |
| 0 (before | ignore, | 2 |
| 2 |
The answer is 2. If we had blindly set left = 0 + 1 = 1 at the last step, the window would be "bba", which contains a repeat, and the answer would be wrongly 3. We use that mistake as an exercise.
Problem: longest substring with at most k distinct characters
#Keep a frequency map for the window. Expand right, adding its character. While the map has more than k distinct keys, remove s[left] (decrease its count, and delete the key if it reaches zero) and advance left. The window is the longest valid one ending at right. For "eceba" with k = 2, the answer is 3 ("ece").
Problem: find all anagrams of a pattern in a text
#This is a fixed-size window of the pattern's length m, comparing letter counts. Maintain a count array for the window. At each step, add the entering character, remove the leaving one, and compare the window's counts with the pattern's counts. With 26 letters, the comparison is a constant 26 operations. For s = "cbaebabacd" and p = "abc", the answers are positions 0 ("cba") and 6 ("bac").
// File: Main.javaimport java.util.ArrayList;import java.util.Arrays;import java.util.HashMap;import java.util.List;import java.util.Map;public class Main { static int longestUnique(String s) { Map<Character, Integer> last = new HashMap<>(); // character -> last index seen int best = 0; int left = 0; for (int right = 0; right < s.length(); right++) { char c = s.charAt(right); Integer previous = last.get(c); if (previous != null && previous >= left) { // only a repeat inside the window counts left = previous + 1; } last.put(c, right); best = Math.max(best, right - left + 1); } return best; } static int longestAtMostKDistinct(String s, int k) { if (k == 0) { return 0; } Map<Character, Integer> counts = new HashMap<>(); int best = 0; int left = 0; for (int right = 0; right < s.length(); right++) { counts.merge(s.charAt(right), 1, Integer::sum); while (counts.size() > k) { // too many distinct characters: shrink char leaving = s.charAt(left); left++; if (counts.merge(leaving, -1, Integer::sum) == 0) { counts.remove(leaving); } } best = Math.max(best, right - left + 1); } return best; } // Lowercase letters only. Returns the start index of every anagram of p inside s. static List<Integer> findAnagrams(String s, String p) { List<Integer> result = new ArrayList<>(); int m = p.length(); if (m == 0 || m > s.length()) { return result; } int[] need = new int[26]; int[] window = new int[26]; for (char c : p.toCharArray()) { need[c - 'a']++; } for (int i = 0; i < s.length(); i++) { window[s.charAt(i) - 'a']++; // character enters if (i >= m) { window[s.charAt(i - m) - 'a']--; // character leaves } if (i >= m - 1 && Arrays.equals(window, need)) { result.add(i - m + 1); } } return result; } public static void main(String[] args) { String[] tests = {"abcabcbb", "bbbbb", "pwwkew", "", "abba"}; StringBuilder out = new StringBuilder(); for (String t : tests) { out.append(longestUnique(t)).append(' '); } System.out.println(out.toString().trim()); System.out.println(longestAtMostKDistinct("eceba", 2) + " " + longestAtMostKDistinct("aa", 1) + " " + longestAtMostKDistinct("abc", 0) + " " + longestAtMostKDistinct("", 3) + " " + longestAtMostKDistinct("abaccc", 2)); System.out.println(findAnagrams("cbaebabacd", "abc") + " " + findAnagrams("abab", "ab") + " " + findAnagrams("a", "ab")); }}3 1 3 0 23 2 0 0 4[0, 6] [0, 1, 2] []Complexity: each of these is O(n) time. longestUnique and longestAtMostKDistinct use O(min(n, alphabet)) extra space for the map, and the anagram search uses O(1) (two arrays of 26). The anagram window does O(26) work per step to compare arrays, a constant.
How to recognise a sliding window problem
#- The question is about a contiguous subarray or substring (not subsequences).
- You are asked for a longest, shortest, maximum, minimum or count of windows with some property.
- The property can be maintained incrementally when one element enters or leaves.
- The condition is monotonic: making the window bigger moves you consistently in one direction (more distinct characters, a larger sum of non-negatives), and shrinking moves you in the other.
If the problem is about all subarrays with an exact sum that may include negatives, or the property is not monotonic, think prefix sums and hashing instead.
Complexity summary
#Problem | Time | Extra space | Condition |
|---|---|---|---|
Max sum of size | O(n) | O(1) | Any numbers |
Smallest subarray with sum at least target | O(n) | O(1) | Non-negative numbers |
Longest substring without repeats | O(n) | O(min(n, alphabet)) | |
Longest with at most | O(n) | O(k) | |
All anagrams of | O(n) | O(1) for a 26-letter alphabet | Lowercase letters |
Minimum window substring (exercise) | O(n + m) | O(alphabet) |
Edge cases and common mistakes
#- Applying a sum window to arrays with negative numbers. The shrink logic breaks.
kbigger than the array, ork <= 0. Guard it.- Off-by-one in the window length. The length of
[left, right]isright - left + 1. - Stale entries in a last-seen map. Move
leftonly forward. Usemax(left, previous + 1)or checkprevious >= left. - Not removing zero counts from a frequency map, so that
map.size()overstates the number of distinct characters. - Forgetting to update the answer after shrinking (or before), depending on whether you maximise or minimise.
- Overflow. Running sums of
ints can overflow, so uselong. - Resetting state wrongly. The window summary must be updated for both the element entering and the element leaving.
- Confusing a substring with a subsequence. A window is always contiguous.
- Empty input. The loops handle it, but check that your return value (such as
0or an empty list) is what the problem expects.
Exercises
#Exercise 1
Trace
#For [1, 4, 2, 10, 2, 3, 1, 0, 20] and k = 4, write the window sum at each position. What is the maximum, and where is the window?
Exercise 2: Vowels in a window
#Given a lowercase string s and an integer k, return the maximum number of vowels (a, e, i, o, u) in any substring of length k. For "abciiidef" and k = 3 the answer is 3.
Exercise 3: Flip at most k zeros
#A binary array contains 0s and 1s. You may flip at most k zeros to ones. Return the length of the longest run of consecutive ones you can get. For [1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0] and k = 2, the answer is 6.
Exercise 4: Debug
#This version of "longest substring without repeating characters" gives the wrong answer for "abba". Explain why, and fix it.
static int longestUniqueBuggy(String s) { Map<Character, Integer> last = new HashMap<>(); int best = 0; int left = 0; for (int right = 0; right < s.length(); right++) { char c = s.charAt(right); if (last.containsKey(c)) { left = last.get(c) + 1; } last.put(c, right); best = Math.max(best, right - left + 1); } return best;}Exercise 5: Interview-style, the minimum window substring
#Given strings s and t, return the shortest substring of s that contains every character of t, including duplicates (if t has two as, the window needs at least two as). Return "" if there is none. For s = "ADOBECODEBANC" and t = "ABC", the answer is "BANC".
Show solutionHide solution
The array has 9 elements, so there are 9 - 4 + 1 = 6 windows.
Window (positions) | Elements | Sum |
|---|---|---|
0 to 3 |
| 17 |
1 to 4 |
| 18 |
2 to 5 |
| 17 |
3 to 6 |
| 16 |
4 to 7 |
| 6 |
5 to 8 |
| 24 |
The maximum is 24, at positions 5 to 8. With the sliding formula: start at 17, then 17 + 2 - 1 = 18, 18 + 3 - 4 = 17, 17 + 1 - 2 = 16, 16 + 0 - 10 = 6, 6 + 20 - 2 = 24. ✓
// File: Main.javapublic class Main { public static void main(String[] args) { int[] a = {1, 4, 2, 10, 2, 3, 1, 0, 20}; int k = 4; long window = 0; for (int i = 0; i < k; i++) { window += a[i]; } long best = window; int bestStart = 0; for (int i = k; i < a.length; i++) { window += a[i] - a[i - k]; if (window > best) { best = window; bestStart = i - k + 1; } } System.out.println(best + " starting at " + bestStart); }}24 starting at 5Exercise 2
Show solutionHide solution
Keep the count of vowels in the current window. When a character enters, add one if it is a vowel. When the window is longer than k, remove the character that leaves, subtracting one if it was a vowel. Track the maximum. Time O(n), space O(1).
// File: Main.javapublic class Main { static boolean isVowel(char c) { return c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u'; } static int maxVowels(String s, int k) { int count = 0; int best = 0; for (int i = 0; i < s.length(); i++) { if (isVowel(s.charAt(i))) { count++; } if (i >= k && isVowel(s.charAt(i - k))) { count--; // the character that just left the window } best = Math.max(best, count); } return best; } public static void main(String[] args) { System.out.println(maxVowels("abciiidef", 3)); System.out.println(maxVowels("aeiou", 2)); System.out.println(maxVowels("leetcode", 3)); System.out.println(maxVowels("rhythms", 4)); System.out.println(maxVowels("", 3)); }}32200When the string is shorter than k, the "window" never reaches full size, so the method returns the vowels in the whole string. Whether that is acceptable depends on the problem statement, and the function would otherwise reject such input, as maxSum did earlier.
Exercise 3
Show solutionHide solution
Turn the question into: the longest window that contains at most k zeros, because flipping those zeros makes the whole window ones. Count the zeros in the window. Expand right. If the zero count exceeds k, shrink from the left until it does not. Record the window length each time. Time O(n), space O(1).
// File: Main.javapublic class Main { static int longestOnes(int[] a, int k) { int zeros = 0; int best = 0; int left = 0; for (int right = 0; right < a.length; right++) { if (a[right] == 0) { zeros++; } while (zeros > k) { if (a[left] == 0) { zeros--; } left++; } best = Math.max(best, right - left + 1); } return best; } public static void main(String[] args) { System.out.println(longestOnes(new int[] {1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0}, 2)); System.out.println(longestOnes(new int[] {0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1}, 3)); System.out.println(longestOnes(new int[] {1, 1, 1}, 0)); System.out.println(longestOnes(new int[] {0, 0}, 0)); }}61030For the first input, one best window is positions 4 to 9: 0, 0, 1, 1, 1, 1 holds two zeros (which we flip) and four ones, so the run of ones has length 6. A condition such as "at most k of something" is a strong sign that a sliding window fits.
Exercise 4
Show solutionHide solution
The bug is left = last.get(c) + 1 with no check that the previous occurrence is inside the current window. In "abba", when we reach the final a, its previous index is 0, but left is already 2 because of the second b. The buggy code sets left = 1, moving left backwards and bringing the repeated b back inside the window. The window "bba" has length 3, so the method returns 3, but the correct answer is 2.
The fix: left must never move backwards. Use left = Math.max(left, last.get(c) + 1).
// File: Main.javaimport java.util.HashMap;import java.util.Map;public class Main { static int longestUniqueBuggy(String s) { Map<Character, Integer> last = new HashMap<>(); int best = 0; int left = 0; for (int right = 0; right < s.length(); right++) { char c = s.charAt(right); if (last.containsKey(c)) { left = last.get(c) + 1; } last.put(c, right); best = Math.max(best, right - left + 1); } return best; } static int longestUniqueFixed(String s) { Map<Character, Integer> last = new HashMap<>(); int best = 0; int left = 0; for (int right = 0; right < s.length(); right++) { char c = s.charAt(right); if (last.containsKey(c)) { left = Math.max(left, last.get(c) + 1); } last.put(c, right); best = Math.max(best, right - left + 1); } return best; } public static void main(String[] args) { System.out.println(longestUniqueBuggy("abba") + " " + longestUniqueFixed("abba")); System.out.println(longestUniqueBuggy("abcabcbb") + " " + longestUniqueFixed("abcabcbb")); }}3 23 3Both versions agree on "abcabcbb", which is why the bug is easy to miss. It only shows up when a repeated character's previous occurrence is already behind the window.
Exercise 5
Show solutionHide solution
State. Keep need[c], the number of copies of character c that the window still needs. Initially it is the count in t. Keep missing, the total number of required characters still not covered (initially t.length()).
Expand. When s[right] enters, if need[c] > 0 it fills one of the missing slots, so decrease missing. Always decrease need[c]. It can become negative, which means the window holds extra copies.
Shrink. While missing == 0 (the window contains everything needed), record the window if it is the shortest so far. Then remove s[left]: increase need[c] back, and if it becomes positive, we have lost a required character, so increase missing. Advance left.
Every character enters once and leaves at most once, so the time is O(|s| + |t|). Space is O(alphabet).
// File: Main.javapublic class Main { // Assumes ASCII characters. static String minWindow(String s, String t) { if (t.isEmpty() || t.length() > s.length()) { return ""; } int[] need = new int[128]; for (char c : t.toCharArray()) { need[c]++; } int missing = t.length(); int bestStart = 0; int bestLength = Integer.MAX_VALUE; int left = 0; for (int right = 0; right < s.length(); right++) { char in = s.charAt(right); if (need[in] > 0) { missing--; } need[in]--; while (missing == 0) { if (right - left + 1 < bestLength) { bestLength = right - left + 1; bestStart = left; } char out = s.charAt(left); need[out]++; if (need[out] > 0) { missing++; } left++; } } return bestLength == Integer.MAX_VALUE ? "" : s.substring(bestStart, bestStart + bestLength); } public static void main(String[] args) { System.out.println(minWindow("ADOBECODEBANC", "ABC")); System.out.println(minWindow("a", "a")); System.out.println("[" + minWindow("a", "aa") + "]"); System.out.println(minWindow("ab", "b")); System.out.println(minWindow("aa", "aa")); }}BANCa[]baaFor "ADOBECODEBANC", the window first becomes valid at "ADOBEC" (length 6). Shrinking and sliding on, the shortest valid window found is "BANC" (length 4). The array size 128 assumes ASCII; for other characters use a HashMap<Character, Integer>.
Where to go next
#- Next: Prefix Sums and Difference Arrays handles range sums and subarray-sum problems that sliding windows cannot, including negative numbers.
- Related: Two Pointers for the underlying idea, and Hashing for the frequency maps used here.
- Practice habit: when you see "longest" or "shortest" together with "substring" or "subarray", write down what the window must remember, and how it changes when an element enters and when one leaves.
Connected lessons
Linked from 5 guides
Other pages in the library that point here.
Keep reading
More in Technology →Guide · Technology
How to Learn Data Structures and Algorithms Without Burning Out
Guide · Career
The OA Landscape: Platforms, Formats, Proctoring and Rules
Guide · Career
A LinkedIn Profile That Gets Recruiter Messages

