使用范围
此方法针对的对象是一段连续的区间。
做题模板:

区分子数组/子串、子序列、子集
子数组/子串是原数组中连续的一段区间,要求保持顺序,也要求连续。
子序列是原数组中删除若干元素后剩下的序列,不要求保持顺序,要求连续。
子集是从原来集合中任意选取一些元素,不要求保持顺序,也不要求连续。
举例说明
对于数组 [1, 2, 3, 4]:
子数组:[1,2]、[2,3,4]、[3]、[1,2,3,4]
注意:[1,3] 不是子数组(不连续)
子序列:[1,3]、[2,4]、[1,2,4]、[1,2,3,4]
注意:[2,1] 不是子序列(顺序改变)
子集:{1,3}、{2,4}、{1,2,4}、{1,2,3,4}
注意:{2,1} 和 {1,2} 是同一个子集(顺序无关)
题目实例
长度最小的子数组

子数组为一段连续的区间,可以考虑用滑动窗口的方法解答这个问题。
解题思路:
该题目需要我们找的是总和大于等于target的最小长度的子序列。根据总和,我们可以定义一个sum变量来实现进窗口,当加到总和大于等于target的时候,可以进行更新结果,最后在出窗口,进行后面数组的判断。
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int left=0,right=0;
int n=nums.size();
int sum=0,minlen=INT_MAX;
for(;right<n;right++)
{
sum+=nums[right];
while(sum>=target)
{
minlen=min(minlen,right-left+1);
sum-=nums[left];
left++;
}
}
return minlen==INT_MAX?0:minlen;
}
};
注:时间复杂度:虽然代码是两层循环,但是我们的 left 指针和 right 指针都是不回退的,两者
最多都往后移动 n 次。因此时间复杂度是 O(N) 。
无重复字符的最长子串

子串为一段连续的区间,可以考虑用滑动窗口的方法解答这个问题。
解题思路:哈希表+滑动窗口(记得维护哈希表)
右端元素进入窗口时,用哈希表统计元素个数;根据题意:不含重复的字符,这表明这段连续区间内每个元素都只有一个,那么我们就可以得到我们出窗口的条件:只要有一个元素在这段区间内超过1了就对该区间进行出窗口,直到该区间内无重复元素时对它进行更新结果。
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int hash[128]={0};
int left=0,right=0;
int maxlen=0;
for(;right<s.size();right++)
{
char ch=s[right];
hash[ch]++;
while(hash[ch]>1)
{
char ret=s[left];
hash[ret]--;
left++;
}
maxlen=max(maxlen,right-left+1);
}
return maxlen;
}
};
当只有小写字母或者大写字母时,哈希表只用hash[26];
当不止一种且都是ASCII中的元素时,哈希表用hash[128]。
最大连续 1 的个数 III
1004. 最大连续1的个数 III - 力扣(LeetCode)

根据题意:求取在一段区间内把里面的k个0变成1后,数组中连续1的最大个数,即求取连续区间内最大个数的1+k的最大个数。
解题思路:
用一个变量count来统计0在区间内出现的个数,根据题目:最多可以反转k个0,可以得出出窗口的条件:count>k,在出窗口的时候记得维护count变量。出窗口后更新结果。
class Solution {
public:
int longestOnes(vector<int>& nums, int k) {
int count=0;
int left=0,right=0;
int maxnum=0;
for(;right<nums.size();right++)
{
//for循环中right++已经是进窗口了
if(nums[right]==0)
{
count++;
}
//出窗口
while(count>k)
{
if(nums[left]==0)
{
count--;
}
left++;
}
//更新结果
maxnum=max(maxnum,right-left+1);
}
return maxnum;
}
};
将 x 减到 0 的最小操作数
1658. 将 x 减到 0 的最小操作数 - 力扣(LeetCode)

