DSA Notes · chapter 07 · Java Live Code

A window is a contiguous slice [left, right] that only grows on the right and only shrinks on the left. Each index enters and leaves at most once, so the hot loop is linear. The four cards below are the four invariants you actually type: no-repeat last-seen, fixed anagram counts, exactly-K by subtraction, and a decreasing deque of indices for the window max.

← Binary Search · Two Pointers →

07 · sliding window

Four windows, four invariants

Read the simple Java until the dry-run table is obvious, then type the interview block. Notebook links at the bottom of each card are other candidates’ families, not a Job 10454435 list. Rotten oranges is multi-source BFS — it is not a window, even when the story talks about “minutes spreading.”

Longest substring without repeating characters

Recognize it from

Longest (or shortest) contiguous substring where every character is unique. Wording like “no duplicate in the window,” “at most one of each,” or “longest valid slice under a shrinking rule.” If they allow holes, it is a subsequence and this loop is the wrong tool.

What it actually does

right walks every index once and always includes s.charAt(right). The window [left, right] is the longest suffix of the prefix s[0..right] that has unique characters. A HashMap stores the last index where each character appeared. When the character at right was already inside the current window, left jumps to one past that last index.

The jump is legal because any start between the old left and that last index still contains the duplicate. Characters whose last index is before left are already outside the window — their map entry is stale. That is why the interview block guards with lastSeen >= left instead of blindly setting left = lastSeen + 1.

Simple — read this first

public int lengthOfLongestSubstring(String s) {
    Map<Character, Integer> last = new HashMap<>();
    int left = 0, best = 0;
    for (int right = 0; right < s.length(); right++) {
        char c = s.charAt(right);
        // last.getOrDefault(..., -1)+1 is 0 when c is new, so max keeps left still
        left = Math.max(left, last.getOrDefault(c, -1) + 1);
        last.put(c, right);
        best = Math.max(best, right - left + 1);
    }
    return best;
}

Interview — type this

public int lengthOfLongestSubstring(String s) {
    if (s == null || s.isEmpty()) return 0;
    Map<Character, Integer> last = new HashMap<>();
    int left = 0, best = 0;
    for (int right = 0; right < s.length(); right++) {
        char c = s.charAt(right);
        Integer lastSeen = last.get(c);
        // stale lastSeen < left is outside the window — do not pull left backward
        if (lastSeen != null && lastSeen >= left) {
            left = lastSeen + 1;
        }
        last.put(c, right);
        best = Math.max(best, right - left + 1);
    }
    return best;
}
Example 1 — simple

Input s = "abca". Four characters, one repeat at the end.

index 0 1 2 3 s a b c a
Dry-run — HashMap last-seen, left only moves forward
rightclastSeenleft aftermapwindowbest
0a—0a:0a1
1b—0a:0 b:1ab2
2c—0a:0 b:1 c:2abc3
3a0 (≥ left)1a:3 b:1 c:2bca3

Answer 3 — windows abc and bca both have length 3.

Example 2 — every step

Input s = "abcabcbb". Watch left jump on every repeat, including the two trailing bs.

index 0 1 2 3 4 5 6 7 s a b c a b c b b
Every right — lastSeen ≥ left is the only time left jumps
rightclastSeenlastSeen ≥ left?leftwindowbest
0a—no0a1
1b—no0ab2
2c—no0abc3
3a0yes → left=11bca3
4b1yes → left=22cab3
5c2yes → left=33abc3
6b4yes → left=55cb3
7b6yes → left=77b3

Answer 3. The interesting jump is right=7: last b is at 6, which is still inside [5,6], so left becomes 7. If you wrote left = lastSeen + 1 with no guard on a string like "abba", left would move backward when a stale a is seen after the window has already passed it.

TC O(n) SC O(Σ) alphabet
Why this map Last-seen index lets you jump left in O(1) instead of walking it with a while-loop plus a set. The while-loop is also O(n) total, but the guard is the thing people miss. Unicode or mixed case: ask; int[128] is enough for ASCII.
Common mistakes
  • Setting left = lastSeen + 1 when lastSeen < left — left moves backward and the window can contain a duplicate.
  • Storing frequencies and forgetting to delete a key when the count hits 0 (set version).
  • Treating “longest consecutive values” (HashSet of numbers, order does not matter) as this window. Consecutive-in-value is not contiguous-in-index.

