DSA sheet · Strings · Sliding Window pattern (fixed size + frequency map)

Permutation in String

Here the teacher solves the same question three times, each one better than the last. First a brute force with two loops and a map per substring. Then a sliding window with two frequency arrays (the fastest on LeetCode). Then a space-optimised sliding window with one map and a count, the version an interviewer may push you towards. Along the way she explains why an array beats a hashmap when the alphabet is fixed, and why a key whose count drops to 0 has to be deleted from a map.

Why it matters: "does s2 contain some rearrangement of s1 as a contiguous piece?" is the yes/no twin of Find All Anagrams (problem 10). Learn one and you know both. The "count of characters still needed" trick from Part C comes back in Minimum Window Substring.

Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Before starting

Substring = contiguous

A substring is a run of characters that sit side by side in the string (for arrays the same idea is called a subarray). You choose where it starts and where it ends and take everything in between. You can't jump over a character.

index012345
s2eidboa"bo" (3..4) is a substring
s2eidboa"ba" taken from 3 and 5 is not (o is skipped)

Brute force over all windows repeats work

The plain way to handle "some substring must satisfy a rule" is two loops: pick a start, then walk an end forward and test each piece. That's about n² pieces → O(n²). The waste is that neighbouring pieces overlap. The teacher's example: s1 = "abcdf", s2 = "abcdifgh…". The piece "abcdi" fails. The next piece "bcdif" checks b, c, d again, even though we just counted them. Sliding window keeps the overlap and only updates the edges.

The window [left..right]: expand and shrink

The current piece is called the window. left is its first index, right its last, and its length is right − left + 1.

As the teacher keeps saying, expansion belongs to right, shrinking belongs to left, and left undoes whatever right did. Neither pointer ever goes backwards, so together they make at most 2n moves: O(n). That's how two loops become one.

Fixed-size vs variable-size windows

Fixed size (k)Variable size
use it whenthe answer must have a known length (here: len(s1))you want the longest / shortest / number of windows obeying a rule
movesadd 1 on the right; once the window is longer than k (or full, depending on style), drop 1 on the leftadd 1 on the right; then while the rule is broken (or, for shortest, while it still holds) drop on the left
shrink withif: one drop is always enoughwhile
template: fixed-size window of size k
def fixed_window(arr, k):
    out = []
    left = 0
    for right in range(len(arr)):
        # 1. add arr[right] to the window's counts
        if right - left + 1 > k:       # one too many
            # 2. remove arr[left] from the counts
            left += 1
        if right - left + 1 == k:
            out.append((left, right))  # 3. test the full window
    return out

The variable-window family, in one breath

This problem is the simplest kind: the window length is fixed at len(s1), so the shrink decision never depends on the data.

Frequency array vs frequency map

A frequency map stores character → how many times it appears. In Python, a dict or collections.Counter. When the characters can only be the 26 lowercase letters, a frequency array of 26 slots does the same job: slot ord(ch) − ord('a') holds the count of ch.

frequency array and map side by side
from collections import Counter

arr = [0] * 26
for ch in "abca":
    arr[ord(ch) - ord('a')] += 1      # arr[0]=2 (a), arr[1]=1 (b), arr[2]=1 (c)

freq = {}
for ch in "abca":
    freq[ch] = freq.get(ch, 0) + 1    # {'a': 2, 'b': 1, 'c': 1}

same = Counter("abca")                # the same counts in one line
Doubt: why does the teacher prefer the array here?
→ A hashmap has to hash each key to find its slot, which is extra work on every access. An array goes straight to index ord(ch) − 97. When the set of possible keys is small and fixed (26 letters), always prefer the array. Her map-based solution in Part C really was slower on LeetCode for this reason.

What is a permutation?

A permutation of a string is any rearrangement of all its characters, each used as many times as in the original. For "ab" there are two: "ab" and "ba". Two strings are permutations of each other exactly when they have the same length and the same count of every character. That's the whole test we'll use, so order never needs to be checked.


