题目来源
题目描述
class Solution {
public:
bool checkInclusion(string s1, string s2) {
}
};
题目解析
分析数据量
1 <= s1.length, s2.length <= 10^4
,因为两个10^4
相乘为10^6
,所以两个字符最多只能看一遍,就要得出答案,时间复杂度为O(N)
分析题意
str1,str2每种种类必须一样,个数必须一样,顺序可以不一样。这叫做编写次
滑动窗口 + 欠账表
思路
依次尝试固定以s2中的每一个位置l作为左端点开始的len1长度的子串s2[l…l+len1]是否是s2的排列。即可
如果固定l做左断点,尝试失败,继续尝试l+1位置位置,【左右边界都不需要回退】,这是滑动窗口的前提。
- 通过一个记账本,charCount作为【总账表】维护s1的词频表;
- 滑动窗口内每一个右边界字符进入窗口后,【还账】,charCount[str[r] - ‘a’]–
- 如果某个字符多还了(变成赋值),即尝试失败,开始尝试下一个左端点(l++);
- 左边界出窗口后,表示【重新赊账】:charCount[str2[l] - ‘a’]++
- 最终如果欠账还足了(窗口长度达到len1),则尝试成功,直接返回true。
class Solution {
int process( string str1, string str2){
std::vector<int> count (256);
int M = str1.size();
for (int i = 0; i < M; ++i) {
count[str1[i]]++;
}
int all = str1.size(); //一开始欠所有
int R = 0;
// 0~M-1
for (; R < M; R++) { // 最早的M个字符,让其窗口初步形成
if (count[str2[R]] > 0) { //--之前 >0才是有效还款
all--;
}
count[str2[R]]--;
}
// 窗口初步形成了,并没有判断有效无效,决定下一个位置一上来判断
// 接下来的过程,窗口右进一个,左吐一个
for (; R < str2.size(); R++) {
if (all == 0) { // R-1
return R - M;
}
if (count[str2[R]] > 0) { //--之前大于0才是有效还款
all--;
}
count[str2[R]]--;
if (count[str2[R - M]] >= 0) { //只有++之前>=0时才是有效吐出
all++;
}
count[str2[R - M]]++;
}
return all == 0 ? R - M : -1;
}
public:
bool checkInclusion(string s1, string s2) {
if(s1.size() > s2.size()){
return false;
}
return process(s1, s2) != -1;
}
};
class Solution {
public:
bool checkInclusion(string s1, string s2) {
if(s1 > s2){
return false;
}
int len1 = s1.size(); //借款人借出去了多少款项
int len2 = s2.size(); //还款人手里有多少可还款项
//账本,题目规定只包含小写字母
std::vector<int> charCountArr(26, 0);
//将借出去的东西先计入账本
for (char c : s1) {
charCountArr[c - 'a']++;
}
//开始尝试对每个款项分别还款
//首先定义还款窗口的左右边界指针
int left = 0, right = 0;
//从可还款项的第0位开始,如果移动次数已经超出len2 - len1了,无法从后面新增款数了,必然就还不完,所以外层循环直接跳出
while (left <= len2 - len1){
//开始尝试还款,每进入还款窗口的款项要尝试减一,而且,减掉后不能小于0,否则就还多了
while (right <= left + len1 && right < len2 && charCountArr[s2[right] - 'a'] > 0){
charCountArr[s2[right] - 'a']--; //对应的还款项代还次数减一
right++; //继续进入下一个还款项
}
//跳出上面循环会有如下情况:
//1、最后一次尝试了,right马上就越界了
//2、还没还完,出现某一个款项还多了
//3、本身就不需要还款(0),但是尝试还款
//4、刚好每个款项还完了,又尝试还一个本不该还的款项
//上面四种情况中,只有最后一种情况才算还款成功,这时还款窗口宽度刚好等于还款项数,才能算还款成功
if (right - left == len1) {
return true;
}
//如果是其他三种情况,则left在当前位置是不可能还款成功的,左边界的款项在进入还款窗口的时候,已经做过还账了,但是此时这种情况是恒定还款失败的,所以需要将这一次错误的或者是无效的还账取消,将这个款项重新赊账
//特别注意出现一种特殊情况的时候,如何理解下面这行代码
//如果第一次就尝试还一个本不该还的(0)款项,为什么还要赊账?这一步可以不理解为赊账了,left移动后right还没有移动,也就是right在left的左边,如果这个款项恒定为0,则right永远都不会移动走了,为了避免这样,我们可以假装先借着,下一次循环,必定能够还给他,循环才得以继续运行
charCountArr[s2[left] - 'a']++; // 重新【"赊账"】
//往后移动
left++;
}
return false; // 所有的左端点均尝试还账失败,不可能再有答案了
}
};
举个例子(例子待更改)
假设str1=ccaba
,str2=cccbabacbac....
(1)先使用str1做出一张欠账表
(2)遍历str2,开始花钱
(2.1) 先做出一个窗口来
当前窗口是不是str1的变形词呢?不是,因为all != 0
(2.2)开始移动窗口
- 每次移动一个单词,然后判断all 是否为0,如果发现中途有all = 0,那么就返回true