Skip to content
Commitwise
Menu
Coding Interview

Sliding window

Turn "check every subarray" into one pass by sliding a window over the data, with fixed and variable windows, Longest Substring Without Repeating Characters and Sliding Window Maximum in Swift, Kotlin, Java and TypeScript.

intermediate

The core idea with one simple example.Step by step, with more examples and exercises.A cheat sheet: key APIs, gotchas and trade-offs.Internals, trade-offs and production pitfalls.

“What’s the highest total of 7 consecutive days of sales?” The direct answer adds up every block of 7 days separately: for a year of data, 359 blocks × 7 additions. The sliding-window answer adds the first block once, then for each next day adds the day coming in and subtracts the day going out: 2 operations per step, however long the window is.

Problems about contiguous parts of an array or string (subarrays, substrings, consecutive days) are very often sliding-windowSliding windowA technique that keeps a running result for a contiguous range and updates it as the range moves, instead of recomputing every subarray.Open in glossary → problems, and spotting that turns O(n·k) or O(n²) into O(n).

New to some of these? Open what you need, or skip ahead.

Basics:What are arrays and indexes?

An array is a row of values stored one after another: [5, 8, 2, 9]. Each position has an index, starting at 0: here 5 is at index 0 and 9 at index 3. The last index is always count - 1 (Swift) or size - 1 / length - 1 (Kotlin, Java, TypeScript).

Reading nums[2] is instant, whatever the array’s size. Reading an index that doesn’t exist (like nums[4] here) crashes with an “index out of range” error (in TypeScript it quietly gives undefined, a bug of its own), which is why loops usually go from 0 up to, but not including, the length.

Basics:What are lists, sets and maps?

Collections hold many values:

  • A list (Swift Array, Kotlin List, Java List, a TypeScript array) keeps items in order, and allows duplicates: ["Ana", "Ben", "Ana"].
  • A set holds each value at most once and answers “is this in it?” very quickly.
  • A map (Swift Dictionary, Kotlin, Java and TypeScript Map) stores values under keys: "Ana" → 30, looked up by the key.

Lists use indexes starting at 0: in ["a", "b", "c"], "a" is at index 0 and "c" at index 2.

Basics:What does O(n) mean?

Big-O describes how the amount of work grows with the size of the input n:

  • O(1): the same work whatever the size (reading array[5]).
  • O(log n): grows very slowly (about 20 steps for a million items).
  • O(n): grows in step with the input (one pass over the array).
  • O(n²): comparing every item with every other item, which gets slow fast.

You’ll see it next to every solution, for time and for extra space (memory). The Big-O tutorial explains it properly.

Part Not started

A fixed-size window

Picture a train window and a long row of houses. As the train moves, one house appears on the right and one disappears on the left. You don’t need to look at every house in the window again; you only update what changed.

A window is a range of the array between two indices, left and right:

  • Fixed size: the window always holds k elements. Each step, one element enters on the right and one leaves on the left.
  • Variable size: the window grows on the right while it’s valid, and shrinks from the left when it breaks a rule (too big a sum, a repeated character). You’re looking for the longest or shortest valid window.

A sliding window maintains an aggregate over a contiguous range [left, right] incrementally, so each element is added once and removed at most once: O(n) total, provided the aggregate supports O(1) (or O(log k)) insert and delete. Sums, counts and frequency maps do; maximums need a monotonic deque.

The variable-size version is only correct when the validity condition is monotonic: if a window is invalid, every larger window containing it is invalid too (and vice versa for “at least” conditions). That’s why it works for non-negative sums and character counts, and silently fails with negative numbers. The Detailed version covers that, the “exactly K = at most K − at most K−1” trick, and the deque for window maximums.