Part A · Brute force: every substring, one map each

LeetCode 567

1The question in simple words

You get two strings, s1 (short) and s2 (long). Return True if some contiguous piece of s2 is a permutation of s1, otherwise False.

index01234567
s2eidbaooos1 = "ab" → True ("ba" at 3..4)
s2eidboaoos1 = "ab" → False

In the second string both b and a are there, but an o sits between them. Permutations of "ab" are "ab" and "ba"; neither appears side by side, so the answer is False.

2What the constraints tell us

3Intuition

A permutation of s1 has exactly len(s1) characters, so look at every piece of s2 of that length. For each, count its letters into a map and compare with s1's map. Equal maps → True. Example: s1 = "ab" → {a:1, b:1}. Pieces of s2 = "eidbaooo": "ei", "id", "db", "ba" → {b:1, a:1} equals {a:1, b:1} → True.

Doubt 1: why not sort instead of counting?
→ Sorting works ("ba" sorted is "ab"), but sorting a piece of length n costs n log n each time. Counting costs n, so counting is cheaper.

4Building the logic

She uses two loops: i = start of the piece, j = end of the piece, moving forward from i. Each step adds s2[j] to a map that belongs to this start i (a fresh map per i).

If every start fails, return False after the loops. A mismatch is never a reason to return False early: a later piece may still match.

Doubt 2 (a fix): in the video, she decides "compare now" with map.size() == n and "break" with map.size() > n. Is the map's size the piece's length?
→ No. A map's size is the number of different letters, not the number of letters. They're equal only when there are no repeats. With s1 = "aa" and s2 = "aa", the map {a:2} has size 1, never 2, so it never compares and wrongly returns False. Use the piece's length instead: compare when j − i + 1 == n, then break (any longer piece is useless). The tests check this case.
Doubt 3: what goes into match?
→ She leaves it to us: loop over the keys and check that both maps have the same count for each one, or use a built-in comparison. In Python, two dicts compare equal with == when they have the same keys and values.

5Approach steps

  1. If len(s1) > len(s2): return False.
  2. Build need = counts of s1.
  3. For each start i: make an empty map window.
  4. For j from i: add s2[j] to window. When the length j − i + 1 reaches n: if window equals need → return True; otherwise break.
  5. After all starts: return False.

6Code (Python)

Brute force: two loops, a map per start
class Solution:
    def checkInclusion(self, s1, s2):
        n, m = len(s1), len(s2)
        if n > m:
            return False

        need = {}
        for ch in s1:
            need[ch] = need.get(ch, 0) + 1

        for i in range(m):                      # start of the piece
            window = {}                         # fresh map for this start
            for j in range(i, m):               # end of the piece
                window[s2[j]] = window.get(s2[j], 0) + 1
                if j - i + 1 == n:              # length check, NOT map size
                    if self.match(window, need):
                        return True
                    break                       # longer pieces are useless
        return False

    def match(self, a, b):
        return a == b                           # same keys, same counts

7Code line by line

linewhat it means
if n > m: return Falses2 has no piece long enough.
need = {...}s1's frequency map, built once.
for i in range(m):Every possible start. (Starts too close to the end never reach length n, so they just end quietly.)
window = {}New map for each start, so nothing from the last start leaks in.
window[s2[j]] = ... + 1Grow the piece by one letter.
if j - i + 1 == n:The piece is exactly s1's length. Only now is a comparison meaningful.
if self.match(...): return TrueFound one permutation, which is all the question asks.
breakStop growing this start; go to the next i.
return FalseNo piece matched.

8Dry run (hand table)

s1 = "ab" (need = {a:1, b:1}), s2 = "eidbaooo".

ipieces built (j = i, i+1)window map at length 2equal to need?action
0e → ei{e:1, i:1}nobreak
1i → id{i:1, d:1}nobreak
2d → db{d:1, b:1}nobreak
3b → ba{b:1, a:1}yesreturn True