Notebook problems for this topic

ProblemPatternLinks
Longest / shortest valid contiguous slice Variable window; left only advances clue · clue
Longest repeating character replacement (variation) Variable window + max frequency; at most k replacements cheat card
Count complete subarrays Variable window on distinct vs global distinct cheat card
Srenitha — maximum consecutive numbers (UNNAMED) Classic window after you lock the predicate; exact constraint unnamed R4 coding
Rotten oranges — not a window Multi-source BFS on a grid; minutes are BFS layers clue

Find all anagrams — fixed window

Recognize it from

Every start index where a substring of s is an anagram of p. Window length is locked at p.length(). Same letters, any order. Not “group anagrams of a word list” (that is a map of sorted keys) and not “exactly K distinct.”

What it actually does

A fixed window of width m = p.length() slides one character at a time. After the first m characters, each step adds s[i] and removes s[i − m]. The window is an anagram when its letter bag equals p’s letter bag.

Sorting each slice makes the idea visible and is too slow. The interview loop keeps two int[26] arrays (or one deficit array) and a single missing / match counter so you do not scan 26 letters every step.

Simple — read this first

public List<Integer> findAnagrams(String s, String p) {
    char[] need = p.toCharArray();
    Arrays.sort(need);
    String key = new String(need);
    List<Integer> ans = new ArrayList<>();
    int m = p.length();
    for (int i = 0; i + m <= s.length(); i++) {
        // sort copies the window — O(m log m) per start, so you feel the waste
        char[] w = s.substring(i, i + m).toCharArray();
        Arrays.sort(w);
        if (key.equals(new String(w))) ans.add(i);
    }
    return ans;
}

Interview — type this

public List<Integer> findAnagrams(String s, String p) {
    List<Integer> ans = new ArrayList<>();
    if (s == null || p == null || s.length() < p.length()) return ans;
    int[] need = new int[26];
    for (int i = 0; i < p.length(); i++) need[p.charAt(i) - 'a']++;
    int missing = p.length();
    int m = p.length();
    for (int i = 0; i < s.length(); i++) {
        int in = s.charAt(i) - 'a';
        // need[c] > 0 means this copy of c is still owed to p
        if (need[in] > 0) missing--;
        need[in]--;
        if (i >= m) {
            int out = s.charAt(i - m) - 'a';
            need[out]++;
            // after restore, > 0 means we now owe that letter again
            if (need[out] > 0) missing++;
        }
        if (i >= m - 1 && missing == 0) ans.add(i - m + 1);
    }
    return ans;
}
Example 1 — simple

Input s = "cba", p = "abc". One window, sort both sides.

s = c b a p = a b c 0 1 2
One start — sort the window and compare to sorted p
iwindowsorted windowsorted pmatch
0cbaabcabcyes → record 0

Answer [0].

Example 2 — every step

Input s = "cbaebabacd", p = "abc". Every slide of width 3. Interview column is the missing counter after add (and remove when i ≥ 3).

index 0 1 2 3 4 5 6 7 8 9 s c b a e b a b a c d p a b c m = 3
Every i — add s[i], remove s[i−3] when the window is full
iaddremovewindowmissingstarts
0c—c2—
1b—cb1—
2a—cba00
3ecbae10
4bbaeb10
5aaeba10
6bebab10
7ababa10
8cabac00, 6
9dbacd10, 6

Answer [0, 6] — cba and bac. Sort-every-window agrees: only those two slices sort to abc.

TC simple O((n−m) m log m) TC interview O(n) SC O(1) / O(m) brute copy
Why int[26] + missing An anagram is a bag of letters, not a sorted string. One add and one remove updates the bag in O(1). Arrays.equals on two length-26 arrays is also correct; the match counter just avoids the 26-scan. Confirm lowercase a–z before you type - 'a'.
Common mistakes
  • Recording i instead of i − m + 1 when the right end is i.
  • Comparing strings with ==, or sorting in place on a shared buffer.
  • Jumping to “exactly K distinct” subtraction — anagrams are a fixed width, not a distinct-count.

Notebook problems for this topic

ProblemPatternLinks
Variation of Find All Anagrams Fixed window, 26-count / match Java in ch.05 · cheat card · R4 coding

