Problem overview
The function checkInclusion receives two strings of lowercase letters, s1 and s2. It returns true when some substring of s2 is a permutation of s1, and false otherwise. A permutation uses the same letters as s1, each as many times, in any order: the permutations of "abb" are "abb", "bab" and "bba". A substring is a run of consecutive characters.
So the question is whether s2 has a run of letters, exactly as long as s1, with the same letter counts as s1.
With s1 = "ab" and s2 = "eidbaooo" the answer is true: the run "ba" at indexes 3 and 4 is a permutation of "ab". With s2 = "eidboaoo" it is false: s2 has an a and a b, but never side by side. With s1 = "adc" and s2 = "dcda" it is true, because the last three letters, "cda", hold one a, one c and one d.
The lesson asks for O(n + m) time, where n is the length of s2 and m the length of s1, and O(1) extra space.
Approach: a window that keeps its counts up to date
The slow way is to take every substring of s2 of length m and count its letters from scratch. That costs O(n * m) time, too slow for strings of 100,000 letters.
The faster way slides a window of length m across s2 and updates the counts as it moves. When the window moves one step, one letter enters on the right and one letter leaves on the left, so only two counts change. No substring is ever counted again from scratch.
We don't compare two count arrays at every step, either. We keep one array, need, with 26 counts, one per letter. need[0] says how many more a's the window still needs, need[1] how many more b's, and so on. At the start, need holds the counts of s1. A letter that enters the window lowers its count, a letter that leaves raises it. A second number, missing, starts at m and counts how many letters of s1 the window still lacks. When missing is 0 and the window is exactly m letters long, it has all the letters of s1 with no room for others, so it is a permutation.
Here is a run on s1 = "ab", s2 = "eidbaooo". Each line shows the letter that just entered and the value of missing after the window is updated:
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 e, i and d are not in s1, so missing stays at 2. The b brings it to 1. At last=4, the a arrives and the e leaves, and missing reaches 0: the window is "ba".
With s1 = "adc" and s2 = "dcda" the run is:
last=0 letter=d missing=2
last=1 letter=c missing=1
last=2 letter=d missing=1
last=3 letter=a missing=0
At last=2 the second d is extra, so missing stays 1. At last=3 the first d leaves and the a arrives: the window is "cda".
Step by step
- Create
needwithmake([]int, 26): 26 zeros, one per letter. - Count the letters of
s1. Aforloop withrangeovers1gives each character asc; the index is dropped with_. Inside,need[c-'a']++adds one. The letters a to z have consecutive codes, soc-'a'is 0 for a, 1 for b, and so on. A letter that appears twice ins1is needed twice. - Set
missingtolen(s1): before the window holds anything, it lacks every letter. - Slide over
s2. The loop variablelastis the index of the window's right end. Each turn adds the letter atlast. - Add the new letter.
codeiss2[last] - 'a', the letter's number from 0 to 25. Ifneed[code]is above 0,s1still needed this letter, somissing--. A count of 0 or less means the window already had all the copiess1asks for, so this one is extra andmissingstays. Thenneed[code]--in every case. It can drop below 0, which means the window has more of that letter thans1. - Remove the old letter once the window is too long. When
last >= len(s1), the window has m + 1 letters, so the first one,s2[last-len(s1)], leaves. Compute its number inoldCodeand doneed[oldCode]++, which undoes what its arrival did. Ifneed[oldCode]is now above 0, the window lost a letter thats1needs, somissing++. Otherwise it was a spare copy and nothing is lost. - Test the window. If
missing == 0, returntrue. In the first turns the window is shorter thans1, so it can't hold all of its letters yet andmissingcan't be 0. - Return
falseafter the loop. Ifs1is longer thans2,missingnever reaches 0, so this is the answer as well.
The solution in Go
func checkInclusion(s1 string, s2 string) bool {
need := make([]int, 26)
for _, c := range s1 {
need[c-'a']++
}
missing := len(s1)
for last := 0; last < len(s2); last++ {
code := s2[last] - 'a'
if need[code] > 0 {
missing--
}
need[code]--
if last >= len(s1) {
oldCode := s2[last-len(s1)] - 'a'
need[oldCode]++
if need[oldCode] > 0 {
missing++
}
}
if missing == 0 {
return true
}
}
return false
}
Go details worth knowing
rangeover a string gives runes. Infor _, c := range s1,cis arune(anint32), the code of the character. Here the string is ASCII, so each character is one rune. For "ab",rangeyields index 0 with code 97 and index 1 with code 98.- Indexing a string gives a byte.
s2[last]is auint8(byte), not a character. Subtracting'a'gives anotherbytebetween 0 and 25 for a lowercase letter, which is a valid index for the sliceneed. - Two kinds of subtraction.
c-'a'is anint32, ands2[last]-'a'is abyte. Both work as slice indexes. An index that is an integer type of any width is accepted. lencounts bytes. For lowercase English letters one byte is one letter, solen(s1)is the number of letters.- A slice of 26 ints plays the role of an array of counters. A map would work, too, but the slice is faster and its size doesn't depend on the input.
Complexity
Time: O(n + m). Counting s1 takes m steps. The window loop runs n times with constant work per turn: each letter of s2 enters the window once and leaves it once.
Space: O(1). The 26-element slice and a few integers don't grow with the input.
Edge cases and common mistakes
s1longer thans2. No window of the right length exists.checkInclusion("abc", "ab")returnsfalse.- Same length. The only window is
s2itself.checkInclusion("a", "a")returnstrue. - Repeated letters in
s1. Having the right letters is not enough; the counts matter.checkInclusion("aab", "abcb")returnsfalse, butcheckInclusion("aab", "abab")returnstrue, thanks to the window"aba". - Letters in the wrong places. The letters of
s1may be spread out ins2without making a permutation:checkInclusion("abc", "ccccbbbbaaaa")returnsfalse. The substring has to be consecutive. - Counting the letters going out wrongly. The test
need[oldCode] > 0after the increment matters. Raisingmissingfor every leaving letter would be wrong when the window held a spare copy of it. - Forgetting that the window must be exactly m long. Without the removal step,
missingcould reach 0 for a window longer thans1, which can hold extra letters.
Testing
"ab","eidbaooo"givestrue."ab","eidboaoo"givesfalse."adc","dcda"givestrue."abc","ab"givesfalse."a","a"givestrue."aab","abab"givestrue, and"aab","abcb"givesfalse.
