Permutation in String
A permutation of a string uses the same letters in any order, each one as many times as the original does: tar, rat and art are permutations of each other. You get two strings s1 and s2 made of lowercase English letters. Return true if some permutation of s1 appears in s2 as a substring (a run of consecutive characters), and false otherwise.
Function
- s1string
- the letters to rearrange
- s2string
- the string to search in
- Returnsboolean
- true if a substring of s2 is a rearrangement of s1
Constraints
1 ≤ s1.length ≤ 2 × 1041 ≤ s2.length ≤ 5 × 104s1ands2contain only lowercase English letters (atoz).s1may be longer thans2.
Examples
- Input
- s1 = "tar"s2 = "smartphone"
- Output
- true
- Explanation
- The substring
artat indices 2 to 4 ofsmartphoneholds onea, onerand onet, the same letters astar.
- Input
- s1 = "noon"s2 = "onion"
- Output
- false
- Explanation
- The windows of length 4 are
onioandnion.noonneeds twons and twoos, and each window has aniin place of one of them.
- Input
- s1 = "abcd"s2 = "dcb"
- Output
- false
- Explanation
- Any permutation of
abcdhas 4 letters, anddcbhas only 3, so it cannot contain one.
Follow-up
Can you return every index of s2 where a permutation of s1 starts, still in O(m + n) time?
Hints
Open them one at a time. Each one gives away a little more.
In a permutation the order of the letters does not matter. What about a substring of
s2decides whether it is a permutation ofs1, and how long must it be?Only substrings of length
m = s1.lengthcan work, and such a substring is a permutation ofs1exactly when its 26 letter counts equal the counts ofs1.Slide a window of length
macrosss2. Each step adds one letter on the right and drops one on the left, so update the window's counts with one +1 and one -1 instead of recounting, and compare them with the counts ofs1.
Solution
Listing permutations is hopeless: 10 letters already have 3,628,800 orderings. Ignore order instead. A substring of s2 is a permutation of s1 exactly when it has length m and the same count of every letter. So slide one window of length m across s2, updating its counts by one letter in and one letter out per step.
Count every window from scratch
Correct, but does not finish on the largest tests
Intuition
Count the letters of s1 once in a table of 26 numbers. Then take every substring of s2 of length m, count it in a fresh table, and compare. In smartphone the windows are sma, mar, art and so on, and art matches tar.
It checks every candidate, so it is correct. But neighbouring windows share m-1 letters and you recount them all: with m = 2 × 10^4 and n = 5 × 10^4 that is 30,001 windows of 20,000 letters, about 6 × 10^8 steps.
Algorithm
- If
s1is longer thans2, returnfalse. - Count the letters of
s1in a tableneedof 26 zeros. - For every start index from 0 to
n-m, count the letters of themcharacters from that start in a fresh table. - If that table equals
need, returntrue. - After the last window, return
false.
def checkInclusion(s1, s2):
m, n = len(s1), len(s2)
if m > n:
return False
need = [0] * 26 # need[0] is 'a', need[25] is 'z'
for ch in s1:
need[ord(ch) - 97] += 1
for start in range(n - m + 1):
# Count the letters of s2[start : start + m] from scratch
window = [0] * 26
for i in range(start, start + m):
window[ord(s2[i]) - 97] += 1
if window == need:
return True
return FalseSlide the window and compare 26 counts
Intuition
Neighbouring windows differ in two letters: moving from mar to art drops the m and adds the t. So keep one table for the current window and change it with one +1 and one -1 per step.
Each step costs two updates and a comparison of 26 numbers, whatever m is: about 26 × 50,000 = 1.3 × 10^6 operations on the largest input.
Algorithm
- If
s1is longer thans2, returnfalse. - Count
s1intoneedand the firstmletters ofs2intowindow. - If the two tables are equal, return
true. - For each
ifrommton-1: add 1 fors2[i], subtract 1 fors2[i-m], and returntrueif the tables are equal. - Return
false.
def checkInclusion(s1, s2):
m, n = len(s1), len(s2)
if m > n:
return False
need = [0] * 26
window = [0] * 26
for i in range(m):
need[ord(s1[i]) - 97] += 1
window[ord(s2[i]) - 97] += 1 # the first window is s2[0 : m]
if window == need:
return True
for i in range(m, n):
window[ord(s2[i]) - 97] += 1 # s2[i] enters on the right
window[ord(s2[i - m]) - 97] -= 1 # s2[i - m] leaves on the left
if window == need:
return True
return FalseSlide the window and track unbalanced letters
Intuition
A step changes only two counts, so comparing all 26 repeats work. Keep balance[c], the copies of c in s1 minus the copies in the window, and unbalanced, the number of letters whose balance is not 0. The window is a permutation of s1 exactly when unbalanced is 0.
One rule keeps the counter right: before you change a balance, add 1 to unbalanced if it was 0; after, subtract 1 if it is 0. A move from 2 to 1 triggers neither, which is right. For tar in smartphone, unbalanced starts at 3. s and m push it to 5, a brings it to 4, r in and s out make 2, and t in and m out make 0 at art.
You can test from the first letter: while the window is shorter than m, the balances add up to a positive number, so one of them is not 0.
Algorithm
- If
s1is longer thans2, returnfalse. - Count
s1intobalanceand setunbalancedto the number of letters with a balance other than 0. - For each index
iofs2, subtract 1 from the balance ofs2[i], adding 1 tounbalancedif that balance was 0 and subtracting 1 if it becomes 0. - If
i ≥ m, add 1 to the balance ofs2[i-m]with the same bookkeeping. - If
unbalancedis 0, returntrue. After the loop, returnfalse.
def checkInclusion(s1, s2):
m, n = len(s1), len(s2)
if m > n:
return False
# balance[c]: copies of letter c in s1 minus copies in the window
balance = [0] * 26
for ch in s1:
balance[ord(ch) - 97] += 1
unbalanced = sum(1 for b in balance if b != 0)
for i in range(n):
# s2[i] enters the window on the right
c = ord(s2[i]) - 97
if balance[c] == 0:
unbalanced += 1
balance[c] -= 1
if balance[c] == 0:
unbalanced -= 1
# s2[i - m] leaves on the left once the window would pass m letters
if i >= m:
c = ord(s2[i - m]) - 97
if balance[c] == 0:
unbalanced += 1
balance[c] += 1
if balance[c] == 0:
unbalanced -= 1
if unbalanced == 0:
return True
return False
Pitfalls and edge cases
- Checking only that every letter of
s1is in the window.onioholds every letter ofnoonbut is not a permutation of it. - Removing the wrong letter. When
s2[i]enters,s2[i-m]leaves; removings2[i-m+1]drops a letter that is still inside the window. - Skipping the first window. If you compare only after sliding,
tarat index 0 oftarpis never found. - Not returning
falsefirst whens1is longer thans2. In Rustn - mon unsigned lengths underflows, and in Swift the range0...(n - m)crashes. - Comparing arrays with
==where that compares references, as in JavaScript and Dart. In Java useArrays.equals.
Frequently asked questions3
What is the time complexity of Permutation in String?
With a sliding window it is O(m + n), where m is the length of s1 and n the length of s2. You count s1 once, then each letter of s2 enters the window once and leaves it once. Recounting every window from scratch costs O(n · m).
Is Permutation in String the same as finding an anagram inside a string?
Yes. A permutation of s1 is an anagram of it, so the question is whether some substring of s2 of length m is an anagram of s1. The whole-string anagram check compares counts once; here it runs on a sliding window.
Can I use a hash map instead of an array of 26 counters?
Yes, and you need one if the strings can hold any character. With lowercase letters only, an array of 26 is faster. With a map, delete a key when its count drops to 0 so that maps with the same letters compare equal, or use the unbalanced counter, which works the same with a map.
Similar problems
Problems that use the same ideas. Solving two or three of them is what makes a pattern stick.
def checkInclusion(s1, s2):
# Write code hereTest #1
Test #2
Test #3
Arguments
s1 = "tar" s2 = "smartphone"
Expected Output
true