Exactly-K via atMost(K) − atMost(K−1)

Recognize it from

Count of subarrays with exactly K odds, exactly K distinct, exactly K zeros, and so on. The resource is a 0/1 budget (or a distinct-count budget) and values do not go negative in a way that breaks monotonicity. If they want an exact sum and the array can contain negatives, that is prefix + HashMap, not this subtract.

What it actually does

“Exactly K” is not monotone: adding one more odd can take you from K to K+1. “At most K” is monotone: once you have too many odds you must shrink, and you never want those discarded left indices back. For each right end, after shrinking, every left in [left, right] is a valid at-most-K window ending at right — there are right − left + 1 of them.

Then exactly(K) = atMost(K) − atMost(K−1). Nice subarrays: odd → 1, even → 0, K odds. The simple block is only atMost so you can see the engine. The interview block subtracts. Guard k < 0 so atMost(K−1) is 0 when K is 0.

Simple — read this first

public int atMostOdd(int[] a, int k) {
    if (k < 0) return 0;
    int left = 0, odd = 0, ans = 0;
    for (int right = 0; right < a.length; right++) {
        if ((a[right] & 1) == 1) odd++;
        while (odd > k) {
            // evens do not change odd, but they still leave the window
            if ((a[left] & 1) == 1) odd--;
            left++;
        }
        // every [l, right] with l in [left, right] has ≤ k odds
        ans += right - left + 1;
    }
    return ans;
}

Interview — type this

public int numberOfSubarrays(int[] a, int k) {
    // exactly k = (windows with ≤ k) minus (windows with ≤ k−1)
    return atMostOdd(a, k) - atMostOdd(a, k - 1);
}

private int atMostOdd(int[] a, int k) {
    if (k < 0) return 0;
    int left = 0, odd = 0, ans = 0;
    for (int right = 0; right < a.length; right++) {
        odd += a[right] & 1;
        while (odd > k) odd -= a[left++] & 1;
        ans += right - left + 1;
    }
    return ans;
}
Example 1 — simple

Input a = [1, 1, 2], k = 1. Dry-run atMost(1) only — that is the simple method.

index 0 1 2 a 1 1 2 odd, odd, even
atMost(1) — shrink while odd > 1, then add (right−left+1)
righta[right]odd before shrinkleft afterodd afteraddans
0110111
1121 (drop index 0)112
2211124

atMost(1) = 4 windows: [1], [1] (index 1), [1,2], [2]. That is not the nice-subarray answer. Exactly 1 odd is 3 of those four (drop the even singleton). Interview subtracts atMost(0) = 1 → 3.

Example 2 — every step

Input a = [1, 1, 2, 1, 1], k = 3. Both atMost values, every right.

index 0 1 2 3 4 a 1 1 2 1 1
atMost(3) — k = 3 odds
rightodd after addshrink?leftaddans
01no011
12no023
22no036
33no0410
44yes, drop a[0], odd=31414
atMost(2) — same array
rightodd after addshrink?leftaddans
01no011
12no023
22no036
33yes, drop a[0], odd=2139
43yes, drop a[1], odd=22312

14 − 12 = 2. The two subarrays with exactly 3 odds are [1,1,2,1] (indices 0..3) and [1,2,1,1] (indices 1..4).

TC O(n) two passes SC O(1)
Why subtract two windows At-most is the monotone budget you can shrink. Exactly is a difference of two legal counts. A prefix HashMap on odd-counts also works (same as subarray-sum-K on 0/1); the window is enough here because odds only add 0 or 1. Nice in chapter 05 is odds, not a numeric prefix-sum of the raw values — see chapter 10 for that distinction.
Common mistakes
  • Returning atMost(k) as the answer when they asked for exactly K.
  • Forgetting if (k < 0) return 0, so atMost(k − 1) walks with a negative budget.
  • Using this shrink on an exact sum when negatives exist — left may skip the only valid start.

Notebook problems for this topic

ProblemPatternLinks
Count Number of Nice Subarrays Exactly K odds = atMost(K) − atMost(K−1) Java in ch.05 · cheat card · clue · clue · R4 coding

Sliding window maximum — monotonic deque

Recognize it from

