Permutation in String
1. Problem & Core Objective
The problem
Given two strings s1 and s2, return true if s2 contains a permutation of s1 — that is, if one of s1's permutations appears as a substring of s2.
Input: s1 = "ab", s2 = "eidbaooo" Output: true ("ba" is a permutation of "ab")
Input: s1 = "ab", s2 = "eidboaoo" Output: false ("bo","oa","ao"… none match)Constraints:
1 <= s1.length, s2.length <= 10^4s1ands2consist of lowercase English letters
What the interviewer is actually testing
This is the fixed-size window — the third of the three templates, and the only one in this section where the window never changes length.
- Do you translate "permutation" into "identical character counts"? A permutation is a rearrangement, so order is irrelevant and multiplicity is everything. This is Valid Anagram applied to every window.
- Do you notice the window size is fixed at
s1.length()? A permutation ofs1has exactlys1.length()characters, so you never need variable-size logic. - Can you make the comparison
O(1)instead ofO(26)? Thematchescounter is the refinement, and the incremental reasoning behind it is the interesting part. - Do you guard
s1.length() > s2.length()? Otherwise the initial window construction runs off the end.
2. First-Principles Thought Process
Step 1 — Constraints
Both strings up to 10^4.
O(n · m)→10^8. Borderline; probably passes, but it's not the target.O(n · m log m)(sorting each window) → too slow.O(n)orO(26n)→ the target.
Lowercase English only — so int[26], not a HashMap (06).
Step 2 — Translate the word "permutation"
A permutation of s1 is a rearrangement of exactly its characters. So a substring w of s2 is a permutation of s1 iff:
w.length() == s1.length(), andwands1have identical character counts.
That's the anagram condition. So the question becomes:
Does any window of
s2of lengths1.length()have the same character counts ass1?
Step 3 — The window size is fixed
This is the key structural observation, and it simplifies everything. Because a permutation has exactly m = s1.length() characters, only windows of exactly that length can match. There is no "expand until valid, then shrink" — the window is always size m, sliding one position at a time.
That makes this Template 1 (fixed size) rather than the longest/shortest-valid templates.
Step 4 — Choose the summary
The window must answer: "do my character counts equal s1's?"
- Two
int[26]arrays — one fors1(fixed), one for the window (updated as it slides). - Comparison is
Arrays.equals(need, window)— 26 comparisons, a constant.
So the straightforward version is O(26n) = O(n). That's already optimal asymptotically, which is worth saying out loud before optimizing further.
Step 5 — The refinement: an incremental matches counter
To make the check genuinely O(1), maintain a counter of how many of the 26 slots currently agree between need and window. Then "is this window a permutation" is matches == 26.
The insight that makes it work:
When you change one slot by ±1, only that slot can change its agreement status.
So after each increment or decrement, you check just that one slot:
- If it just became equal to
need→ one more slot agrees,matches++. - If it just moved one step past
need(from equal to unequal) →matches--. - Any other movement doesn't change the status.
Step 6 — The guard
If s1 is longer than s2, no window of size s1.length() exists. The initial construction loop would index past the end of s2 and throw. One line up front:
if (s1.length() > s2.length()) return false;3. Solution Paths
Approach 1 — Brute force: sort every window
public boolean checkInclusion(String s1, String s2) {
char[] target = s1.toCharArray();
Arrays.sort(target);
int m = s1.length();
for (int i = 0; i + m <= s2.length(); i++) {
char[] window = s2.substring(i, i + m).toCharArray();
Arrays.sort(window);
if (Arrays.equals(target, window)) return true;
}
return false;
}Sorting canonicalizes each window, so two anagrams compare equal.
- Time:
O(n · m log m)— a sort per window. - Space:
O(m).
Counter-questions on this approach
⭐ "You sort every window from scratch. What do consecutive windows have in common?"
Almost everything — adjacent windows share
m − 1characters. Only one character leaves and one enters. Re-sorting rebuilds information I already had, which is the signal to maintain the summary incrementally instead.
"Why sort at all, when you only need counts?"
Sorting produces a total ordering when a multiset comparison is strictly weaker — the same argument as in Valid Anagram. Counting is
O(m)per window instead ofO(m log m), and incremental counting isO(1).
Approach 2 — Fixed window with count arrays
public boolean checkInclusion(String s1, String s2) {
int m = s1.length(), n = s2.length();
if (m > n) return false; // no window of that size exists
int[] need = new int[26], window = new int[26];
for (int i = 0; i < m; i++) { // build s1's counts and the first window
need[s1.charAt(i) - 'a']++;
window[s2.charAt(i) - 'a']++;
}
if (Arrays.equals(need, window)) return true;
for (int right = m; right < n; right++) {
window[s2.charAt(right) - 'a']++; // element enters
window[s2.charAt(right - m) - 'a']--; // element leaves
if (Arrays.equals(need, window)) return true;
}
return false;
}How it works. Build both count arrays for the first window, then slide: add the entering character, remove the leaving one, compare.
Trace on s1 = "ab", s2 = "eidbaooo" (m = 2):
right | Window | Counts match {a:1, b:1}? |
|---|---|---|
| — | "ei" | no |
| 2 | "id" | no |
| 3 | "db" | no |
| 4 | "ba" | yes → return true ✓ |
The leaving index is right - m. For right = 4 and m = 2, that's index 2 — the character just outside the new window [3, 4]. Verify it on a two-element example before trusting it.
- Time:
O(26n)=O(n). - Space:
O(1)— 52 ints.
Counter-questions on this approach
⭐ "You call Arrays.equals on every slide — that's 26 comparisons each time. Is it still O(n)?"
Yes. 26 is a constant fixed by the problem's alphabet, not a function of the input, so
O(26n)isO(n). It's a real constant factor and it can be removed, but the asymptotic claim is already correct — I'd say so rather than pretending this version is deficient.
⭐ "Why is the window size fixed? Don't you need to expand and shrink?"
Because a permutation of
s1has exactlys1.length()characters. A longer or shorter substring can't be one, so only windows of that size are candidates. That's what makes this the fixed-size template rather than the longest-valid one.
"Why s2.charAt(right - m) for the leaving character?"
After adding the character at
right, the window spans[right - m + 1, right], so the character that just fell out is atright - m. It's the standard fixed-window index and the usual off-by-one — worth checking on a concrete case: withm = 2andright = 4, the window is[3,4]and the departed index is 2.
"What happens if you forget the m > n guard?"
The initial construction loop indexes
s2.charAt(i)foriup tom - 1, which exceedss2's length, and it throwsStringIndexOutOfBoundsException. It's not a logic error that produces a wrong answer — it's a crash, and it's the first test case an interviewer will try.
Approach 3 — Incremental matches counter (optimal)
public boolean checkInclusion(String s1, String s2) {
int m = s1.length(), n = s2.length();
if (m > n) return false;
int[] need = new int[26], window = new int[26];
for (int i = 0; i < m; i++) {
need[s1.charAt(i) - 'a']++;
window[s2.charAt(i) - 'a']++;
}
int matches = 0;
for (int i = 0; i < 26; i++) if (need[i] == window[i]) matches++;
for (int right = m; right < n; right++) {
if (matches == 26) return true;
int in = s2.charAt(right) - 'a'; // entering
window[in]++;
if (window[in] == need[in]) matches++; // just became equal
else if (window[in] == need[in] + 1) matches--; // just stopped being equal
int out = s2.charAt(right - m) - 'a'; // leaving
window[out]--;
if (window[out] == need[out]) matches++;
else if (window[out] == need[out] - 1) matches--;
}
return matches == 26;
}How the increment logic works. After changing one slot by ±1, only that slot's agreement status can change:
| Slot moved | Status change | Action |
|---|---|---|
Landed exactly on need | unequal → equal | matches++ |
Moved one step past need (up) | equal → unequal | matches-- |
Moved one step past need (down) | equal → unequal | matches-- |
| Anything else | unchanged | nothing |
For an increment, "one step past" means need + 1; for a decrement, need - 1.
- Time:
O(n)— genuinely constant work per slide. - Space:
O(1).
Counter-questions on this approach
⭐ "Why can you check only the one slot you changed, instead of all 26?"
Because a single
++or--touches exactly one slot. Every other slot's relationship toneedis unchanged, so re-examining them can't reveal anything new. That's the general shape of maintaining a derived invariant incrementally — recompute only what the change could have affected.
⭐ "Why need[in] + 1 for the increment case but need[in] - 1 for the decrement?"
Because "just stopped being equal" means the slot moved away from
needby one, and the direction depends on which operation was applied. An increment overshoots toneed + 1; a decrement undershoots toneed - 1. Getting these backwards makesmatchesdrift and the answer silently wrong.
"Why is the matches == 26 check at the top of the loop rather than the bottom?"
Because the initial window is already built and counted before the loop starts, so its validity must be tested before the first slide. Putting the check at the top covers it. The alternative is an explicit test before the loop plus a check at the bottom — same thing, one more line.
"Is this actually worth the extra complexity over Arrays.equals?"
Honestly, marginally. Both are
O(n); this removes a factor of 26. I'd write theArrays.equalsversion first because it's obviously correct, and offer this as the refinement — the incremental reasoning is the part worth demonstrating, not the speed.
Comparison
| Approach | Time | Space | Notes |
|---|---|---|---|
| Sort each window | O(n · m log m) | O(m) | Too slow |
Count arrays + Arrays.equals | O(26n) = O(n) | O(1) | Write this first |
Count arrays + matches | O(n) | O(1) | Truly O(1) per slide |
4. Why the Optimal Wins
Against sorting each window. Adjacent windows share m − 1 characters, so re-sorting each one rebuilds information already available. Maintaining counts incrementally reduces per-window work from O(m log m) to O(1).
Against the Arrays.equals version. Both are O(n). The matches counter removes the 26-element scan by noticing that a single-slot change can only affect that slot's agreement. It's a constant-factor win, and I'd say so plainly rather than overselling it — the value is in demonstrating the incremental-invariant technique, which reappears in Minimum Window Substring as the have/need counter.
Why O(n) is the floor. Every character of s2 must be examined, since a matching window could sit anywhere. So O(n) is optimal.
The connection worth naming:
This is Valid Anagram evaluated at every position of
s2— and the sliding window is what makes "at every position" costO(1)instead ofO(m).
5. Java Prerequisites
Fixed-alphabet counting
int[] need = new int[26];
need[c - 'a']++; // LOWERCASE — base is 'a'Note this question is lowercase while the previous one was uppercase. Check the constraints rather than copying the base character from the last problem — 'A' vs 'a' is a 32-offset that produces out-of-range indices and throws.
Arrays.equals on primitive arrays
Arrays.equals(need, window); // element-by-element, O(26) here
need == window // reference identity — always false for distinct arraysArrays.equals is the static helper. Note it's not what a HashMap would call if you used an array as a key — see 05.
Fixed-window index arithmetic
for (int right = m; right < n; right++) {
window[s2.charAt(right) - 'a']++; // entering
window[s2.charAt(right - m) - 'a']--; // leaving
}The departing index is right - m. Derive it rather than memorizing: after adding right, the window is [right - m + 1, right], so the element just outside on the left is right - m.
The length guard
if (s1.length() > s2.length()) return false;Prevents StringIndexOutOfBoundsException in the initial build loop. Constraints guarantee both are at least 1, so there's no empty-string case, but the length relationship is unconstrained.
charAt vs toCharArray
s2.charAt(i); // O(1), no allocationBoth ends of the window are indexed independently here, so charAt is the right choice. toCharArray would allocate an O(n) copy for no benefit.
6. Interview Communication Guide
Clarifying questions
- "Does the permutation need to be a contiguous substring of
s2?" — yes. A subsequence version would be a different (and easier) problem. - "Are both strings lowercase English only?" — decides
int[26]versus a map. - "Do I return a boolean, or the index where it occurs?" — boolean here; the index version is a trivial change.
- "What if
s1is longer thans2?" — return false; confirm so the guard is justified rather than defensive. - "Should I find all occurrences, or just whether one exists?" — just existence, which lets me return early.
The pitch
"A permutation of
s1is a rearrangement of exactly its characters, so a substring ofs2is a permutation ofs1if and only if it has the same length and the same character counts. That's the anagram condition applied at every position.Crucially, the length being fixed means I only ever need windows of exactly
s1.length()— so this is a fixed-size window, not an expand-and-shrink one.I'll build an
int[26]fors1and one for the first window ofs2, then slide: add the entering character, remove the leaving one, and compare the arrays. That'sO(26n), which isO(n)since 26 is a constant.To make the comparison genuinely
O(1)I can keep amatchescounter of how many of the 26 slots currently agree. When I change one slot by one, only that slot's agreement can change — so I adjustmatchesby inspecting just that slot. The window is a permutation exactly whenmatches == 26.One guard up front: if
s1is longer thans2, no window of that size exists and I'd index out of bounds building the first one."
Edge cases to raise proactively
s1 | s2 | Expected | Why |
|---|---|---|---|
"ab" | "eidbaooo" | true | "ba" at index 3 |
"ab" | "eidboaoo" | false | No window matches |
"abc" | "ab" | false | s1 longer — needs the guard |
"a" | "a" | true | Equal lengths; the initial window is the answer |
"ab" | "ab" | true | Initial window matches, loop never runs |
"ab" | "ba" | true | Reversal is a permutation |
"aa" | "ab" | false | Multiplicity matters, not just the character set |
s1 longer than s2 is the one to volunteer — it's a crash, not a wrong answer, and it's the first thing an interviewer will test.
"aa" vs "ab" is the second — it catches solutions using a Set instead of counts, which would see {a} ⊆ {a,b} and wrongly return true. It proves you understood "multiset", not "set".
s1.length() == s2.length() is worth mentioning too: the sliding loop never executes, so correctness depends entirely on checking the initial window.
7. Follow-Up Questions — Modified Constraints
The interviewer changes a constraint of the original problem and asks you to solve it again. ⭐ marks the most likely.
⭐ "Find all starting indices where a permutation of s1 occurs, not just whether one does." (LC 438 — Find All Anagrams in a String)
Nearly the same code: instead of returning
trueon a match, appendright - m + 1to a result list and keep sliding. StillO(n)time; space becomesO(n)for the output in the worst case. This is the most common escalation, and the two problems are usually asked together.
"What if the alphabet weren't restricted to lowercase English?"
int[26]breaks. Switch toHashMap<Character,Integer>for both counts, and compare withmap.equals(map)— or keep thematchesidea by tracking the number of satisfied distinct keys, which is exactly thehave/needstructure in Minimum Window Substring. Space becomesO(k)in the distinct characters.
"What if s1 could contain characters that must appear at least that many times, rather than exactly?"
The window is no longer fixed-size — a valid window could be longer than
s1. That converts this into the shortest-valid template, which is precisely Minimum Window Substring. Good illustration that "exactly" versus "at least" is what decides fixed versus variable size.
"What if s2 streams in and you can't index backwards?"
Buffer the last
mcharacters in anArrayDeque<Character>so you can evict the departing character without indexing the original string.O(m)memory instead ofO(1), but it no longer needs random access.
"What if you had to handle q different s1 queries against the same s2?"
Running the window per query is
O(q · n). If all queries share the same lengthm, precompute a rolling count signature for every window ofs2once —O(n)— hash each signature, and answer each query inO(m)by hashing its counts and looking up. Different lengths need one pass per distinct length.
"What if you needed permutations of s1 allowing up to k character substitutions?"
The exact-match condition relaxes to a distance: for each window, the number of substitutions needed is half the sum of absolute differences between the count arrays (each surplus character pairs with a deficit). That's
O(26)per window to compute — soO(26n)overall, and thematchesshortcut no longer applies.