For s2 = "eidboaoo" the length-2 pieces are ei, id, db, bo, oa, ao, oo; none equals {a:1, b:1} → False ✓. Again, rows i = 1, 2, … each re-count a letter the previous row had already counted.

9Complexity & remember

Remember the brute forceEvery start i, grow j, count into a fresh map. When the length (not the map size) equals len(s1), compare and break. Any match → True; none → False.

Part B · Sliding window with two frequency arrays

1The question again, with the new goal

Same question. Goal: one pass over s2 instead of two nested loops, by reusing the counts from the previous window.

2What the constraints tell us now

3Intuition: why sliding window

Strings have two patterns in this sheet. Two pointers is for comparing exactly two characters (one at each pointer). Here we care about every character in a contiguous piece, so it's sliding window. And the brute force has the repetition we saw in Part 0 (b, c, d counted again for "bcdif"). Sliding window removes it.

Picture: a frame of width len(s1) sliding along s2 one letter at a time. When it slides, one letter enters on the right and one leaves on the left. freq2 changes in just two slots. Then we compare freq2 with freq1.

4Building the logic from the example

s1 = "ab", s2 = "eidbaooo". left = right = 0 at e.

Doubt 1: in her whiteboard version she uses a map and deletes a letter when its count hits 0. Why?
→ When e leaves, its count becomes 0, but the key e is still in the map. She was using the map's size, and a stale key makes it look like the window still has 3 different letters. Deleting zero keys keeps the map honest. In Python it matters for another reason too: {'e': 0, 'a': 1, 'b': 1} == {'a': 1, 'b': 1} is False for plain dicts, so leftover zeros would hide a real match. With the 26-slot array there are no keys to delete: a 0 slot is simply 0. (The whiteboard version is written out below, with the deletion.)
Doubt 2: she keeps an is_match flag while comparing. Why not return False on the first different slot?
→ A different slot only means this window fails. Later windows may still match ("ei" fails, but "ba" works). So on a mismatch: set the flag to False and break the 26-loop, not the main loop. Only "flag still True" ends the function (with True).
Doubt 3: does the order "shrink, then compare" matter?
→ Yes. In this code the window first grows to n+1, gets trimmed back to n, and then we compare, so every full window is checked exactly once. If you compared first and trimmed afterwards (with "> n"), the trimmed window would never be checked. Another correct order is the one in Part C: compare when the length is exactly n, then drop the left letter right away, so the next add brings it back to n.

5Approach steps

  1. If len(s1) > len(s2): return False.
  2. freq1 = counts of s1 in 26 slots; freq2 = 26 zeros; left = 0.
  3. For right over s2: freq2[s2[right]] += 1.
  4. If the window is longer than n: freq2[s2[left]] −= 1; left += 1.
  5. If the window is exactly n: compare the 26 slots; if all equal → return True.
  6. After the loop: return False.

6Code (Python)

Sliding window with two frequency arrays
class Solution:
    def checkInclusion(self, s1, s2):
        n = len(s1)
        if n > len(s2):
            return False

        freq1 = [0] * 26                        # what s1 has
        freq2 = [0] * 26                        # what the window has
        for ch in s1:
            freq1[ord(ch) - ord('a')] += 1

        left = 0
        for right in range(len(s2)):
            freq2[ord(s2[right]) - ord('a')] += 1      # expand

            if right - left + 1 > n:                   # shrink: left undoes right
                freq2[ord(s2[left]) - ord('a')] -= 1
                left += 1

            if right - left + 1 == n:                  # full window: compare
                is_match = True
                for i in range(26):
                    if freq1[i] != freq2[i]:
                        is_match = False
                        break                          # only this window fails
                if is_match:
                    return True
        return False
