567. Permutation in String
Problem Description
You are given two strings s1
and s2
. Your task is to determine if s2
contains any permutation of s1
as a substring.
A permutation means rearranging the letters of a string. For example, "abc" has permutations like "abc", "acb", "bac", "bca", "cab", and "cba".
The problem asks you to return true
if any permutation of s1
exists as a contiguous substring within s2
, and false
otherwise.
For example:
- If
s1 = "ab"
ands2 = "eidbaooo"
, the function should returntrue
becauses2
contains "ba" which is a permutation ofs1
. - If
s1 = "ab"
ands2 = "eidboaoo"
, the function should returnfalse
because no substring ofs2
is a permutation ofs1
.
The key insight is that you need to find a substring in s2
that has the same length as s1
and contains exactly the same characters with the same frequencies as s1
, just potentially in a different order.
Intuition
The key observation is that checking if a substring is a permutation of s1
doesn't require generating all permutations. Instead, we only need to verify that the substring has the same character frequencies as s1
.
Think of it this way: if two strings are permutations of each other, they must contain exactly the same characters with exactly the same counts. For instance, "abc" and "bca" both have one 'a', one 'b', and one 'c'.
Since we're looking for a substring of s2
that's a permutation of s1
, we know this substring must have the same length as s1
. This naturally leads us to consider a sliding window approach - we can examine each substring of s2
with length equal to len(s1)
.
The naive approach would be to check every possible window by counting character frequencies for each position, but this would be inefficient. Instead, we can maintain a sliding window that moves through s2
one character at a time. As the window slides:
- We add the new character entering the window
- We remove the character leaving the window
- We check if the current window matches
s1
's character distribution
To efficiently track whether our current window matches s1
, we use a counter that starts with the character counts from s1
. As we process characters in s2
, we decrement counts for characters entering our window and increment counts for characters leaving. We also maintain a need
variable that tracks how many distinct characters still need to be matched. When need
becomes 0, we've found a valid permutation.
This approach transforms the problem from "find a permutation" to "find a window with matching character frequencies", which can be solved efficiently in linear time.
Learn more about Two Pointers and Sliding Window patterns.
Solution Approach
We implement a sliding window solution that maintains a fixed-size window equal to the length of s1
as it slides through s2
.
Data Structures:
cnt
: A Counter (hash map) that tracks the difference between required character counts (froms1
) and current window character countsneed
: An integer tracking how many distinct characters still need to be matched
Algorithm Steps:
-
Initialize the counter and need variable:
- Create
cnt = Counter(s1)
to store character frequencies froms1
- Set
need = len(cnt)
, which is the number of distinct characters ins1
- Store
m = len(s1)
for the window size
- Create
-
Slide the window through
s2
: For each characterc
at indexi
ins2
:a. Add character to window:
- Decrement
cnt[c] -= 1
(we've seen one more of this character) - If
cnt[c]
becomes0
, it means we've matched all occurrences of characterc
, so decrementneed -= 1
b. Remove character from window (if window is full):
- When
i >= m
, the window size exceedsm
, so we need to remove the leftmost character - The character to remove is
s2[i - m]
- Increment
cnt[s2[i - m]] += 1
(we're losing one of this character) - If
cnt[s2[i - m]]
becomes1
, it means we no longer have enough of this character, so incrementneed += 1
c. Check for valid permutation:
- If
need == 0
, all character requirements are satisfied, returnTrue
- Decrement
-
Return result:
- If we finish traversing
s2
without finding a valid window, returnFalse
- If we finish traversing
Why this works:
- The
cnt
values represent the "deficit" or "surplus" of each character - When
cnt[c] = 0
, we have exactly the right amount of characterc
- When
cnt[c] < 0
, we have extra occurrences ofc
(doesn't affect validity) - When
cnt[c] > 0
, we're missing occurrences ofc
- The window is valid when all characters from
s1
havecnt
values ≤ 0, which happens whenneed = 0
Time Complexity: O(n)
where n = len(s2)
, as we visit each character once
Space Complexity: O(k)
where k
is the number of distinct characters in s1
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the algorithm with s1 = "ab"
and s2 = "eidbaooo"
.
Initial Setup:
cnt = Counter("ab") = {'a': 1, 'b': 1}
need = 2
(we need to match 2 distinct characters)m = 2
(window size)
Sliding the window through s2 = "eidbaooo":
i = 0, c = 'e':
- Add 'e' to window:
cnt['e'] -= 1
→cnt['e'] = -1
- Since
cnt['e'] ≠ 0
,need
stays at 2 - Window: "e" (size 1)
- Not checking for removal yet (i < m)
i = 1, c = 'i':
- Add 'i' to window:
cnt['i'] -= 1
→cnt['i'] = -1
- Since
cnt['i'] ≠ 0
,need
stays at 2 - Window: "ei" (size 2)
- Not checking for removal yet (i < m)
i = 2, c = 'd':
- Add 'd' to window:
cnt['d'] -= 1
→cnt['d'] = -1
- Since
cnt['d'] ≠ 0
,need
stays at 2 - Window is now full, remove leftmost character 'e' (at position i-m = 0):
cnt['e'] += 1
→cnt['e'] = 0
- Since
cnt['e']
became 0 (not 1),need
stays at 2
- Current window: "id"
i = 3, c = 'b':
- Add 'b' to window:
cnt['b'] -= 1
→cnt['b'] = 0
- Since
cnt['b'] = 0
, we've matched all 'b's:need -= 1
→need = 1
- Remove 'i' from window (at position 1):
cnt['i'] += 1
→cnt['i'] = 0
- Since
cnt['i']
became 0 (not 1),need
stays at 1
- Current window: "db"
i = 4, c = 'a':
- Add 'a' to window:
cnt['a'] -= 1
→cnt['a'] = 0
- Since
cnt['a'] = 0
, we've matched all 'a's:need -= 1
→need = 0
need = 0
means we found a valid permutation!- Current window: "ba" (which is indeed a permutation of "ab")
- Return True
The algorithm correctly identifies that "ba" (at positions 3-4 in s2) is a permutation of "ab".
Solution Implementation
1from collections import Counter
2
3class Solution:
4 def checkInclusion(self, s1: str, s2: str) -> bool:
5 # Count frequency of each character in s1
6 char_count = Counter(s1)
7 # Number of unique characters that still need to be matched
8 chars_needed = len(char_count)
9 # Length of the target string s1 (window size)
10 window_size = len(s1)
11
12 # Iterate through each character in s2
13 for index, char in enumerate(s2):
14 # Add current character to window by decreasing its required count
15 char_count[char] -= 1
16 # If this character's count reaches exactly 0, one less character type is needed
17 if char_count[char] == 0:
18 chars_needed -= 1
19
20 # If window size exceeds s1 length, remove the leftmost character
21 if index >= window_size:
22 left_char = s2[index - window_size]
23 # Remove leftmost character from window by increasing its count
24 char_count[left_char] += 1
25 # If this character's count becomes 1 (was 0), we need this character again
26 if char_count[left_char] == 1:
27 chars_needed += 1
28
29 # If all character requirements are met, permutation found
30 if chars_needed == 0:
31 return True
32
33 # No permutation of s1 found in s2
34 return False
35
1class Solution {
2 public boolean checkInclusion(String s1, String s2) {
3 // Count distinct characters needed to match
4 int distinctCharsNeeded = 0;
5 // Frequency array for characters (26 letters in alphabet)
6 int[] charFrequency = new int[26];
7
8 // Build frequency map for s1 and count distinct characters
9 for (char ch : s1.toCharArray()) {
10 int index = ch - 'a';
11 charFrequency[index]++;
12 // If this is the first occurrence of this character, increment distinct count
13 if (charFrequency[index] == 1) {
14 distinctCharsNeeded++;
15 }
16 }
17
18 int patternLength = s1.length();
19 int textLength = s2.length();
20
21 // Sliding window approach
22 for (int i = 0; i < textLength; i++) {
23 // Add current character to window (right side)
24 int currentCharIndex = s2.charAt(i) - 'a';
25 charFrequency[currentCharIndex]--;
26 // If frequency becomes 0, we've matched all occurrences of this character
27 if (charFrequency[currentCharIndex] == 0) {
28 distinctCharsNeeded--;
29 }
30
31 // Remove leftmost character from window if window size exceeds pattern length
32 if (i >= patternLength) {
33 int leftCharIndex = s2.charAt(i - patternLength) - 'a';
34 charFrequency[leftCharIndex]++;
35 // If frequency becomes 1, we need to match this character again
36 if (charFrequency[leftCharIndex] == 1) {
37 distinctCharsNeeded++;
38 }
39 }
40
41 // Check if all characters are matched (permutation found)
42 if (distinctCharsNeeded == 0) {
43 return true;
44 }
45 }
46
47 return false;
48 }
49}
50
1class Solution {
2public:
3 bool checkInclusion(string s1, string s2) {
4 // Track the number of unique characters that still need to be matched
5 int uniqueCharsToMatch = 0;
6
7 // Frequency array to track the difference between required and current window characters
8 // Positive: need more of this character, Negative: have excess of this character
9 int charFrequencyDiff[26] = {0};
10
11 // Build the frequency map for s1 (the pattern we're looking for)
12 for (char ch : s1) {
13 int charIndex = ch - 'a';
14 if (++charFrequencyDiff[charIndex] == 1) {
15 // This character wasn't needed before, now it is
16 ++uniqueCharsToMatch;
17 }
18 }
19
20 int patternLength = s1.size();
21 int textLength = s2.size();
22
23 // Sliding window approach
24 for (int windowEnd = 0; windowEnd < textLength; ++windowEnd) {
25 // Add current character to the window
26 int currentCharIndex = s2[windowEnd] - 'a';
27 if (--charFrequencyDiff[currentCharIndex] == 0) {
28 // This character's frequency now matches exactly
29 --uniqueCharsToMatch;
30 }
31
32 // Remove the leftmost character if window size exceeds pattern length
33 if (windowEnd >= patternLength) {
34 int leftCharIndex = s2[windowEnd - patternLength] - 'a';
35 if (++charFrequencyDiff[leftCharIndex] == 1) {
36 // This character was balanced, now we need one more
37 ++uniqueCharsToMatch;
38 }
39 }
40
41 // Check if all characters are matched (permutation found)
42 if (uniqueCharsToMatch == 0) {
43 return true;
44 }
45 }
46
47 return false;
48 }
49};
50
1/**
2 * Checks if s2 contains a permutation of s1 as a substring
3 * Uses sliding window technique with character frequency counting
4 *
5 * @param s1 - The pattern string whose permutation we're looking for
6 * @param s2 - The string to search within
7 * @returns true if s2 contains any permutation of s1, false otherwise
8 */
9function checkInclusion(s1: string, s2: string): boolean {
10 // Number of unique characters that still need to be matched
11 let uniqueCharsNeeded: number = 0;
12
13 // Character frequency array for lowercase English letters (a-z)
14 // Positive values indicate chars needed from s1, negative means excess chars
15 const charFrequency: number[] = Array(26).fill(0);
16
17 // ASCII value of 'a' for index calculation
18 const asciiOffsetA: number = 'a'.charCodeAt(0);
19
20 // Build frequency map for s1 and count unique characters
21 for (const char of s1) {
22 const charIndex: number = char.charCodeAt(0) - asciiOffsetA;
23 charFrequency[charIndex]++;
24
25 // If this is the first occurrence of this character, increment unique count
26 if (charFrequency[charIndex] === 1) {
27 uniqueCharsNeeded++;
28 }
29 }
30
31 const patternLength: number = s1.length;
32 const textLength: number = s2.length;
33
34 // Sliding window approach through s2
35 for (let windowEnd: number = 0; windowEnd < textLength; windowEnd++) {
36 // Add current character to the window
37 const currentCharIndex: number = s2.charCodeAt(windowEnd) - asciiOffsetA;
38 charFrequency[currentCharIndex]--;
39
40 // If frequency becomes 0, this character is perfectly matched
41 if (charFrequency[currentCharIndex] === 0) {
42 uniqueCharsNeeded--;
43 }
44
45 // Remove the leftmost character if window size exceeds pattern length
46 if (windowEnd >= patternLength) {
47 const leftCharIndex: number = s2.charCodeAt(windowEnd - patternLength) - asciiOffsetA;
48 charFrequency[leftCharIndex]++;
49
50 // If frequency becomes 1, we now need this character again
51 if (charFrequency[leftCharIndex] === 1) {
52 uniqueCharsNeeded++;
53 }
54 }
55
56 // All characters are matched when uniqueCharsNeeded becomes 0
57 if (uniqueCharsNeeded === 0) {
58 return true;
59 }
60 }
61
62 return false;
63}
64
Time and Space Complexity
Time Complexity: O(m + n)
, where m
is the length of string s1
and n
is the length of string s2
.
- Creating the Counter for
s1
takesO(m)
time - The main loop iterates through each character in
s2
, which takesO(n)
time - Inside the loop, all operations (dictionary access, arithmetic operations, and comparisons) are
O(1)
- Therefore, the total time complexity is
O(m + n)
Space Complexity: O(|Σ|)
, where Σ
is the character set.
- The Counter
cnt
stores at most all unique characters froms1
, but since we're dealing with lowercase English letters, the maximum size is bounded by 26 - The space used by the Counter is independent of the input size and depends only on the character set size
- Since the character set is lowercase letters (a constant size of 26), the space complexity is effectively
O(1)
or constant space
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Misunderstanding When to Update chars_needed
The Problem:
A common mistake is updating chars_needed
for every change in char_count
, rather than only when crossing the zero boundary. Developers might incorrectly write:
# INCORRECT char_count[char] -= 1 if char_count[char] <= 0: # Wrong condition! chars_needed -= 1
This fails because chars_needed
should only change when we transition from "not having enough" to "having exactly enough" (count goes from 1 to 0) or vice versa. If a character appears multiple times and we already have enough, further occurrences shouldn't affect chars_needed
.
The Solution:
Only update chars_needed
when the count crosses zero:
- Decrement when going from positive to zero (just satisfied requirement)
- Increment when going from zero to positive (just lost satisfaction)
# CORRECT char_count[char] -= 1 if char_count[char] == 0: # Exactly at boundary chars_needed -= 1
Pitfall 2: Incorrect Window Boundary Management
The Problem: Developers often struggle with when to start removing characters from the window. Common mistakes include:
# INCORRECT - Off by one error if index > window_size: # Should be >= left_char = s2[index - window_size] # INCORRECT - Wrong index calculation if index >= window_size: left_char = s2[index - window_size - 1] # Wrong!
The Solution:
The window should maintain exactly window_size
characters. When index >= window_size
, we have window_size + 1
characters, so we need to remove the leftmost one at position index - window_size
:
# CORRECT if index >= window_size: left_char = s2[index - window_size]
Pitfall 3: Not Handling Characters Not in s1
The Problem:
When encountering characters in s2
that don't exist in s1
, developers might worry about special handling or think the algorithm breaks. They might try to add checks like:
# UNNECESSARY if char in char_count: char_count[char] -= 1 # ... more logic
The Solution:
Python's Counter
automatically handles missing keys by treating them as having a count of 0. When we decrement a non-existent character, it becomes -1, which doesn't affect chars_needed
since we only care about transitions through zero. The algorithm naturally handles extra characters by letting their counts go negative without impacting the validity check.
Pitfall 4: Checking Window Validity Too Early
The Problem:
Checking if chars_needed == 0
before the window reaches the correct size:
# INCORRECT - Might return True with incomplete window
for index, char in enumerate(s2):
if chars_needed == 0: # Checking too early!
return True
char_count[char] -= 1
# ... rest of logic
The Solution:
Only check for validity after processing the current character and potentially removing the leftmost character. The window must contain exactly window_size
characters when we check:
# CORRECT
for index, char in enumerate(s2):
# Process current character
char_count[char] -= 1
if char_count[char] == 0:
chars_needed -= 1
# Remove leftmost if needed
if index >= window_size:
# ... removal logic
# Now check validity
if chars_needed == 0:
return True
Given a sorted array of integers and an integer called target, find the element that
equals to the target and return its index. Select the correct code that fills the
___
in the given code snippet.
1def binary_search(arr, target):
2 left, right = 0, len(arr) - 1
3 while left ___ right:
4 mid = (left + right) // 2
5 if arr[mid] == target:
6 return mid
7 if arr[mid] < target:
8 ___ = mid + 1
9 else:
10 ___ = mid - 1
11 return -1
12
1public static int binarySearch(int[] arr, int target) {
2 int left = 0;
3 int right = arr.length - 1;
4
5 while (left ___ right) {
6 int mid = left + (right - left) / 2;
7 if (arr[mid] == target) return mid;
8 if (arr[mid] < target) {
9 ___ = mid + 1;
10 } else {
11 ___ = mid - 1;
12 }
13 }
14 return -1;
15}
16
1function binarySearch(arr, target) {
2 let left = 0;
3 let right = arr.length - 1;
4
5 while (left ___ right) {
6 let mid = left + Math.trunc((right - left) / 2);
7 if (arr[mid] == target) return mid;
8 if (arr[mid] < target) {
9 ___ = mid + 1;
10 } else {
11 ___ = mid - 1;
12 }
13 }
14 return -1;
15}
16
Recommended Readings
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
https assets algo monster cover_photos stack svg Sliding Window Maximum Monotonic Stack We have an array and a sliding window defined by a start index and an end index The sliding window moves from left of the array to right There are always k elements in the window The window
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!