题意:移除最左边或最右边的数,使移除的数等于x
转换思维:题目中移除的是最右边或最左边的数,是不是只要保证中间的区域只要等于全部数的总和-x就行了,所以我们就转换求结果等于sum-x的最长子数组,最后用总长减最长子数组即可。
解题思路:
根据思路,我们可以在数组中找一段连续的区间等于sum-x即可,解题方法如第一道题一样,相当于这个题目的target=sum-x,但是更新结果和出窗口的位置不一样。这题是要在总和等于sum-x的时候更新结果,而出窗口的提交条件是区间中的和加起来大于target,所以不能在出窗口的时候更新结果,出完窗口后,我们才能更新结果。
细节问题:这里我们求的是最长的子数组,但是定义最长子数组长度的值不能初始化为0,因为当数组中全部数都大于x的时候,最长子数组的长度为0,而整个数组的和为x的时候,最长子数组长度的和也为0,但是第一种输出的结果为-1,第二个输出的是数组的长度。
class Solution {
public:
int minOperations(vector<int>& nums, int x) {
//求取中间连续区间的值等于总和-x
long long sum=0;
for(auto &s:nums)
{
sum+=s;
}
if(sum<x)
{
return -1;
}
int left=0,right=0;
int maxnum=-1;
long long ret=0;
long long target=sum-x;
for(;right<nums.size();right++)
{
ret+=nums[right];
while(ret>target)
{
//出窗口
ret-=nums[left];
left++;
}
if(ret==target)
maxnum=max(maxnum,right-left+1);
}
return maxnum==-1?-1:nums.size()-maxnum;
}
};
水果成篮

题意解析:一共有两个篮子,每个篮子只能放同一种水果(也就是数组中数字一样的)例如:
fruits = [1,2,3,2,2] 这个数组中有三种水果,分别是1,2,3.
解题思路:哈希表+滑动窗口
进窗口:把右端的值放入到哈希表中,哈希表中统计每个水果的个数。
出窗口:题目中的限制条件是两个篮子,那么两个篮子就可以设置成出窗口的条件。
更新结果:更新最大数目
哈希表可以用两种形式的:unordered_map<int,int>hash,int hash[100001]={0};
如果用unordered_map这种容器的哈希表,记得元素的个数为0的时候要删除元素。
int totalFruit(vector<int>& fruits) {
unordered_map<int,int>hash;
int left=0,right=0,n=fruits.size();
int maxlen=-1;
for(;right<n;right++)
{
hash[fruits[right]]++;
while(hash.size()>2)
{
//出窗口
hash[fruits[left]]--;
if(hash[fruits[left]]==0)
{
hash.erase(fruits[left]);
}
left++;
}
maxlen=max(maxlen,right-left+1);
}
return maxlen;
}
};
class Solution {
public:
int totalFruit(vector<int>& fruits) {
int hash[100001]={0};
int left=0,right=0,n=fruits.size();
int maxlen=-1;
int count=0;//水果的种类
for(;right<n;right++)
{
if(hash[fruits[right]]++==0)
{
count++;
}
while(count>2)
{
//出窗口
hash[fruits[left]]--;
if(hash[fruits[left]]==0)
{
count--;
}
left++;
}
maxlen=max(maxlen,right-left+1);
}
return maxlen;
}
};
找到字符串中所有字母异位词
438. 找到字符串中所有字母异位词 - 力扣(LeetCode)