Sliding window with a dict (her whiteboard version, deleting zero keys)
class Solution:
    def checkInclusion(self, s1, s2):
        n = len(s1)
        if n > len(s2):
            return False
        need = {}
        for ch in s1:
            need[ch] = need.get(ch, 0) + 1
        window = {}
        left = 0
        for right in range(len(s2)):
            window[s2[right]] = window.get(s2[right], 0) + 1
            if right - left + 1 > n:
                lc = s2[left]
                window[lc] -= 1
                if window[lc] == 0:
                    del window[lc]                     # keep no stale keys
                left += 1
            if right - left + 1 == n and window == need:
                return True
        return False

7Code line by line

linewhat it means
freq1 / freq2 = [0] * 26Two count arrays: the target and the current window.
freq2[...s2[right]...] += 1Expand: the new letter is in the window.
if right - left + 1 > n:The window is one letter too long.
freq2[...s2[left]...] -= 1 left += 1Shrink: undo the leaving letter's +1 and move the start.
if right - left + 1 == n:Only a full-length window can be a permutation.
is_match / for i in range(26)Compare slot by slot. Stop the 26-loop at the first difference.
if is_match: return TrueEvery slot equal → this window is a permutation of s1.
return FalseNo full window matched.

8Dry run (hand table, freq2 after every slide, zeros hidden)

s1 = "ab" → freq1 = {a:1, b:1}. s2 = "eidbaooo".

stepright (char added)window before shrinkfreq2 after addingshrink? (what leaves)window after shrinkfreq2 after shrinkcompare
10 (e)[0..0] ee:1noee:1too short
21 (i)[0..1] eie:1 i:1no (length 2)eie:1 i:1no
32 (d)[0..2] eide:1 i:1 d:1yes, length 3: e leaves[1..2] idi:1 d:1no
43 (b)[1..3] idbi:1 d:1 b:1yes: i leaves[2..3] dbd:1 b:1no
54 (a)[2..4] dbad:1 b:1 a:1yes: d leaves[3..4] bab:1 a:1yes → return True
step 3eidbaoooe left the window, freq2 = i:1 d:1
LR
step 5eidbaooofreq2 = b:1 a:1 = freq1 → True
LR

The False example, s2 = "eidboaoo", all the way through:

stepright (char added)window before shrinkfreq2 after addingshrink? (what leaves)window after shrinkfreq2 after shrinkcompare
10 (e)ee:1noee:1too short
21 (i)eie:1 i:1noeie:1 i:1no
32 (d)eide:1 i:1 d:1e leavesidi:1 d:1no
43 (b)idbi:1 d:1 b:1i leavesdbd:1 b:1no
54 (o)dbod:1 b:1 o:1d leavesbob:1 o:1no
65 (a)boab:1 o:1 a:1b leavesoao:1 a:1no
76 (o)oaoo:2 a:1o leavesaoa:1 o:1no
87 (o)aooa:1 o:2a leavesooo:2no

Loop ends → return False ✓. At step 5 the window "dbo" has b and step 6 adds a, but by then b has already left: they're never inside the same length-2 window.

9Complexity & remember

Remember two arraysAdd right → if longer than n, remove left → if exactly n, compare 26 slots → equal means True. Fixed alphabet? Use an array, not a hashmap.

Part C · Space-optimised: one map + count

1Why another version?

Part B is already linear and fast. But it keeps two count structures. If the alphabet were big (thousands of different characters instead of 26), two structures plus a full compare at every step would be costly, and an interviewer may ask: can you do it with one map, and without comparing all slots every time? That's this part. It's the same idea as the one-map solution of Find All Anagrams.

2What stays the same

Same question, constraints and base case (len(s1) > len(s2) → False). Same window of length len(s1), same "left undoes right".

3Intuition: one map that counts down, plus a number

4Building the logic from the example