func maxSum(_ nums: [Int], _ k: Int) -> Int {
    var sum = nums[0..<k].reduce(0, +)      // the first window
    var best = sum
    for right in k..<nums.count {
        sum += nums[right] - nums[right - k] // one element in, one element out
        best = max(best, sum)
    }
    return best
}
fun maxSum(nums: IntArray, k: Int): Int {
    var sum = (0 until k).sumOf { nums[it] }   // the first window
    var best = sum
    for (right in k until nums.size) {
        sum += nums[right] - nums[right - k]   // one element in, one element out
        best = maxOf(best, sum)
    }
    return best
}
static int maxSum(int[] nums, int k) {
    int sum = 0;
    for (int i = 0; i < k; i++) sum += nums[i];   // the first window
    int best = sum;
    for (int right = k; right < nums.length; right++) {
        sum += nums[right] - nums[right - k];     // one element in, one element out
        best = Math.max(best, sum);
    }
    return best;
}
function maxSum(nums: number[], k: number): number {
  let sum = 0;
  for (let i = 0; i < k; i++) sum += nums[i];          // the first window
  let best = sum;
  for (let right = k; right < nums.length; right++) {
    sum += nums[right] - nums[right - k];              // one element in, one element out
    best = Math.max(best, sum);
  }
  return best;
}

The trick is in one line: sum += nums[right] - nums[right - k]. When the window moves one step right, the new element nums[right] comes in and the element k places back, nums[right - k], falls out. Each step costs the same, whether k is 3 or 3,000.

A window that loses the wrong element

Off-by-one errors love sliding windows. Here’s a version that adds and removes inside one loop. Which line is wrong?

Spot the bugA window that loses the wrong element

Tap the line with the bug.

func maxSum(_ nums: [Int], _ k: Int) -> Int {
    var sum = 0
    var best = Int.min
    for i in nums.indices {
        sum += nums[i]
        if i >= k { sum -= nums[i - k + 1] }
        if i >= k - 1 { best = max(best, sum) }
    }
    return best
}
fun maxSum(nums: IntArray, k: Int): Int {
    var sum = 0
    var best = Int.MIN_VALUE
    for (i in nums.indices) {
        sum += nums[i]
        if (i >= k) sum -= nums[i - k + 1]
        if (i >= k - 1) best = maxOf(best, sum)
    }
    return best
}
static int maxSum(int[] nums, int k) {
    int sum = 0;
    int best = Integer.MIN_VALUE;
    for (int i = 0; i < nums.length; i++) {
        sum += nums[i];
        if (i >= k) sum -= nums[i - k + 1];
        if (i >= k - 1) best = Math.max(best, sum);
    }
    return best;
}
function maxSum(nums: number[], k: number): number {
  let sum = 0;
  let best = -Infinity;
  for (let i = 0; i < nums.length; i++) {
    sum += nums[i];
    if (i >= k) sum -= nums[i - k + 1];
    if (i >= k - 1) best = Math.max(best, sum);
  }
  return best;
}

Predict the output

What gets printed?

print(maxSum([2, 1, 5, 1, 3, 2], 3))
println(maxSum(intArrayOf(2, 1, 5, 1, 3, 2), 3))
System.out.println(maxSum(new int[] {2, 1, 5, 1, 3, 2}, 3));
console.log(maxSum([2, 1, 5, 1, 3, 2], 3));

Window aggregates that aren’t sums

AggregateStructureCost per step
sum, counta numberO(1)
distinct values, anagram checka frequency map (or an IntArray(26)) plus a “matches” counterO(1)
max or mina monotonic deque of indicesO(1) amortized
median, k-th largesttwo heaps with lazy deletion, or an order-statistic treeO(log k)

The frequency map with a “how many characters currently match” counter is what makes Minimum Window Substring O(n): instead of comparing two maps every step (O(alphabet)), update the counter when a single character’s count crosses its required value.

Exercisehard

Sliding Window Maximum

Given an array nums and a window size k, return the maximum of each window as it slides from left to right.

nums = [1, 3, -1, -3, 5, 3, 6, 7], k = 3
windows: [1 3 -1] [3 -1 -3] [-1 -3 5] [-3 5 3] [5 3 6] [3 6 7]
output:  [3, 3, 5, 5, 6, 7]

The naive answer is O(n·k), and a heap gives O(n log n). Find an O(n) solution.

›

When a new element arrives, every element in the window that’s smaller than it can never be a window maximum again: the new one is bigger and will stay in the window longer.

›

