Problem overview
The method 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 the same number of times in any order, so "abb", "bab" and "bba" are all permutations of each other. A substring is a run of consecutive characters.
With s1 = "ab" and s2 = "eidbaooo" the answer is True: the letters at indexes 3 and 4 are "ba". With s2 = "eidboaoo" the answer is False. That string has an a and a b, but never next to each other. With s1 = "adc" and s2 = "dcda" the answer 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: one count for each of the 26 letters.
Approach: a window as long as s1, with a count of what it still lacks
A matching substring has exactly len(s1) letters, so only windows of that length matter. The slow approach counts the letters of each window from scratch, which costs O(n · m). The faster approach slides one window across s2 and updates the counts as it moves: one letter comes in on the right and, once the window is full, one leaves on the left. Each letter of s2 enters once and leaves once.
The list need has one slot per letter and describes what the window still needs. At the start it holds the letters of s1. When a letter enters the window its slot goes down by 1, and when a letter leaves it goes up by 1. A slot above 0 means the window lacks copies of that letter. A slot at 0 or below means the window has all it needs, and a negative slot means it holds extras.
Checking all 26 slots after every move would cost 26 steps each time. A single counter, missing, avoids that: it counts the letters of s1 the window still lacks. It starts at len(s1) and changes only when a needed copy comes in (it goes down) or a needed copy leaves (it goes up). When missing reaches 0, the window holds every letter of s1 as many times as s1 has it. A window of exactly that length has no room for anything else, so it is a permutation.
Here is the first example, s1 = "ab" and s2 = "eidbaooo". The last column lists the nonzero slots of need after each turn:
last | new letter | missing | nonzero slots of need |
|---|---|---|---|
| 0 | e | 2 | a: 1, b: 1, e: -1 |
| 1 | i | 2 | a: 1, b: 1, e: -1, i: -1 |
| 2 | d | 2 | a: 1, b: 1, d: -1, i: -1 |
| 3 | b | 1 | a: 1, d: -1 |
| 4 | a | 0 | (none) |
At last = 2 the e leaves, because the window is longer than s1. At last = 4 the window is "ba", nothing is missing, and the method returns True.
Step by step
- Create
needas[0] * 26: slot 0 for a, slot 1 for b, and so on. - Loop over
s1withcas the letter. - Count each letter by adding 1 to
need[ord(c) - ord('a')].ordgives a character's code, and the codes of a to z are consecutive, so subtracting the code of'a'gives 0 to 25. - Set
missingtolen(s1): the empty window lacks every letter. - Slide the window with a loop where
lastgoes overrange(len(s2)). Each turn adds the letter atlaston the right. - Compute the new letter's slot and store it in
code. - If
need[code]is above 0, this letter was still needed: subtract 1 frommissing. - Subtract 1 from
need[code], whether or not it was needed. It can go negative. - Check whether the window is too long with
last >= len(s1). If so, the first letter,s2[last - len(s1)], has to leave. - Compute the leaving letter's slot and store it in
oldCode. - Add 1 to
need[oldCode]: the window no longer holds it. - If
need[oldCode]is now above 0, the window lost a letter thats1needs: add 1 tomissing. - If
missingis 0, returnTrue. - After the loop, return
False. No window matched. Whens1is longer thans2,missingnever reaches 0, so this is the answer too.
The solution in Python
class Solution:
def checkInclusion(self, s1: str, s2: str) -> bool:
need = [0] * 26
for c in s1:
need[ord(c) - ord('a')] += 1
missing = len(s1)
for last in range(len(s2)):
code = ord(s2[last]) - ord('a')
if need[code] > 0:
missing -= 1
need[code] -= 1
if last >= len(s1):
oldCode = ord(s2[last - len(s1)]) - ord('a')
need[oldCode] += 1
if need[oldCode] > 0:
missing += 1
if missing == 0:
return True
return False
Python details worth knowing:
- Why test before subtracting. The check
need[code] > 0must run beforeneed[code] -= 1. A letter that arrives when its slot is 0 or negative is a spare copy and must not lowermissing. - Likewise on the way out. The slot goes up first, then the test
need[oldCode] > 0runs. Only a slot that ends above 0 means the window gave up a copy thats1needs. - Characters are strings. Iterating over a string gives one-character strings.
ordturns one into its integer code, andord('a')is the base for the subtraction. - Negative slots are fine. A list slot has no lower bound. A count of -1 simply records an extra copy in the window.
Complexity
Time: O(n + m). Counting s1 takes m steps. The main loop runs n times and does a constant amount of work in each turn: a few list reads and writes and no inner loop.
Space: O(1). The list need always has 26 slots, whatever the lengths of the strings.
Edge cases and common mistakes
s1longer thans2."abc"and"ab"returnFalse. The window never reaches the length ofs1, somissingstays above 0.- Repeated letters.
"abb"in"ab"returnsFalse, and"aab"in"abaa"returnsTrue(the window "baa"). A plain set of letters would treat "ab" like "aab", so counts are needed, not just presence. - Single letters.
"a"in"a"returnsTrueand"a"in"b"returnsFalse. Whens1has one letter, the window is full from the first turn. - Window one letter off. The test
last >= len(s1)removes a letter only once the window holdslen(s1) + 1letters. Using>would let the window grow tolen(s1) + 1letters, which can report a match that doesn't exist:s1 = "ab"ins2 = "axb"would returnTrue, though the correct answer isFalse. - Comparing whole count lists. Comparing two 26-slot lists for every window works and is correct, but costs 26 steps per turn. The
missingcounter keeps each turn constant.
Testing
s1 = "ab",s2 = "eidbaooo"givesTrue.s1 = "ab",s2 = "eidboaoo"givesFalse.s1 = "adc",s2 = "dcda"givesTrue.s1 = "abc",s2 = "ab"givesFalse.s1 = "abb",s2 = "ab"givesFalse.s1 = "aab",s2 = "abaa"givesTrue.s1 = "a",s2 = "b"givesFalse.
