Problem overview
Given two strings of lowercase letters, s1 and s2, the method returns true when s2 contains a permutation of s1 as a substring. A permutation uses the same letters as s1, each as many times, in any order. A substring is a run of consecutive characters. So the question is whether some run of s2, exactly as long as s1, holds every letter as many times as s1 does.
For s1 = "ab" and s2 = "eidbaooo" the answer is true, because s2 contains "ba" at indexes 3 and 4. For s2 = "eidboaoo" it is false: there is an a and a b, but never next to each other. For s1 = "adc" and s2 = "dcda" the answer is true because of the last three letters, "cda".
Approach
The direct method takes every substring of s2 with the length of s1, counts its letters and compares them with the letters of s1. With strings of 100,000 letters that is O(n · m) work, too slow.
The window method avoids recounting. The window is a run of s2 exactly as long as s1. When it slides one place to the right, one letter enters on the right and one leaves on the left, so a count array can be updated in constant time. The lesson keeps one array of 26 counts, need, with this meaning: need[x] is how many more copies of letter x the window still needs. A negative value means the window holds more of that letter than s1 has. Next to it, a counter missing says how many letters of s1 the window still lacks. When missing is 0, the window holds every letter of s1 as many times as s1 has it, and since the window is exactly as long as s1, it has no room for anything else: it is a permutation.
Each letter of s2 enters the window once and leaves it once, so the time is O(n + m), and the 26 counts are constant extra space.
Step by step
- The counts. Create
val need = IntArray(26), 26 zeros. Avalarray can still change its elements. - Count
s1. Loopfor (c in s1)and doneed[c - 'a']++. Subtracting twoChars gives anInt, and 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. - The missing counter. Start
var missing = s1.length: before the window holds anything, every letter is missing. - Slide. Loop
lastovers2.indices. Each turn, the letter at indexlastenters the window on its right end. - The entering letter. Store its number in
val code = s2[last] - 'a'. Ifneed[code] > 0,s1still needed this letter, somissing--. If the count is 0 or less, the window already has all the copiess1asks for, so this one is extra andmissingstays. - Update the count. Do
need[code]--either way: the window holds one more of this letter. - Drop the leaving letter. When
last >= s1.length, the window has become one letter too long, so the first letter,s2[last - s1.length], must leave. Store its number inoldCode, doneed[oldCode]++, and if the count is now above 0 the window lost a letter thats1needs, somissing++. - Test. If
missing == 0, returntrue. - No window matched. After the loop, return
false. This also covers the case wheres1is longer thans2:missingcan never reach 0.
The solution in Kotlin
class Solution {
fun checkInclusion(s1: String, s2: String): Boolean {
val need = IntArray(26)
for (c in s1) {
need[c - 'a']++
}
var missing = s1.length
for (last in s2.indices) {
val code = s2[last] - 'a'
if (need[code] > 0) missing--
need[code]--
if (last >= s1.length) {
val oldCode = s2[last - s1.length] - 'a'
need[oldCode]++
if (need[oldCode] > 0) missing++
}
if (missing == 0) return true
}
return false
}
}
Kotlin details:
for (c in s1)goes over the characters of aStringdirectly, ands2.indicesis the range0 until s2.length.'d' - 'a'is anInt(it is 3). ACharminus aChargives anInt, which is what an array index needs, so no.codecall is required.need[code]--works on an array element: it reads, subtracts 1 and stores back.- One-line
if (...) missing--without braces is valid Kotlin when the body is a single statement. - The function returns from inside the loop. Reaching the last line means every window failed.
A trace
With s1 = "ab" and s2 = "eidbaooo", this is the state after each turn (a and b are need[0] and need[1]), until the method stops:
last=0 letter=e a=1 b=1 missing=2
last=1 letter=i a=1 b=1 missing=2
last=2 letter=d a=1 b=1 missing=2
last=3 letter=b a=1 b=0 missing=1
last=4 letter=a a=0 b=0 missing=0
Letters that s1 doesn't contain, like e, i and d, only push their own counts below zero and are later undone when they leave. At last=4 the window is "ba", missing is 0, and the method returns true.
A second trace, with a repeated letter: s1 = "aab" and s2 = "abab".
last=0 letter=a a=1 b=1 missing=2
last=1 letter=b a=1 b=0 missing=1
last=2 letter=a a=0 b=0 missing=0
Here s1 has two a's, so need[0] starts at 2, and the window "aba" is a permutation of "aab".
Complexity
Time: O(n + m), where n is the length of s2 and m the length of s1. Counting s1 takes m steps, and the window loop runs n turns of constant work.
Space: O(1). The array has 26 entries however long the strings are.
Edge cases and common mistakes
s1longer thans2.checkInclusion("abc", "ab")returnsfalse. The window never gets long enough, somissingstays above 0.- Equal single letters.
checkInclusion("a", "a")returnstrue. - Repeated letters. Counts, not just sets, are needed.
"aab"in"abbaa"is found (the run"baa"), and"hello"is not found in"ooolleoooleh". - Letters from outside
s1. They may enter and leave the window freely; they push their counts below 0, and the window can't reachmissing == 0while one of them is inside, because the window is only as long ass1. - Changing
missingfor spare copies. Only a letter thats1still needs (a count above 0 before it enters, or after it leaves) changesmissing. Adjusting it on every enter or leave makes it wrong as soon as a letter is repeated or comes from outsides1.
Testing
These calls were run against the solution above:
s1 | s2 | Result |
|---|---|---|
"ab" | "eidbaooo" | true |
"ab" | "eidboaoo" | false |
"adc" | "dcda" | true |
"abc" | "ab" | false |
"a" | "a" | true |
"aab" | "abbaa" | true |
"hello" | "ooolleoooleh" | false |