Keep a deque of indices whose values decrease from front to back. Before adding index i, pop smaller-or-equal values from the back; drop the front if it has left the window. The front is always the current maximum.

SolutionTry it yourself firstTime O(n)Space O(k)
func maxSlidingWindow(_ nums: [Int], _ k: Int) -> [Int] {
    var result: [Int] = []
    var deque: [Int] = []          // indices; values decrease front to back
    var head = 0                   // the front, so removing from the front is O(1)
    for i in nums.indices {
        if head < deque.count && deque[head] <= i - k { head += 1 }
        while deque.count > head && nums[deque[deque.count - 1]] <= nums[i] { deque.removeLast() }
        deque.append(i)
        if i >= k - 1 { result.append(nums[deque[head]]) }
    }
    return result
}
fun maxSlidingWindow(nums: IntArray, k: Int): IntArray {
    val result = IntArray(nums.size - k + 1)
    val deque = ArrayDeque<Int>()                        // indices; values decrease front to back
    for (i in nums.indices) {
        if (deque.isNotEmpty() && deque.first() <= i - k) deque.removeFirst()   // left the window
        while (deque.isNotEmpty() && nums[deque.last()] <= nums[i]) deque.removeLast()
        deque.addLast(i)
        if (i >= k - 1) result[i - k + 1] = nums[deque.first()]
    }
    return result
}
int[] maxSlidingWindow(int[] nums, int k) {
    int[] result = new int[nums.length - k + 1];
    Deque<Integer> deque = new ArrayDeque<>();           // indices; values decrease front to back
    for (int i = 0; i < nums.length; i++) {
        if (!deque.isEmpty() && deque.peekFirst() <= i - k) deque.pollFirst();
        while (!deque.isEmpty() && nums[deque.peekLast()] <= nums[i]) deque.pollLast();
        deque.addLast(i);
        if (i >= k - 1) result[i - k + 1] = nums[deque.peekFirst()];
    }
    return result;
}
function maxSlidingWindow(nums: number[], k: number): number[] {
  const result: number[] = [];
  const deque: number[] = [];      // indices; values decrease front to back
  let head = 0;                    // the front, so we never call shift()
  for (let i = 0; i < nums.length; i++) {
    if (head < deque.length && deque[head] <= i - k) head++;
    while (deque.length > head && nums[deque[deque.length - 1]] <= nums[i]) deque.pop();
    deque.push(i);
    if (i >= k - 1) result.push(nums[deque[head]]);
  }
  return result;
}

Each index is pushed once and popped at most once, so the total is O(n) despite the inner while. Store indices, not values: you need them to know when the front has left the window, and they handle duplicates correctly. This monotonic deque is reusable: shortest subarray with sum at least K (with negatives), constrained subsequence sums, and jump-game DP with a window all use it. (The Swift and TypeScript versions never shrink their array, so for very long streams, compact it occasionally or use Deque from swift-collections.)

  • The aggregate must support O(1) (or O(log k)) add and remove: sums and counts directly, frequency maps with a “matches” counter, a monotonic deque of indices for max/min, two heaps for medians.
  • Check the leaving index by writing one window by hand: the window is right - k + 1 ... right, so right - k leaves.

Check this part

  1. In a fixed window of size k, which element leaves when right moves to a new index?

  2. Which problems usually call for a sliding window?

  3. What makes Sliding Window Maximum O(n)?

  4. Why does the monotonic deque store indices rather than values?

A fixed window of k elements moves one step at a time: add the element entering on the right, subtract the one leaving on the left (nums[right - k]). Every step costs the same, whatever k is.

Part Not started

A variable-size window

Problemmedium

Longest Substring Without Repeating Characters

Given a string s, find the length of the longest substring without repeating characters.

