LeetCode #567 Medium

Permutation in String

Permutation in String: given two strings s1 and s2, return true if s2 contains a permutation of s1 as a contiguous substring.

Constraints
  • 1 <= s1.length, s2.length <= 10⁴
  • s1 and s2 consist of lowercase English letters.
stringsliding-windowhash-table
Open on LeetCode ↗
02

Intuition

Permutation in string asks whether any permutation of s1 appears as a contiguous substring of s2. Generating permutations is factorial and hopeless; the reframing is what makes it linear. A permutation of s1 is any arrangement of exactly its characters, so: - A substring is a permutation of s1 precisely when it has the same length and the same character frequencies — order is irrelevant. Equal length means only windows of length s1.length can match, which fixes the window size and turns this into a sliding-window frequency comparison. Build a frequency count for s1, then slide a window of that length across s2, updating counts as characters enter and leave. When the two counts match, a permutation has been found. Comparing two 26-element arrays at each position is O(26) per window — constant, and perfectly acceptable. The faster version keeps a matches counter: how many of the 26 letters currently have exactly the right count. A window matches when that reaches 26. Updating it correctly requires checking whether a count became correct or stopped being correct with each change, comparing before and after. Testing only one side leaves the counter permanently wrong. The window slides by two operations — decrement the outgoing character, increment the incoming — so each step is O(1) regardless of window size. If s1 is longer than s2, no window exists and the answer is false immediately. This is the same machinery as Find All Anagrams in a String, which returns every match rather than just whether one exists.

How to spot this pattern

A fixed-width window plus a matches counter that tracks how many distinct characters have hit their exact required count. Comparing one integer against the number of needed characters replaces re-comparing two frequency maps every step.

03

Approach

Try it first

Before reading on: price up what counting everything costs here, then ask what makes a window invalid, and which pointer should move when it is. Aim for O(n) time and O(1) space.

1

Reframe permutations as frequencies

A permutation is any arrangement of the same characters, so equal length plus equal frequencies is the whole test — order never matters.

2

Fix the window size

Only windows of length s1.length can match. If s1 is longer than s2, return false immediately — no valid window exists.

3

Build the target counts

Count each character in s1. A window matches when its own counts equal these exactly.

4

Slide with two updates

Decrement the outgoing character and increment the incoming one. Each step is O(1), independent of the window's size.

5

Compare counts per window

With a 26-letter alphabet, comparing the arrays is O(26) — constant. This version is simple and fast enough for any input here.

6

Or track a matches counter

Count letters whose frequency is exactly right, matching at 26. Check whether each change made a letter correct or broke it — testing one side leaves the counter wrong.

7

Cost of the scan

Each character enters and leaves once, giving O(n) time with the counter or O(26n) with array comparison, and O(1) space for the fixed alphabet.

04

Solution & live demo

▶1class Solution:
▶2 def checkInclusion(self, s1, s2):
▶3 from collections import Counter
▶4 k = len(s1)
▶5 if k > len(s2):
▶6 return False
▶7 need = Counter(s1)
▶8 have = Counter()
▶9 matches = 0
▶10 need_keys = len(need)
▶11 for r in range(len(s2)):
▶12 have[s2[r]] += 1
▶13 if have[s2[r]] == need.get(s2[r], 0):
▶14 matches += 1
▶15 elif have[s2[r]] == need.get(s2[r], -1) + 1:
▶16 pass
▶17 if r >= k:
▶18 left_ch = s2[r - k]
▶19 if have[left_ch] == need.get(left_ch, 0):
▶20 matches -= 1
▶21 have[left_ch] -= 1
▶22 if r >= k - 1 and matches == need_keys:
▶23 return True
▶24 return False
05

Common pitfalls

Comparing full frequency maps each step

✗ Wrong
if have == need: return True
✓ Right
if matches == need_keys: return True

Dictionary comparison is O(26) per position, turning the scan into O(26n). Tracking transitions to and from the exact count makes each step O(1).

Decrementing matches unconditionally on removal

✗ Wrong
have[left_ch] -= 1
matches -= 1
✓ Right
if have[left_ch] == need.get(left_ch, 0):
    matches -= 1
have[left_ch] -= 1

Removing a surplus copy, or a character not in s1 at all, doesn't break a satisfied requirement. The counter should only fall when a character leaves its exact-match state, which is tested before the decrement.

Checking the answer before the window is full

✗ Wrong
if matches == need_keys: return True
✓ Right
if r >= k - 1 and matches == need_keys:

Early in the scan the window is shorter than s1, so a coincidental match on a prefix reports a permutation that isn't one. The width guard ensures a full-size window.

06

Edge cases

s1 longer than s2

No window of that width fits in s2, so the loop never reaches full width and the answer is false; guard against this or let the loop naturally produce zero valid windows.

s1 and s2 are identical

The single full-width window is trivially a permutation of itself, so matches reaches the needed count on the last character processed.

Repeated characters in s1

The need map naturally holds counts greater than 1 for repeated letters; the matches-counter approach still works since equality is checked per distinct key, not per occurrence.

No permutation exists anywhere in s2

Every window's matches count stays below the needed total, and the function correctly returns false after scanning the whole string.

07

Complexity

Time
O(n)
Space
O(1)
n is len(s2); each character enters and leaves the window exactly once, and the count maps are bounded by the alphabet size (26 lowercase letters).