Sliding windows
A sliding window keeps a contiguous range [left, right] and updates its state incrementally as the edges move. Fixed windows slide one step at a time; variable windows grow on the right and shrink on the left.
Learning goals
- Slide a fixed-size window in O(n) instead of O(n · k)
- Grow and shrink a variable window while it stays valid
- Count subarrays with “at most K” and derive “exactly K” by subtraction
- Find the smallest window that covers a requirement
Prerequisites
To find your busiest hour of website traffic minute by minute, you do not re-add sixty numbers for each hour. You add the newest minute and subtract the oldest.
Fixed-size windows
For windows of exactly k elements, compute the first window, then for each step add the entering element and remove the leaving one. Each step is O(1), so the whole scan is O(n) no matter how large k is.
The window state can be anything that supports add and remove: a sum, a frequency map, or a count of vowels.
Worst
O(n)
Space
O(1)
Fixed-size window: maximum sum of k consecutive values
Interactive · edit the inputint[] arr · length 9
The first window [0..2] sums to 8. That costs k additions, once.
1public static int maxWindowSum(int[] arr, int k) {2int sum = 0;3for (int i = 0; i < k; i++) sum += arr[i]; // first window4int best = sum;5for (int right = k; right < arr.length; right++) {6sum += arr[right] - arr[right - k]; // add the new value, drop the old one7best = Math.max(best, sum);8}9return best;10}
Live state
k
3
sum
8
best
8
Execution trace (dry run table)Show 8 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | k=3 sum=8 best=8 | The first window [0..2] sums to 8. That costs k additions, once. |
| 2 | L6 | k=3 sum=7 best=8 | Slide right: + arr[3] (1) − arr[0] (2) = 7. Two operations instead of re-adding k values. |
| 3 | L6 | k=3 sum=9 best=9 | Slide right: + arr[4] (3) − arr[1] (1) = 9. Two operations instead of re-adding k values. New best. |
| 4 | L6 | k=3 sum=6 best=9 | Slide right: + arr[5] (2) − arr[2] (5) = 6. Two operations instead of re-adding k values. |
| 5 | L6 | k=3 sum=12 best=12 | Slide right: + arr[6] (7) − arr[3] (1) = 12. Two operations instead of re-adding k values. New best. |
| 6 | L6 | k=3 sum=10 best=12 | Slide right: + arr[7] (1) − arr[4] (3) = 10. Two operations instead of re-adding k values. |
| 7 | L6 | k=3 sum=12 best=12 | Slide right: + arr[8] (4) − arr[5] (2) = 12. Two operations instead of re-adding k values. |
| 8 | L9 | return=12 | return 12, from window [4..6]. O(n) time instead of O(n · k). |
Variable-size windows
A variable window grows by moving right and shrinks by moving left. It works when the condition is monotonic: adding an element can only push the window toward invalid (or toward valid), and removing one moves it the other way. Positive numbers and sum constraints have this property.
Each index enters the window once and leaves at most once, so even with a while loop inside the for loop the total work is O(n).
Variable window: shortest subarray with sum ≥ target
Interactive · edit the inputint[] arr · length 6
Positive values make the window sum grow when right moves and shrink when left moves. That monotonicity is what lets a window work.
1public static int minSubArrayLen(int target, int[] arr) { // all values positive2int left = 0, sum = 0, best = Integer.MAX_VALUE;3for (int right = 0; right < arr.length; right++) {4sum += arr[right]; // grow the window5while (sum >= target) { // valid: try to shrink it6best = Math.min(best, right - left + 1);7sum -= arr[left++];8}9}10return best == Integer.MAX_VALUE ? 0 : best;11}
Live state
target
7
left
0
sum
0
best
∞
Execution trace (dry run table)Show 18 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | target=7 left=0 sum=0 best=∞ | Positive values make the window sum grow when right moves and shrink when left moves. That monotonicity is what lets a window work. |
| 2 | L4 | target=7 left=0 right=0 sum=2 best=∞ | Add arr[0] = 2; window sum is 2. |
| 3 | L4 | target=7 left=0 right=1 sum=5 best=∞ | Add arr[1] = 3; window sum is 5. |
| 4 | L4 | target=7 left=0 right=2 sum=6 best=∞ | Add arr[2] = 1; window sum is 6. |
| 5 | L4 | target=7 left=0 right=3 sum=8 best=∞ | Add arr[3] = 2; window sum is 8. |
| 6 | L6 | target=7 left=0 right=3 sum=8 best=4 | [0..3] reaches the target with length 4. Best so far: 4. Now try a shorter window. |
| 7 | L7 | target=7 left=1 right=3 sum=6 best=4 | Drop arr[0]; sum is 6. |
| 8 | L4 | target=7 left=1 right=4 sum=10 best=4 | Add arr[4] = 4; window sum is 10. |
| 9 | L6 | target=7 left=1 right=4 sum=10 best=4 | [1..4] reaches the target with length 4. Best so far: 4. Now try a shorter window. |
| 10 | L7 | target=7 left=2 right=4 sum=7 best=4 | Drop arr[1]; sum is 7. |
| 11 | L6 | target=7 left=2 right=4 sum=7 best=3 | [2..4] reaches the target with length 3. Best so far: 3. Now try a shorter window. |
| 12 | L7 | target=7 left=3 right=4 sum=6 best=3 | Drop arr[2]; sum is 6. |
| 13 | L4 | target=7 left=3 right=5 sum=9 best=3 | Add arr[5] = 3; window sum is 9. |
| 14 | L6 | target=7 left=3 right=5 sum=9 best=3 | [3..5] reaches the target with length 3. Best so far: 3. Now try a shorter window. |
| 15 | L7 | target=7 left=4 right=5 sum=7 best=3 | Drop arr[3]; sum is 7. |
| 16 | L6 | target=7 left=4 right=5 sum=7 best=2 | [4..5] reaches the target with length 2. Best so far: 2. Now try a shorter window. |
| 17 | L7 | target=7 left=5 right=5 sum=3 best=2 | Drop arr[4]; sum is 3. |
| 18 | L10 | return=2 | return 2. Each index entered and left the window at most once: 11 pointer moves, O(n). |
At-most-K constraints
For “at most K distinct values” or “at most K zeros”, keep counts inside the window. When the window breaks the limit, shrink from the left until it is valid again, then record the answer for this right.
Shrinking is safe because any window that contains the invalid one is also invalid, so no answer is skipped.
At most K distinct: longest valid window
Interactive · edit the inputint[] arr · length 8
The map counts each value inside the window; its size is the number of distinct values.
1public static int longestAtMostK(int[] arr, int k) {2Map<Integer, Integer> count = new HashMap<>();3int left = 0, best = 0;4for (int right = 0; right < arr.length; right++) {5count.merge(arr[right], 1, Integer::sum);6while (count.size() > k) { // too many distinct values7if (count.merge(arr[left], -1, Integer::sum) == 0) count.remove(arr[left]);8left++;9}10best = Math.max(best, right - left + 1);11}12return best;13}
Live state
k
2
Execution trace (dry run table)Show 23 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | k=2 | The map counts each value inside the window; its size is the number of distinct values. |
| 2 | L5 | k=2 left=0 right=0 distinct=1 best=0 | Add 1. The window has 1 distinct value. |
| 3 | L10 | k=2 left=0 right=0 best=1 | [0..0] is valid with length 1. Best is 1. |
| 4 | L5 | k=2 left=0 right=1 distinct=2 best=1 | Add 2. The window has 2 distinct values. |
| 5 | L10 | k=2 left=0 right=1 best=2 | [0..1] is valid with length 2. Best is 2. |
| 6 | L5 | k=2 left=0 right=2 distinct=2 best=2 | Add 1. The window has 2 distinct values. |
| 7 | L10 | k=2 left=0 right=2 best=3 | [0..2] is valid with length 3. Best is 3. |
| 8 | L5 | k=2 left=0 right=3 distinct=2 best=3 | Add 2. The window has 2 distinct values. |
| 9 | L10 | k=2 left=0 right=3 best=4 | [0..3] is valid with length 4. Best is 4. |
| 10 | L5 | k=2 left=0 right=4 distinct=3 best=4 | Add 3. The window has 3 distinct values. |
| 11 | L7 | k=2 left=1 right=4 distinct=3 | Too many distinct values: drop 1 from the left. |
| 12 | L7 | k=2 left=2 right=4 distinct=3 | Too many distinct values: drop 2 from the left. |
| 13 | L7 | k=2 left=3 right=4 distinct=2 | Too many distinct values: drop 1 from the left, which removes 1 from the window entirely. |
| 14 | L10 | k=2 left=3 right=4 best=4 | [3..4] is valid with length 2. Best is 4. |
| 15 | L5 | k=2 left=3 right=5 distinct=2 best=4 | Add 2. The window has 2 distinct values. |
| 16 | L10 | k=2 left=3 right=5 best=4 | [3..5] is valid with length 3. Best is 4. |
| 17 | L5 | k=2 left=3 right=6 distinct=2 best=4 | Add 2. The window has 2 distinct values. |
| 18 | L10 | k=2 left=3 right=6 best=4 | [3..6] is valid with length 4. Best is 4. |
| 19 | L5 | k=2 left=3 right=7 distinct=3 best=4 | Add 4. The window has 3 distinct values. |
| 20 | L7 | k=2 left=4 right=7 distinct=3 | Too many distinct values: drop 2 from the left. |
| 21 | L7 | k=2 left=5 right=7 distinct=2 | Too many distinct values: drop 3 from the left, which removes 3 from the window entirely. |
| 22 | L10 | k=2 left=5 right=7 best=4 | [5..7] is valid with length 3. Best is 4. |
| 23 | L12 | return=4 | return 4, window [0..3]. Each index enters and leaves once: O(n). |
Exact-K via subtraction
“Exactly K” is not monotonic: a window with exactly K distinct values may grow into one with K + 1, then shrink back to K. A single window cannot track that cleanly.
Instead count windows with at most K and subtract those with at most K − 1. For each right, the number of valid subarrays ending there is right - left + 1, so each count is a single O(n) pass.
Exactly K distinct = atMost(K) − atMost(K − 1)
Interactive · edit the inputint[] arr · length 5
Counting “exactly 2” directly is awkward for a window, because shrinking can skip past valid states. Count “at most 2” and subtract “at most 1”.
1public static int exactlyK(int[] arr, int k) {2return atMost(arr, k) - atMost(arr, k - 1);3}45static int atMost(int[] arr, int k) {6Map<Integer, Integer> count = new HashMap<>();7int left = 0, total = 0;8for (int right = 0; right < arr.length; right++) {9count.merge(arr[right], 1, Integer::sum);10while (count.size() > k) {11if (count.merge(arr[left], -1, Integer::sum) == 0) count.remove(arr[left]);12left++;13}14total += right - left + 1; // subarrays ending at right15}16return total;17}
Live state
k
2
Execution trace (dry run table)Show 32 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | k=2 | Counting “exactly 2” directly is awkward for a window, because shrinking can skip past valid states. Count “at most 2” and subtract “at most 1”. |
| 2 | L5 | k=2 | Pass 1: atMost(arr, 2). |
| 3 | L9 | k=2 left=0 right=0 distinct=1 total=0 | Add 1. The window has 1 distinct value. |
| 4 | L14 | k=2 left=0 right=0 total=1 | Every subarray ending at 0 and starting in [0..0] is valid: 1 subarray. Total 1. |
| 5 | L9 | k=2 left=0 right=1 distinct=2 total=1 | Add 2. The window has 2 distinct values. |
| 6 | L14 | k=2 left=0 right=1 total=3 | Every subarray ending at 1 and starting in [0..1] is valid: 2 subarrays. Total 3. |
| 7 | L9 | k=2 left=0 right=2 distinct=2 total=3 | Add 1. The window has 2 distinct values. |
| 8 | L14 | k=2 left=0 right=2 total=6 | Every subarray ending at 2 and starting in [0..2] is valid: 3 subarrays. Total 6. |
| 9 | L9 | k=2 left=0 right=3 distinct=2 total=6 | Add 2. The window has 2 distinct values. |
| 10 | L14 | k=2 left=0 right=3 total=10 | Every subarray ending at 3 and starting in [0..3] is valid: 4 subarrays. Total 10. |
| 11 | L9 | k=2 left=0 right=4 distinct=3 total=10 | Add 3. The window has 3 distinct values. |
| 12 | L11 | k=2 left=1 right=4 distinct=3 | Too many distinct values: drop 1 from the left. |
| 13 | L11 | k=2 left=2 right=4 distinct=3 | Too many distinct values: drop 2 from the left. |
| 14 | L11 | k=2 left=3 right=4 distinct=2 | Too many distinct values: drop 1 from the left, which removes 1 from the window entirely. |
| 15 | L14 | k=2 left=3 right=4 total=12 | Every subarray ending at 4 and starting in [3..4] is valid: 2 subarrays. Total 12. |
| 16 | L16 | atMost(2)=12 | atMost(2) = 12. |
| 17 | L5 | k=1 atMost(2)=12 | Pass 2: atMost(arr, 1). |
| 18 | L9 | k=1 left=0 right=0 distinct=1 total=0 | Add 1. The window has 1 distinct value. |
| 19 | L14 | k=1 left=0 right=0 total=1 | Every subarray ending at 0 and starting in [0..0] is valid: 1 subarray. Total 1. |
| 20 | L9 | k=1 left=0 right=1 distinct=2 total=1 | Add 2. The window has 2 distinct values. |
| 21 | L11 | k=1 left=1 right=1 distinct=1 | Too many distinct values: drop 1 from the left, which removes 1 from the window entirely. |
| 22 | L14 | k=1 left=1 right=1 total=2 | Every subarray ending at 1 and starting in [1..1] is valid: 1 subarray. Total 2. |
| 23 | L9 | k=1 left=1 right=2 distinct=2 total=2 | Add 1. The window has 2 distinct values. |
| 24 | L11 | k=1 left=2 right=2 distinct=1 | Too many distinct values: drop 2 from the left, which removes 2 from the window entirely. |
| 25 | L14 | k=1 left=2 right=2 total=3 | Every subarray ending at 2 and starting in [2..2] is valid: 1 subarray. Total 3. |
| 26 | L9 | k=1 left=2 right=3 distinct=2 total=3 | Add 2. The window has 2 distinct values. |
| 27 | L11 | k=1 left=3 right=3 distinct=1 | Too many distinct values: drop 1 from the left, which removes 1 from the window entirely. |
| 28 | L14 | k=1 left=3 right=3 total=4 | Every subarray ending at 3 and starting in [3..3] is valid: 1 subarray. Total 4. |
| 29 | L9 | k=1 left=3 right=4 distinct=2 total=4 | Add 3. The window has 2 distinct values. |
| 30 | L11 | k=1 left=4 right=4 distinct=1 | Too many distinct values: drop 2 from the left, which removes 2 from the window entirely. |
| 31 | L14 | k=1 left=4 right=4 total=5 | Every subarray ending at 4 and starting in [4..4] is valid: 1 subarray. Total 5. |
| 32 | L2 | atMost(2)=12 atMost(1)=5 return=7 | return 12 − 5 = 7 subarrays with exactly 2 distinct values. Two O(n) passes. |
Minimum covering window
Minimum window problems (shortest substring containing all required characters) expand right until the window covers the requirement, then shrink left while it still covers it, recording the best length each time.
Track how many requirements are satisfied, not just the counts, so checking “is the window valid?” stays O(1). The animation finds the shortest window containing every distinct value of the array.
1static String minWindow(String s, String t) {2int[] need = new int[128];3for (char c : t.toCharArray()) need[c]++;4int missing = t.length(), left = 0, bestL = 0, bestLen = Integer.MAX_VALUE;5for (int right = 0; right < s.length(); right++) {6if (need[s.charAt(right)]-- > 0) missing--; // this char was still required7while (missing == 0) {8if (right - left + 1 < bestLen) { bestLen = right - left + 1; bestL = left; }9if (++need[s.charAt(left++)] > 0) missing++; // removing it breaks coverage10}11}12return bestLen == Integer.MAX_VALUE ? "" : s.substring(bestL, bestL + bestLen);13}
Worst
O(n + m)
Space
O(alphabet)
Minimum window covering every distinct value
Interactive · edit the inputint[] arr · length 8
The array contains 3 distinct values. Find the shortest window containing all of them.
1public static int[] minWindowAllDistinct(int[] arr) {2int need = (int) Arrays.stream(arr).distinct().count();3Map<Integer, Integer> count = new HashMap<>();4int left = 0, bestLen = Integer.MAX_VALUE, bestL = 0;5for (int right = 0; right < arr.length; right++) {6count.merge(arr[right], 1, Integer::sum);7while (count.size() == need) { // window covers every value8if (right - left + 1 < bestLen) { bestLen = right - left + 1; bestL = left; }9if (count.merge(arr[left], -1, Integer::sum) == 0) count.remove(arr[left]);10left++;11}12}13return new int[] { bestL, bestL + bestLen - 1 };14}
Live state
need
3
Execution trace (dry run table)Show 20 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | need=3 | The array contains 3 distinct values. Find the shortest window containing all of them. |
| 2 | L6 | need=3 have=1 best=∞ | Add 2. The window has 1 of 3 values. |
| 3 | L6 | need=3 have=2 best=∞ | Add 1. The window has 2 of 3 values. |
| 4 | L6 | need=3 have=2 best=∞ | Add 1. The window has 2 of 3 values. |
| 5 | L6 | need=3 have=3 best=∞ | Add 3. The window has 3 of 3 values. |
| 6 | L8 | need=3 have=3 best=4 | [0..3] covers everything (length 4). Shrink from the left to look for a shorter one. |
| 7 | L10 | need=3 have=2 best=4 | Dropping 2 loses the last copy of 2: the window no longer covers everything. |
| 8 | L6 | need=3 have=3 best=4 | Add 2. The window has 3 of 3 values. |
| 9 | L8 | need=3 have=3 best=4 | [1..4] covers everything (length 4). Shrink from the left to look for a shorter one. |
| 10 | L10 | need=3 have=3 best=4 | Drop 1; another copy is still inside. |
| 11 | L8 | need=3 have=3 best=3 | [2..4] covers everything (length 3). Shrink from the left to look for a shorter one. |
| 12 | L10 | need=3 have=2 best=3 | Dropping 1 loses the last copy of 1: the window no longer covers everything. |
| 13 | L6 | need=3 have=3 best=3 | Add 1. The window has 3 of 3 values. |
| 14 | L8 | need=3 have=3 best=3 | [3..5] covers everything (length 3). Shrink from the left to look for a shorter one. |
| 15 | L10 | need=3 have=2 best=3 | Dropping 3 loses the last copy of 3: the window no longer covers everything. |
| 16 | L6 | need=3 have=2 best=3 | Add 1. The window has 2 of 3 values. |
| 17 | L6 | need=3 have=3 best=3 | Add 3. The window has 3 of 3 values. |
| 18 | L8 | need=3 have=3 best=3 | [4..7] covers everything (length 4). Shrink from the left to look for a shorter one. |
| 19 | L10 | need=3 have=2 best=3 | Dropping 2 loses the last copy of 2: the window no longer covers everything. |
| 20 | L13 | return=[2, 4] | Shortest covering window is [2..4] (length 3). O(n) with a map. |
Practice ladder
Now try it yourself
Easy
- Maximum average subarray I
- Contains duplicate II (window of size k)
Medium
- Longest substring without repeating characters
- Minimum size subarray sum
- Longest repeating character replacement
- Permutation in string
Advanced
- Minimum window substring
- Subarrays with K different integers
- Sliding window median
Key takeaways
- Fixed windows add one element and remove one per step: O(n).
- Variable windows need a monotonic condition; each index enters and leaves at most once.
- Exactly K = atMost(K) − atMost(K − 1); covering windows expand until valid, then shrink.