"abcabcbb"  → 3   ("abc")
"bbbbb"     → 1   ("b")
"pwwkew"    → 3   ("wke"; "pwke" isn't a substring, it skips a character)
""          → 0
›

Grow a window to the right one character at a time. When is the window no longer valid?

›

Keep the window’s characters in a set. When the new character is already in the set, remove characters from the left until it isn’t, then add it. The answer is the largest window size you ever see.

SolutionTry it yourself firstTime O(n)Space O(min(n, alphabet))

Expand right over every character. Before adding it, shrink from the left until the window has no copy of it. Each character enters the window once and leaves at most once, so the total work is O(n) even though there’s a loop inside the loop.

func lengthOfLongestSubstring(_ s: String) -> Int {
    let chars = Array(s)
    var window = Set<Character>()
    var left = 0
    var best = 0
    for right in chars.indices {
        while window.contains(chars[right]) {
            window.remove(chars[left])
            left += 1
        }
        window.insert(chars[right])
        best = max(best, right - left + 1)
    }
    return best
}
fun lengthOfLongestSubstring(s: String): Int {
    val window = HashSet<Char>()
    var left = 0
    var best = 0
    for (right in s.indices) {
        while (s[right] in window) {
            window.remove(s[left])
            left++
        }
        window.add(s[right])
        best = maxOf(best, right - left + 1)
    }
    return best
}
int lengthOfLongestSubstring(String s) {
    Set<Character> window = new HashSet<>();
    int left = 0, best = 0;
    for (int right = 0; right < s.length(); right++) {
        while (window.contains(s.charAt(right))) {
            window.remove(s.charAt(left));
            left++;
        }
        window.add(s.charAt(right));
        best = Math.max(best, right - left + 1);
    }
    return best;
}
function lengthOfLongestSubstring(s: string): number {
  const chars = [...s];                 // code points, so an emoji counts as one character
  const window = new Set<string>();
  let left = 0;
  let best = 0;
  for (let right = 0; right < chars.length; right++) {
    while (window.has(chars[right])) {
      window.delete(chars[left]);
      left++;
    }
    window.add(chars[right]);
    best = Math.max(best, right - left + 1);
  }
  return best;
}

Watching the window move

Here’s the variable window on "abcabcbb", step by step:

rightcharBefore addingWindow afterSizebest
0a—a11
1b—ab22
2c—abc33
3aa is in the window: remove abca33
4bb is in the window: remove bcab33
5cc is in the window: remove cabc33
6bremove a, then bcb23
7bremove c, then bb13

Notice that left only ever moves right. That’s why the inner while doesn’t make it O(n²): over the whole run, left moves at most n times in total.

Correct, and far too slow

Spot the bugCorrect, and far too slow

Tap the line with the bug.

func longestUnique(_ s: String) -> Int {
    let chars = Array(s)
    var left = 0
    var best = 0
    for right in chars.indices {
        while Set(chars[left...right]).count < right - left + 1 { left += 1 }
        best = max(best, right - left + 1)
    }
    return best
}
fun longestUnique(s: String): Int {
    var left = 0
    var best = 0
    for (right in s.indices) {
        while (s.substring(left, right + 1).toSet().size < right - left + 1) left++
        best = maxOf(best, right - left + 1)
    }
    return best
}
int longestUnique(String s) {
    int left = 0, best = 0;
    for (int right = 0; right < s.length(); right++) {
        while (s.substring(left, right + 1).chars().distinct().count() < right - left + 1) left++;
        best = Math.max(best, right - left + 1);
    }
    return best;
}
function longestUnique(s: string): number {
  let left = 0;
  let best = 0;
  for (let right = 0; right < s.length; right++) {
    while (new Set(s.slice(left, right + 1)).size < right - left + 1) left++;
    best = Math.max(best, right - left + 1);
  }
  return best;
}

  • O(n) holds only if the window’s state is updated incrementally (add the entering element, remove the leaving one). Rebuilding a set or substring per check makes it O(n·k).
  • For “longest substring without repeats”, a last-seen index per character lets left jump directly instead of stepping.

Swift

String can’t be indexed by Int: convert with Array(s), or work on s.utf8 for ASCII input.

TypeScript

s[i] and s.length count UTF-16 units; spread to code points with [...s] if emoji or other astral characters may appear.

Kotlin, Java

For lowercase ASCII input, an IntArray(26) of counts is faster than a HashMap<Char, Int>, with no boxing.

Check this part

  1. What does the window keep for Longest Substring Without Repeating Characters?

  2. The variable window has a while loop inside a for loop. Why is it still O(n)?

  3. The window loop is right, but each check builds a set from the current substring. What is the complexity?

A variable window grows on the right and shrinks from the left when it breaks a rule. Each element enters once and leaves at most once, so the inner while loop still gives O(n), as long as the window's state is updated, not rebuilt.

Part Not started

The variable-window recipe

Most variable-window problems fit the same four steps:

  1. Expand: move right one step and add nums[right] (or s[right]) to the window’s state: a sum, a count, a set.
  2. Check: is the window still valid? (No repeated character, sum below the limit, at most k zeros…)
  3. Shrink: while it’s invalid (or, for “shortest” problems, while it’s still valid), remove nums[left] from the state and move left right.
  4. Record: update the answer with the current window size.

The only things that change between problems are what state you keep and when the window is valid. For “longest”, you record after shrinking until valid; for “shortest”, you record inside the shrinking loop, while it’s still valid.

Exercisemedium

Minimum Size Subarray Sum

Given an array of positive integers nums and a positive integer target, return the length of the shortest contiguous subarray whose sum is at least target. If there is none, return 0.

target = 7,  nums = [2, 3, 1, 2, 4, 3]  → 2   ([4, 3])
target = 4,  nums = [1, 4, 4]           → 1   ([4])
target = 11, nums = [1, 1, 1, 1, 1]     → 0
›

This is a “shortest” problem: grow the window until its sum reaches the target, then try to make it smaller while it still reaches the target.

›

Inside the shrinking loop, first record the window size, then subtract nums[left] and move left. Start the answer at “infinity” and return 0 if it never changed.

SolutionTry it yourself firstTime O(n)Space O(1)
func minSubArrayLen(_ target: Int, _ nums: [Int]) -> Int {
    var left = 0
    var sum = 0
    var best = Int.max
    for right in nums.indices {
        sum += nums[right]
        while sum >= target {
            best = min(best, right - left + 1)
            sum -= nums[left]
            left += 1
        }
    }
    return best == Int.max ? 0 : best
}
fun minSubArrayLen(target: Int, nums: IntArray): Int {
    var left = 0
    var sum = 0
    var best = Int.MAX_VALUE
    for (right in nums.indices) {
        sum += nums[right]
        while (sum >= target) {
            best = minOf(best, right - left + 1)
            sum -= nums[left]
            left++
        }
    }
    return if (best == Int.MAX_VALUE) 0 else best
}
int minSubArrayLen(int target, int[] nums) {
    int left = 0, sum = 0, best = Integer.MAX_VALUE;
    for (int right = 0; right < nums.length; right++) {
        sum += nums[right];
        while (sum >= target) {
            best = Math.min(best, right - left + 1);
            sum -= nums[left];
            left++;
        }
    }
    return best == Integer.MAX_VALUE ? 0 : best;
}
function minSubArrayLen(target: number, nums: number[]): number {
  let left = 0;
  let sum = 0;
  let best = Infinity;
  for (let right = 0; right < nums.length; right++) {
    sum += nums[right];
    while (sum >= target) {
      best = Math.min(best, right - left + 1);
      sum -= nums[left];
      left++;
    }
  }
  return best === Infinity ? 0 : best;
}

On [2, 3, 1, 2, 4, 3]: the window first reaches 8 at [2, 3, 1, 2] (length 4), shrinks to [3, 1, 2] (6, too small), grows to [3, 1, 2, 4] (10), shrinks to [1, 2, 4] and [2, 4] (length 3, then 6 is too small)… and finally finds [4, 3], length 2. The word positive in the problem statement is what makes this work: adding an element always increases the sum, and removing one always decreases it.

When the window lies: monotonicity

Shrinking from the left is only safe when removing an element can only move the window in one direction: a smaller sum, fewer repeats. With non-negative numbers, the sum is monotonic in both directions, so the greedy shrink is correct. With negative numbers it isn’t:

Predict the output

minSubArrayLen from the exercise, called with target 5 on [1, -1, 5]. What does it return?

print(minSubArrayLen(5, [1, -1, 5]))
println(minSubArrayLen(5, intArrayOf(1, -1, 5)))
System.out.println(minSubArrayLen(5, new int[] {1, -1, 5}));
console.log(minSubArrayLen(5, [1, -1, 5]));

Exactly K from at most K

“Count subarrays with exactly K distinct values” isn’t monotonic (adding an element can make the count right or wrong), but “at most K” is. So:

exactly(K) = atMost(K) − atMost(K − 1)

where atMost(K) counts subarrays ending at each right as right - left + 1 after shrinking until the window has at most K distinct values. The same trick handles “exactly K odd numbers”, “binary subarrays with sum K” and similar counting questions, each in O(n).

Deep dives

Deep diveKadane's algorithm: when the window can't shrinkInterview favorite

Maximum Subarray (the largest sum of a contiguous subarray) looks like a window problem, but with negative numbers shrinking isn’t safe. Kadane’s algorithm keeps one number instead: the best sum of a subarray ending here.

At each element, either extend the previous subarray or start fresh: current = max(x, current + x), and best = max(best, current). One pass, O(1) space. It’s really dynamic programming with a one-value state, and interviewers like it because “extend or restart” is the whole insight.

TypeShrink conditionRecordExamples
Fixed size kalways remove nums[right - k] once right ≥ kevery step after the first windowmax sum/average of k, anagrams in a string
Longest validwhile invalidafter shrinkinglongest substring without repeats, at most K distinct, max consecutive ones with K flips
Shortest validwhile still validinside the shrink loopminimum size subarray sum, minimum window substring
Count “exactly K”—atMost(K) − atMost(K − 1)subarrays with K distinct, K odd numbers
Window max/min—monotonic deque frontsliding window maximum

Requires a monotonic validity condition (non-negative numbers, counts). Gotchas: off-by-one on the leaving element (right - k), rebuilding state instead of updating it, negative numbers, Swift String indexing (use Array(s) or utf8), TypeScript strings indexed by UTF-16 unit (spread with [...s]).

var left = 0, best = 0
for right in nums.indices {
    add(nums[right])
    while !isValid() { remove(nums[left]); left += 1 }
    best = max(best, right - left + 1)
}
var left = 0
var best = 0
for (right in nums.indices) {
    add(nums[right])
    while (!isValid()) { remove(nums[left]); left++ }
    best = maxOf(best, right - left + 1)
}
int left = 0, best = 0;
for (int right = 0; right < nums.length; right++) {
    add(nums[right]);
    while (!isValid()) { remove(nums[left]); left++; }
    best = Math.max(best, right - left + 1);
}
let left = 0;
let best = 0;
for (let right = 0; right < nums.length; right++) {
  add(nums[right]);
  while (!isValid()) { remove(nums[left]); left++; }
  best = Math.max(best, right - left + 1);
}

Check this part

  1. For "shortest subarray with sum at least target", where do you record the answer?

  2. What changes from one variable-window problem to the next?

  3. Why does the shrinking window fail for minimum subarray sum with negative numbers?

  4. How do you count subarrays with exactly K distinct values in O(n)?

Expand, check, shrink, record: only the state and the validity rule change between problems. Shortest-window problems record inside the shrink loop, and the shrink is only correct when validity is monotonic, which negative numbers break.

Key takeaways

  • A sliding window covers a contiguous range and updates it as it moves, instead of recomputing every subarray.
  • Fixed windows add the entering element and subtract the leaving one (nums[right - k]).
  • Variable windows grow on the right and shrink from the left; left only moves forward, so it’s O(n).
  • Record the answer after shrinking for “longest” and inside the shrink loop for “shortest”.
  • The greedy shrink requires a monotonic validity condition; negative numbers need prefix sums instead.
  • Update window state incrementally; rebuilding it per step silently makes the algorithm O(n·k).
  • Count “exactly K” as atMost(K) − atMost(K − 1).
  • Window max/min with a monotonic deque of indices is O(n) and a reusable building block.

Finished this page?Nice work, this page is complete!

Mark it as done to track your progress. It's saved in this browser, no account needed.

Its cards are now in your Bites review ⚡
Esc

Type to search the whole site.