Framework Thinking - Neetcode 150 - Permutation in String
Here is solution for Permutation in String.
1. Understanding the problem
-
Given two strings s1 and s2, determine if any permutation of s1 is a substring of s2. Return True if there exists a contiguous substring of s2 that is a permutation of s1, otherwise False.
-
Special cases:
-
Empty s1: I’ll treat len(s1) == 0 as True (vacuously a permutation). This can be adjusted to interviewer preference.
-
Do not seperate by the case lowercase English letters
-
2. Clarifying questions
-
Are characters limited to lowercase English letters?
-
What should we return on empty inputs?
-
Expected time/space complexity?, aim for O(n) time and O(1) extra space => Maybe we should use hashmap for this (use hashmap for case duplicate ‘a’: 2, ‘b’: 1).
3. Work through examples
-
s1 = “ab”, s2 = “eidbaooo” → True (“ba” appears).
-
s1 = “ab”, s2 = “eidboaoo” → False (no substring perm).
-
s1 = “aa”, s2 = “aaaa” → True (“aa” appears many places).
-
s1 = “”, s2 = “anything” → True (by our convention).
-
s1 = “xyz”, s2 = “xy” → False (s1 longer than s2).
-
Danger-case: repeated characters and mixed order: s1=”aab”, s2=”eidbaaoo” → True (“baa” or “aba” possible).
=> Because substring we use sliding window, but not only hash map or hash set.
4. Brainstorm 2–3 solutions (with complexities)
4.1. Brute-force permutations + search
-
Generate every permutation of s1 (k! permutations), check if any is substring of s2.
-
Time: O(k! * n) — impractical except tiny k.
-
Space: O(k) per permutation.
4.2. Sliding window + frequency comparison
-
Maintain frequency map of s1 (need) and sliding window frequency of current window in s2 of length k = len(s1).
-
Naive map comparison is O(k) per slide → O(n*k).
=> But we can maintain a matches counter (count how many characters have matched required frequency) to get O(n) time
Notes:
-
We can optimize it to O(N) => Because we insert each time when we traverse the character, or remove left when it exceed k => time to insert O(1).
-
And we can change it to a string to convert each key of the map => O(26 * N).
5. Implement solutions
5.1. Brute Force - O(N klogk)
class Solution:
def checkInclusion(self, s1: str, s2: str) -> bool:
s1 = sorted(s1)
for i in range(len(s2)):
for j in range(i, len(s2)):
subStr = s2[i : j + 1]
subStr = sorted(subStr)
if subStr == s1:
return True
return False
-
Time complexity: O(N^3 logN).
-
Space complexity: O(N).
Notes: We can optimize to only look for substring with length k.
5.2. Hash Table - O(N * K)
-
Using the counter to verify when match => O(N * M), where M is the number of length of S1.
-
If count1 have more character than count2, it means we can continue.
-
If count1 have smaller character than count2 => we use exceed the base of count1 => break and init new count.
class Solution:
def checkInclusion(self, s1: str, s2: str) -> bool:
count1 = {}
for c in s1:
count1[c] = 1 + count1.get(c, 0)
need = len(count1)
for i in range(len(s2)):
count2, cur = {}, 0
for j in range(i, len(s2)):
count2[s2[j]] = 1 + count2.get(s2[j], 0)
if count1.get(s2[j], 0) < count2[s2[j]]:
break
if count1.get(s2[j], 0) == count2[s2[j]]:
cur += 1
if cur == need:
return True
return False
*- Time: O(N* M).
- Space: O(1), since the key we use at most 26 characters.*
5.3. Easy to implement O(26 * N)
def checkInclusion_lowercase(s1: str, s2: str) -> bool:
"""
Assumes s1 and s2 contain only lowercase letters 'a'..'z'.
Returns True if some permutation of s1 is a substring of s2.
"""
n1, n2 = len(s1), len(s2)
if n1 == 0:
return True
if n1 > n2:
return False
cnt1 = [0]*26
cnt2 = [0]*26
base = ord('a')
for ch in s1:
cnt1[ord(ch) - base] += 1
for ch in s2[:n1]:
cnt2[ord(ch) - base] += 1
if cnt1 == cnt2:
return True
for i in range(n1, n2):
# include s2[i], exclude s2[i - n1]
cnt2[ord(s2[i]) - base] += 1
cnt2[ord(s2[i - n1]) - base] -= 1
if cnt1 == cnt2:
return True
return False
5.4. Sliding Window - O(N)
class Solution:
def checkInclusion(self, s1: str, s2: str) -> bool:
if len(s1) > len(s2):
return False
s1Count, s2Count = [0] * 26, [0] * 26
for i in range(len(s1)):
s1Count[ord(s1[i]) - ord('a')] += 1
s2Count[ord(s2[i]) - ord('a')] += 1
matches = 0
for i in range(26):
matches += (1 if s1Count[i] == s2Count[i] else 0)
l = 0
for r in range(len(s1), len(s2)):
if matches == 26:
return True
index = ord(s2[r]) - ord('a')
s2Count[index] += 1
if s1Count[index] == s2Count[index]:
matches += 1
elif s1Count[index] + 1 == s2Count[index]:
matches -= 1
index = ord(s2[l]) - ord('a')
s2Count[index] -= 1
if s1Count[index] == s2Count[index]:
matches += 1
elif s1Count[index] - 1 == s2Count[index]:
matches -= 1
l += 1
return matches == 26
-
Time: O(N).
-
Space: O(26).
-
Idea:
-
Find the matches first k digits.
-
If continue to match, match += 1, if before add s2 match, but after add s1 it do not match more => match -= 1.
-
Compare total matches == 26 return true, else False.
-
=> Instead of compare 26 * N each time query => use match variable for N items.