Max (or min) of every contiguous window of width K. Story wrap: “max of each k days,” warehouse / delivery dressing. Same deque lives in the stacks chapter; keep the full card here because the loop is a sliding window of indices.

What it actually does

A brute scan of each window is O(nk). The interview structure is an ArrayDeque of indices, with values decreasing from front to back. The front is the current max. When index i arrives: drop the front if it is ≤ i − k (it left the window); drop the back while a[back] ≤ a[i] (those values can never be max while i is still in the window); push i. Once i ≥ k − 1, record a[front].

Each index is pushed and popped at most once, so the extra work is O(n), not O(nk). Store indices, not values — otherwise you cannot tell a duplicate max that has already slid out.

Simple — read this first

public int[] maxSlidingWindow(int[] a, int k) {
    int n = a.length;
    int[] ans = new int[n - k + 1];
    for (int i = 0; i + k <= n; i++) {
        int m = a[i];
        // rescan k−1 shared cells every step — that is why this is O(nk)
        for (int j = i + 1; j < i + k; j++) m = Math.max(m, a[j]);
        ans[i] = m;
    }
    return ans;
}

Interview — type this

public int[] maxSlidingWindow(int[] a, int k) {
    if (a == null || a.length == 0 || k <= 0) return new int[0];
    int n = a.length;
    int[] ans = new int[n - k + 1];
    Deque<Integer> dq = new ArrayDeque<>(); // indices, a[idx] decreasing
    for (int i = 0; i < n; i++) {
        if (!dq.isEmpty() && dq.peekFirst() <= i - k) dq.pollFirst();
        while (!dq.isEmpty() && a[dq.peekLast()] <= a[i]) {
            // last can never beat a[i] while i is still in the window
            dq.pollLast();
        }
        dq.offerLast(i);
        if (i >= k - 1) ans[i - k + 1] = a[dq.peekFirst()];
    }
    return ans;
}
Example 1 — simple

Input a = [1, 3, −1], k = 3. One window, so brute and deque agree.

index 0 1 2 a 1 3 −1 k = 3 → one window
Deque of indices — record only when i ≥ k−1
ia[i]pop front?pop back?deque idx (vals)window max
01nono0 (1)—
13noyes, 1 ≤ 31 (3)—
2−1nono (−1 < 3)1,2 (3, −1)3

Answer [3].

Example 2 — every step

Input a = [1, 3, −1, −3, 5, 3, 6, 7], k = 3. Every index of the deque, no skipped i.

index 0 1 2 3 4 5 6 7 a 1 3 −1 −3 5 3 6 7
Every i — deque front is the max of a[i−k+1 .. i] once i ≥ 2
ia[i]expire front (≤ i−3)pop back ≤ a[i]deque idx (vals)max
01——0 (1)—
13—01 (3)—
2−1——1, 2 (3, −1)3
3−3——1, 2, 3 (3, −1, −3)3
451 is 4−3=1, pop 12, 3 (both ≤ 5)4 (5)5
53——4, 5 (5, 3)5
66—5, 46 (6)6
77—67 (7)7

Answer [3, 3, 5, 5, 6, 7]. At i=4 the expired 3 at index 1 would have been wrong if you stored values and could not tell it had left.

TC brute O(nk) TC deque O(n) SC O(k)
Why a deque of indices Front = max in the live window. Back pops anyone who can never win against the new value. A heap of the window is correct but O(n log n) with lazy deletes. java.util.Stack is the wrong type — use ArrayDeque. Window minimum: flip <= to >=.
Common mistakes
  • Storing values, not indices — expired duplicates cannot be dropped with i − k.
  • Using < instead of <= when popping the back, so equal values pile up and the front can be stale.
  • Writing the answer at every i, including before the first full window.

Notebook problems for this topic

ProblemPatternLinks
Sliding Window Maximum Monotonic decreasing deque of indices Java in ch.05 · cheat card · clue

See also on this chapter

Anagrams, nice subarrays, and window max share chapter 05 / 24 / R4 cards already linked on their own blocks. Complete-subarrays and the repeating-character replacement variation sit on the variable-window card. Srenitha’s leftover-hour prompt is UNNAMED — lock their predicate before you type last-seen or deque. Rotten oranges is linked above so you do not reach for a window when the grid is rotting by BFS layers.