s1 = "ab", s2 = "eidbaooo". need = {a:1, b:1}, count = 2, left = 0.

  1. right = e: need[e] is 0 (missing → default 0), not positive → count stays 2. need[e] → −1. Window "e", length 1: not full yet.
  2. right = i: not wanted, need[i] → −1, count 2. Window "ei" has length 2 = len(s1), and count isn't 0, so it's not a permutation. Since it's full and failed, drop the left letter now. e has need −1, which is negative → e was never one we wanted, so count stays. need[e] back to 0, left → 1.
  3. right = d: need[d] → −1. Window "id" full, count 2 → drop i (−1, unwanted) → need[i] = 0, left → 2.
  4. right = b: need[b] is 1 > 0 → wanted → count 1. need[b] → 0. Window "db" full, count 1 → drop d (−1, unwanted) → need[d] = 0, left → 3.
  5. right = a: need[a] is 1 > 0 → count 0. need[a] → 0. count == 0 → return True ("ba").
Doubt 1: when left drops a letter, why is the test "need ≥ 0" and not "need > 0"?
→ Think about what right did. If the letter was wanted, need was at least 1 before right took one, so afterwards it's at least 0. So 0 still means "this copy was wanted, and count went down for it". When it leaves, count must go back up, and need goes up too (we want it again). Only a negative value says "this was an extra copy": then count isn't touched, need just goes back up. She makes exactly this point: "less than zero" means not needed; zero still means needed.
Doubt 2: why shrink when the length is equal to len(s1), not greater, as in Part B?
→ In this version the window is checked (count == 0) right after the add. If a full window failed, it's useless, so she drops its left letter right away. The window is then len(s1) − 1 long, and the next add makes it full again. Both styles look at every full window exactly once.
Doubt 3: why give unwanted letters a value (−1) at all? Couldn't we just ignore them?
→ They're inside the window, so they take up space. Giving them a value keeps one simple rule for leaving: "≥ 0 means it was counted, < 0 means it wasn't". It also handles extra copies of wanted letters: with s1 = "ab" and window "bb", the second b takes need[b] from 0 to −1, so count is correctly not lowered twice.

5Approach steps

  1. If len(s1) > len(s2): return False.
  2. need = counts of s1; count = len(s1); left = 0.
  3. For each right: ch = s2[right]. If need.get(ch, 0) > 0 → count −= 1. Then need[ch] = need.get(ch, 0) − 1.
  4. If count == 0 → return True.
  5. Else, if the window length equals len(s1): lc = s2[left]; if need[lc] ≥ 0 → count += 1; need[lc] += 1; left += 1.
  6. After the loop: return False.

6Code (Python)

One map + count (space-optimised)
class Solution:
    def checkInclusion(self, s1, s2):
        if len(s1) > len(s2):
            return False

        need = {}                                # letter -> how many still needed
        for ch in s1:
            need[ch] = need.get(ch, 0) + 1
        count = len(s1)                          # wanted letters still missing
        left = 0

        for right in range(len(s2)):
            ch = s2[right]
            if need.get(ch, 0) > 0:              # we wanted this one
                count -= 1
            need[ch] = need.get(ch, 0) - 1       # always: it is in the window now

            if count == 0:                       # full window, nothing missing
                return True

            if right - left + 1 == len(s1):      # full but not valid: drop left
                lc = s2[left]
                if need[lc] >= 0:                # it was a wanted copy
                    count += 1
                need[lc] += 1                    # left undoes right
                left += 1
        return False

7Code line by line

linewhat it means
need = {...}The only map. Starts as s1's counts.
count = len(s1)All s1 letters are missing at the start.
if need.get(ch, 0) > 0: count -= 1Checked before decreasing: a positive need means this letter fills a gap.
need[ch] = need.get(ch, 0) - 1Every letter that enters lowers its need, possibly below 0 (unwanted or extra).
if count == 0: return Truecount can only reach 0 when len(s1) wanted letters are inside, and the window is never longer than len(s1), so this window is a permutation.
if right - left + 1 == len(s1):The window is full and it failed, so make room for the next letter.
if need[lc] >= 0: count += 1The leaving letter was a wanted copy → we're missing it again.
need[lc] += 1; left += 1Undo right's −1 and move the start.
return Falsecount never reached 0.

8Dry run (hand table, the map after every slide)