求的是子串,子串是一段连续的区间,可以考虑滑动窗口。
解题思路:哈希+滑动
定义一个变量来统计在s中有多少符合p个数的字符串count,还定义两个哈希表,一个哈希表hash1来统计p中所有个数,一个哈希表hash2来统计s中所有个数,这三个变量都是在接下来的解题中需要维护的。
进窗口:把s中的字符统计的同时,维护count变量,统计有多少个符合p个数的字符串,可以通过hash2[s[right]]<=hash1[s[right]]统计。
出窗口:如果用count>right-left+1来作为出窗口的条件的话,那么就只是表明在这个数组中right-left+1这一段里面没有连续的符合p的子串,而且这个条件也不能判断count是不是等于p.size(),所以这个不能作为判断条件。判断要和p的大小有关,而且要符合长度,所以用right-left+1>p.size()作为判断条件。出窗口时我们要维护count变量,hash2变量和left变量。
更新结果:判断条件:count==p.size()
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
//哈希+滑动
unordered_map<char,int>hash1;
unordered_map<char,int>hash2;
vector<int >result;
for(auto &s:p)
{
hash1[s]++;
}
int left=0,right=0;
int count=0;//符合的个数
for(;right<s.size();right++)
{
char ch=s[right];
if(++hash2[ch]<=hash1[ch])count++;
while(right-left+1>p.size())
{
//出窗口
char i=s[left];
if(hash2[i]--<=hash1[i])count--;
left++;
}
if(count==p.size())
{
result.push_back(left);
}
}
return result;
}
};
串联所有单词的子串

题意解析:找到数组中一段包含words中所有单词的连续区间,单词的顺序可以任意。连续区间,可以考虑用滑动窗口。
关键信息:words中所有字符串长度一样。
解题思路:
把数组分割成words中单词长度的大小,但是我们怎么知道要从哪里开始分割才能遇到words中的单词呢?这时候我们就需要一个循环,这里可以优化:但我们循环超过words字符串长度一样的时候,发现和从0开始遍历的单词是一样的了,所以我们可以缩小循环次数在单词长度之间。
这题上题解题类似,只是把字符看成字符串就行。
细节部分:count的变量应该放在循环里,遍历字符串s的哈希表也要放在循环里,不然哈希表每次遍历都不是新的结果。
class Solution {
public:
vector<int> findSubstring(string s, vector<string>& words) {
unordered_map<string, int> hash1;
vector<int> result;
for (auto& s : words) {
hash1[s]++;
}
int left = 0, right = 0;
int m = words[0].size();
for (int i = 0; i < m; i++) {
int count = 0;
unordered_map<string, int> hash2;
for (right=i,left=i; right < s.size(); right += m) {
string rs = s.substr(right, m);
if (++hash2[rs] <= hash1[rs]) {
count++;
}
while (right - left + 1 > m * words.size()) {
string ls = s.substr(left, m);
if (hash2[ls]-- <= hash1[ls]) {
count--;
}
left += m;
}
if(words.size()==count)
{
result.push_back(left);
}
}
}
return result;
}
};
最小覆盖子串

题目解析:寻找一段连续区间中只要包含t中所有的字符的最短子串,即最短子串中可以存在其他字符,但是一定要全部包含t中所有的字符。
解题思路:和找到字符串中所有字母异位词的解题思路一样,但是这题要求的是返回字符串。
进窗口:遍历右端元素进入哈希表
出窗口:根据题意,寻找的是最短的子串,那么也就是说最好最后一个结尾的就是t元素中包含的全部字符的最后一个字符,所以这里的出窗口的条件就是当区间内刚好count==t.size()。这个时候就是刚好符合题目的要求,所以我们要在出窗口前更新结果,而题目返回的是字符串,那么我们可以通过string容器中的substr函数来进行结果返回,sustr有两个参数,一个是起始位置,一个是截取多少个字符,所以我们要定义两个变量来记录。
细节部分:begin设置为-1,记录子串起始位置的值,返回的时候要对begin进行判断。
class Solution {
public:
string minWindow(string s, string t) {
unordered_map<char,int>hash1;
for(auto&ch:t)
{
hash1[ch]++;
}
int left=0,right=0;
int minlen=INT_MAX;
int count=0;
int begin=-1;
unordered_map<char,int>hash2;
for(;right<s.size();right++)
{
if(++hash2[s[right]]<=hash1[s[right]])count++;
//出窗口
while(count==t.size())
{
if(minlen>right-left+1)
{
minlen=min(minlen,right-left+1);
begin=left;
}
if(hash2[s[left]]--<=hash1[s[left]])count--;
left++;
}
}
if(begin==-1)
return "";
else
return s.substr(begin,minlen);
}
};