Problem overview
The method checkInclusion receives two strings of lowercase English letters, s1 and s2. It returns true when some substring of s2 is a permutation of s1: a run of consecutive letters of s2 that holds the same letters as s1, each one as many times, in any order. For "abb", the permutations are "abb", "bab" and "bba".
With s1 = "ab" and s2 = "eidbaooo" the answer is true, because s2 contains "ba" at indexes 3 and 4. With s2 = "eidboaoo" it is false: s2 has an a and a b, but never next to each other. With s1 = "adc" and s2 = "dcda" it is true: the last three letters, "cda", hold one a, one c and one d, just like "adc", while the first three, "dcd", have two d's and no a.
So the question is whether s2 has a window, exactly as long as s1, that holds every letter of s1 the right number of times. The statement asks for O(n + m) time, where n is the length of s2 and m the length of s1, and O(1) extra space: one count for each of the 26 letters.
Approach: a window that keeps count as it slides
The direct way counts the letters of every substring of s2 of length m from scratch and compares with the counts of s1. That costs O(n · m), which is too slow when both strings are 100,000 letters long.
The faster way slides a window one letter at a time and updates the counts instead of recounting. Moving the window right adds one letter at its right end and, once the window is full, removes one at its left end. Each letter of s2 enters once and leaves once, so the work is O(n).
To avoid comparing two 26-entry arrays at every step, the solution keeps one array and one number:
need[x]says how many more copies of letterxthe window still needs. It starts as the count of the letter ins1. A letter that the window holds too many of makes it negative.missingsays how many letters ofs1the window still lacks in total. It starts as the length ofs1.
When a letter enters and need for it is above 0, the window gains something it lacked, so missing drops by 1. When missing reaches 0, the window holds every letter of s1 as many times as s1 has it. The window is exactly as long as s1, so it has no room for any other letter: it is a permutation.
The trace for "ab" in "eidbaooo" shows the idea. The values are missing after each letter has been processed:
last=0 letter=e missing=2
last=1 letter=i missing=2
last=2 letter=d missing=2
last=3 letter=b missing=1
last=4 letter=a missing=0
The letters e, i and d are not in s1, so missing stays at 2. The b brings it to 1, the a to 0, and the method returns true with the window covering "ba".
Step by step
- Copy the letters of
s2into an array. A SwiftStringcan't be indexed with an integer, socharsisArray(s2.utf8). For lowercase letters each byte is one letter, andchars[i]is its code. - Create
needas an array of 26 zeros, one slot per letter: slot 0 for a, slot 1 for b, and so on. - Count the letters of
s1with a loop overs1.utf8. - Add 1 to
need[Int(c) - 97]. The code ofais 97 and the lowercase letters have consecutive codes, so this gives 0 for a and 25 for z.Int(...)turns the byte into anIntfirst. A letter that appears twice ins1is needed twice. - Set
missingtos1.utf8.count, the number of letters the empty window lacks. - Loop over the right end of the window,
last, from 0 to the end ofchars. Each pass adds the letter at indexlast. - Find the new letter's number in
code. - Count it as found if it was needed. If
need[code]is above 0, subtract 1 frommissing. When the count is 0 or less, the window already has all the copiess1asks for, and this one is extra. - Subtract 1 from
need[code], because the window holds one more of this letter. - Check whether the window is now too long. When
last >= s1.utf8.count, the window has one letter more thans1, and its first letter has to leave. - Find the leaving letter,
chars[last - s1.utf8.count], inoldCode. - Add 1 to
need[oldCode], which undoes what that letter did when it came in. - Update
missing. Ifneed[oldCode]is now above 0, the window lost a letter thats1needs, so add 1 tomissing. If the count is still 0 or less, the window had a spare copy and lost nothing. - Test for a match. If
missingis 0, returntrue. - Return
falseafter the loop. No window matched. This is also the answer whens1is longer thans2, sincemissingcan never reach 0 then.
The solution in Swift
class Solution {
func checkInclusion(_ s1: String, _ s2: String) -> Bool {
let chars = Array(s2.utf8)
var need = [Int](repeating: 0, count: 26)
for c in s1.utf8 {
need[Int(c) - 97] += 1
}
var missing = s1.utf8.count
for last in 0..<chars.count {
let code = Int(chars[last]) - 97
if need[code] > 0 { missing -= 1 }
need[code] -= 1
if last >= s1.utf8.count {
let oldCode = Int(chars[last - s1.utf8.count]) - 97
need[oldCode] += 1
if need[oldCode] > 0 { missing += 1 }
}
if missing == 0 { return true }
}
return false
}
}
The Swift details worth knowing:
- Why not index the string? Swift strings are collections of
Charactervalues, and a character can span several bytes, so there is nos[i]. Either you walk aString.Indexor you copy the content into an array. Here,Array(s2.utf8)gives an array ofUInt8codes: for"abc"it is[97, 98, 99]. s1.utf8.countcounts bytes, which for lowercase ASCII letters equals the number of letters.Int(c)before the subtraction. The bytes areUInt8, and Swift does not mix integer types. Converting toIntfirst also keeps the subtraction from trapping: in Swift, arithmetic overflow, including going below zero on an unsigned type, crashes the program at run time.- Negative counts are normal.
needis an array ofInt, so a letter that the window holds too many of simply goes below 0. - The ranges.
0..<chars.countexcludes the upper bound, so the last pass uses the last index.
Complexity
Time: O(n + m). Counting the letters of s1 takes O(m). The window loop runs once per letter of s2 and does a constant number of operations on each pass, which is O(n). Copying s2 into chars is also O(n).
Space: O(1) for the counts, a fixed 26 entries. The chars copy of s2 is an array of n bytes, a convenience that Swift's string design makes necessary.
Edge cases and common mistakes
s1longer thans2."abc"in"ab"givesfalse: the window can't hold three letters out of two.- Equal strings.
"abc"in"abc"givestrue, since a string is a permutation of itself. A single letter works too:"a"in"a"givestrue. - Repeated letters.
"aab"in"baa"givestrue, but"aab"in"abbab"givesfalse: no window of three holds two a's and a b. Counting only which letters occur, not how many times, would wrongly accept the second. - Letters that are all there but not contiguous.
"ab"in"eidboaoo"givesfalse. The window must be a run of consecutive letters. - Removing the old letter too early. The check
last >= s1.utf8.countkeeps the window at exactly the length ofs1. If it were>, the window would hold one letter too many when tested. - Forgetting to update
missingwhen a letter leaves. The count inneedthen drifts frommissing, and later windows give wrong answers.
Testing
s1 = "ab",s2 = "eidbaooo"givestrue.s1 = "ab",s2 = "eidboaoo"givesfalse.s1 = "adc",s2 = "dcda"givestrue.s1 = "abc",s2 = "ab"givesfalse.s1 = "aab",s2 = "baa"givestrue.s1 = "aab",s2 = "abbab"givesfalse.s1 = "hello",s2 = "ooolleoooleh"givesfalse.