s1 = "ab", s2 = "eidboaoo" (the False example, so we see every step). Start: need = {a:1, b:1}, count = 2, left = 0. "need before" is the value right sees before decreasing.

stepright (char, need before)window [l..r]need after addingcountshrink? (what leaves, why)window afterneed after shrinkcount after
10 e (0, unwanted)ea:1 b:1 e:−12no (length 1)ea:1 b:1 e:−12
21 i (0)eia:1 b:1 e:−1 i:−12yes, full and count ≠ 0: e (−1 → unwanted)ia:1 b:1 e:0 i:−12
32 d (0)ida:1 b:1 e:0 i:−1 d:−12yes: i (−1)da:1 b:1 e:0 i:0 d:−12
43 b (1, wanted)dba:1 b:0 e:0 i:0 d:−11yes: d (−1)ba:1 b:0 e:0 i:0 d:01
54 o (0)boa:1 b:0 … o:−11yes: b (0 ≥ 0 → it was wanted) → count+1oa:1 b:1 … o:−12
65 a (1, wanted)oaa:0 b:1 … o:−11yes: o (−1)aa:0 b:1 … o:01
76 o (0)aoa:0 b:1 … o:−11yes: a (0 → wanted) → count+1oa:1 b:1 … o:−12
87 o (−1)ooa:1 b:1 … o:−22yes: o (−2)oa:1 b:1 … o:−12

"…" = e:0 i:0 d:0, which don't change after step 4.

count never hit 0 → return False ✓. With s2 = "eidbaooo" the first four steps are the same, then step 5 brings in a (need 1 → count 0) → return True on the window "ba".

step 5eidboaoo"bo" full, count 1 → b must leave → count 2
LR
step 6eidboaooa arrives but b is gone → count 1, not 0
LR

9Complexity & remember

Remember one map + countEnter: need > 0 → count −1; need −1. count == 0 → True. Full but not valid → leaving letter: need ≥ 0 → count +1; need +1; left +1.

Part D · Revision page

Brute forceTwo arrays (sliding)One map + count
loopstwo nestedone (+26 compare)one
per windowrebuild a map, compareupdate 2 slots, compare 26update 2 entries, check count
"is it full?"j − i + 1 == n (not map size!)right − left + 1 == nright − left + 1 == n
timeO(m·n)O(n + 26·m)O(n + m)
spacetwo maps2 × 26one map
LeetCode speed (hers)slowfastestslower (hashing)
Find All Anagrams (10)Permutation in String (13)
outputlist of every start indexTrue / False
on a valid windowappend left, keep goingreturn True at once
window and count logicidentical: fixed length len(p) / len(s1), count of missing letters, left undoes right
If you remember only 5 lines 1. Permutation = same length + same letter counts.
2. Slide a window of length len(s1); right adds, left removes (undoes).
3. Fixed 26-letter alphabet → arrays beat hashmaps.
4. One-map trick: need counts down, negative = unwanted, count = letters still missing.
5. Leaving letter: need ≥ 0 means it was wanted → count +1.
Mistakes to avoid ✗ using map size as the piece length (breaks with repeated letters like "aa")
✗ returning False on the first mismatched window
✗ leaving zero-count keys in a dict and then comparing dicts with ==
✗ comparing before trimming with "> n" (the trimmed window is never checked)
✗ "need > 0" when checking the leaving letter (must be ≥ 0)
✗ forgetting the base case len(s1) > len(s2)
test it yourself (paste under any Solution above)
sol = Solution()
print(sol.checkInclusion("ab", "eidbaooo"))   # True
print(sol.checkInclusion("ab", "eidboaoo"))   # False
print(sol.checkInclusion("aa", "aa"))         # True
print(sol.checkInclusion("abc", "ab"))        # False
print(sol.checkInclusion("adc", "dcda"))      # True
print(sol.checkInclusion("ab", "bb"))         # False

Based on this video: Permutation in String · brute force, sliding window, space-optimised