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 of s1 uses the same letters, each one as many times as in s1, in any order. A substring is a run of consecutive characters of s2.
So the real question is: does s2 have a run of letters, exactly as long as s1, that 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" it is true: the last three letters, "cda", have the same letters as "adc", while the first three, "dcd", have two d's and no a.
Approach: a window with a running count
The direct way builds every substring of s2 of length s1.size() and compares its letter counts with those of s1. With strings up to 100,000 letters that takes O(n · m) time, too slow.
Notice that two neighbouring windows share almost all their letters. Sliding the window one step to the right adds one letter on its right end and removes one on its left end. Instead of recounting, keep a running tally and update it for those two letters only.
The tally is an array need of 26 counters, one per letter. Start with need holding the counts of s1: that is what an empty window still needs. When a letter enters the window, its counter goes down by one. When a letter leaves, it goes up by one. A counter at 0 means the window holds exactly as many copies of that letter as s1. A negative counter means the window holds too many.
Checking all 26 counters at every step would cost 26 operations per step. A single integer, missing, avoids that. It counts how many letters of s1 the window still lacks, starting at s1.size(). A letter only reduces missing when it was really needed, that is, when its counter was above 0. When missing reaches 0, the window holds every letter of s1 with its full multiplicity. The window is as long as s1, so it has no room for anything else: it is a permutation.
Here is the walk for s1 = "ab" and s2 = "eidbaooo", showing the window after the step is done:
| last | letter | window | missing |
|---|---|---|---|
| 0 | e | e | 2 |
| 1 | i | ei | 2 |
| 2 | d | id | 2 |
| 3 | b | db | 1 |
| 4 | a | ba | 0 |
At last = 4, missing is 0 and the method returns true.
For s1 = "adc" and s2 = "dcda", the second d at last = 2 drives the counter for d to -1 without lowering missing, which stays at 1. At last = 3 the window drops its first d, the counter for d returns to 0 and, since it is not above 0, missing does not grow. The new a lowers missing to 0, so the answer is true.
Step by step
- Create
need, avector<int>of size 26 filled with 0. Index 0 is for a, index 1 for b, and so on. - Loop over
s1with a range-basedforand achar c. - Count the letter:
need[c - 'a']++. Acharis a small number, and the codes of a to z are consecutive, soc - 'a'is 0 for a, 1 for b and so on. A letter that appears twice ins1is needed twice. - Set
missingtos1.size(): the window is empty, so it lacks everything. - Slide over
s2. The loop variablelastis the index of the letter that enters the window on its right end. - Convert the entering letter to a number from 0 to 25 and store it in
code. - If
need[code]is above 0,s1still needed this letter, so decreasemissing. At 0 or below, this copy is extra andmissingstays. - Decrease
need[code], because the window now holds one more of this letter. The counter may become negative. - Check whether the window got too long:
last >= s1.size()means it now hass1.size() + 1letters, so the first one has to leave. - Find the leaving letter,
s2[last - s1.size()] - 'a', inoldCode. It is the letters1.size()places to the left of the new one. - Increase
need[oldCode], undoing what that letter did when it came in. - If
need[oldCode]is now above 0, the window lost a letter thats1needs, so increasemissing. If the counter is 0 or below, the window had a spare copy and lost nothing. - If
missingis 0, returntrueright away, inside the loop. - Return
falseafter the loop. No window matched. Ifs1is longer thans2,missingnever reaches 0, so this is also the answer there.
The solution in C++
class Solution {
public:
bool checkInclusion(string s1, string s2) {
vector<int> need(26, 0);
for (char c : s1) {
need[c - 'a']++;
}
int missing = s1.size();
for (int last = 0; last < s2.size(); last++) {
int code = s2[last] - 'a';
if (need[code] > 0) missing--;
need[code]--;
if (last >= s1.size()) {
int oldCode = s2[last - s1.size()] - 'a';
need[oldCode]++;
if (need[oldCode] > 0) missing++;
}
if (missing == 0) return true;
}
return false;
}
};
C++ details worth knowing:
- Arithmetic on
char.s2[last] - 'a'is anint. It is a valid index only because the input holds lowercase letters. A capital letter or a digit would give an index outside 0 to 25 and the code would read or write outside the vector. - Signed and unsigned.
s1.size()is unsigned, andlastis anint. Inlast >= s1.size()and inlast - s1.size(),lastis converted to unsigned. That is safe here:lastis never negative, and the subtraction only runs whenlast >= s1.size(), so it can't wrap. Without that guard,last - s1.size()withlast = 1and a three-letters1would wrap to a huge number. - Strings by value. The method takes
string s1, string s2by value, which copies both strings once. That is fine for the sizes here and does not change the complexity. - Keeping the early return. The check
missing == 0sits inside the loop after the update, so the window is tested at every position.
Complexity
Time: O(n + m). Counting s1 takes m steps. Each letter of s2 enters the window once and leaves it at most once, and each of those updates costs constant time, giving n turns of constant work.
Space: O(1). The need vector always has 26 elements, however long the strings are.
Edge cases and common mistakes
s1longer thans2."abc"in"ab"returnsfalse: the window never gets enough letters, somissingnever reaches 0.- Repeated letters.
"aa"in"aba"returnsfalse, because the two a's are not adjacent, while"abb"in"xbabx"returnstruethrough the window "bab". Checking only which letters are present would not be enough: the counts matter. - Counting the entering letter twice, or forgetting the leaving one. The window would grow beyond
s1.size()and would accept strings with extra letters. - Testing
missingbefore the window is full. In the first turns the window is shorter thans1, somissingcannot be 0 yet. The method needs no special case for that. - Single letters.
"a"in"a"givestrueand"a"in"b"givesfalse.
Testing
s1 = "ab",s2 = "eidbaooo"givestrue.s1 = "ab",s2 = "eidboaoo"givesfalse.s1 = "adc",s2 = "dcda"givestrue.s1 = "abb",s2 = "xbabx"givestrue.s1 = "aa",s2 = "aba"givesfalse.s1 = "abc",s2 = "ab"givesfalse.
