双指针问题的一些想法
- 一、[无重复字符的最长子串](https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/)
- 二、[最小覆盖子串](https://leetcode-cn.com/problems/minimum-window-substring/)
- 三、[长度最小的子数组](https://leetcode-cn.com/problems/minimum-size-subarray-sum/)
- 四、[替换后的最长重复字符](https://leetcode-cn.com/problems/longest-repeating-character-replacement/)
- 五、[找到字符串中所有字母异位词 ](https://leetcode-cn.com/problems/find-all-anagrams-in-a-string/)
- 六、[ 字符串的排列 ](https://leetcode-cn.com/problems/permutation-in-string/)
- 七、[子数组最大平均数 I](https://leetcode-cn.com/problems/maximum-average-subarray-i/)
- 七、[ K 个不同整数的子数组 ](https://leetcode-cn.com/problems/subarrays-with-k-different-integers/)
一、无重复字符的最长子串
这道题是比较经典的双指针题目,要求找到不含重复字符的最长子串,朴素想法是二重循环,找到每个区间计算。但是其中存在多余的计算,比如对一个存在重复字符的子串,包含它的子串都不符合要求,而且我们的目标是找最长的子串。
这道题里双指针的想法是,对每个右指针j,都找到其最靠左的左指针i使得区间符合条件,这样,当j每次右移的时候,i绝对不需要向左再移动,因为这样的区间不合法。
class Solution {
public:
int ch[10000];
int lengthOfLongestSubstring(string s) {
int res=0;
for(int i=0,j=0;j<s.size();j++)//i,j停留在上一个i最靠左的无重复无重复子串区间
{
ch[s[j]]++;
while(ch[s[j]]>1)//每次只加入一个字符,假如出现重复,一定是新加入的导致重复,因而移动i找到下一个无重复的子区间,单调性体现在i不能再向左,只能向右
{
ch[s[i]]--;
i++;
}//res能遍历到所有的无重复字符子串的情况,故正确
res=max(res,j-i+1);
}
return res;
}
};
通过这道题可以学习到双指针的基础模板,内层循环对应的是i++的情况,至于i++对应的是合法结果还是非法结果视题目而定,这里由于while循环里对应的状态非法,所以结果更新在while循环后进行;此外,这里还要学习对于变化量的处理方式,我们遇到一个新区间完全不需要判断这里面哪个字母重复,因为之前停留到上个没有重复字母的区间,所以出现重复的一定是新加入的。这样简练的思考处理方式之后也可以看到。
二、最小覆盖子串
题干要求找到s中覆盖t所有字符的最小子串,暴力方法依然是二重循环。这里考虑的优化方法是,由于每次加入一个新的字符,可能使得目前的子串满足条件,此时不需要右端点再向右移动,从而只需要缩减左端点即可,while循环的判断条件变为了维持覆盖的情况,从而找到了每个j的左边最靠右侧的满足条件的i,通过这样的思路遍历所有满足条件的子串。
判断覆盖有个比较取巧的方法,定义dist为匹配数量,只有字符在目标串中出现的数量大于目前串内的数量时,需要增加;同理,只有在当前子串中出现的数量小于等于目标串里的数量,需要减少,具体理解可以看代码。
这里while循环里结果是合法结果,因而在while里更新答案。
class Solution {
public:
vector<int> t_nozero;
string minWindow(string s, string t) {
if(t.size()>s.size()) return "";
vector<int> t_hash((int) 'z'+1,0);
for(auto ti : t) { t_hash[ti]++;}
int l=0,r=0;
vector<int> res((int)'z'+1,0);
int distance=0;
int minlen = INT_MAX,minstart=0;
for(;r<s.size();r++)
{
if(res[s[r]]<t_hash[s[r]]) distance++;
res[s[r]]++;
bool flag=true;
while(distance>=t.size())
{
if(minlen>r-l+1)
{
minlen=r-l+1;
minstart=l;
}
flag=false;
if(res[s[l]]<=t_hash[s[l]]) distance--;
res[s[l]]--;
l++;
}
}
if(minlen!=INT_MAX) return s.substr(minstart,minlen);
return "";
}
//之前时间主要耗费在拷贝上,实际上只要每次在while里记录下当前最短的情况即可,这样就无需之后复盘上一次是否最短
};
之前实现的较慢的方法如下,慢在判断子串的覆盖上,且结果更新放在了外层,导致判断起来较为复杂:
class Solution {
public:
vector<int> t_nozero;
string minWindow(string s, string t) {
if(t.size()>s.size()) return "";
vector<int> t_hash((int) 'z'+1,0);
for(int i=0;i<t.size();i++) { t_hash[t[i]]++; t_nozero.push_back((int) t[i]);}
int l=0,r=0;
vector<int> res((int)'z'+1,0);
string resstr=s;
for(;r<s.size();r++)
{
res[s[r]]++;
bool flag=true;
while(contain(res,t_hash))
{
flag=false;
res[s[l]]--;
l++;
}
if(!flag)
{
l--;
res[s[l]]++;
}
if(contain(res,t_hash)) resstr=(resstr.size()>r-l+1)?s.substr(l,r-l+1):resstr;
}
if(contain(res,t_hash)) return resstr;
return "";
}
bool contain(vector<int> res,vector<int> t_hash)
{
for(int i=0;i<t_nozero.size();i++)
if(res[t_nozero[i]]<t_hash[t_nozero[i]]) return false;
return true;
}
};
三、长度最小的子数组
仿照第一道题的思路可以很快写出来,每当区间值>=target时,就尝试缩小区间,直到区间值小于target。
这道题主要考虑好不成立的处理方式,如果res从来没有被更新过,说明不存在对应区间,结果为0。
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int res=INT_MAX,sum=0;
for(int i=0;i<nums.size();i++) sum+=nums[i];
if(sum<target) return 0;
sum=0;
for(int i=0,j=0;j<nums.size();j++)
{
sum+=nums[j];
while(sum>=target)
{
sum-=nums[i];
i++;
res=min(res,j-i+2);//循环内的如果是合法状态,则直接在循环里更新结果
}
}
if(res==INT_MAX) return 0;
return res;
}
};
通过以上几道题发现,双指针的套路都是当区间符合或者不符合结果时,更新左指针,并在这个过程中更新答案。
四、替换后的最长重复字符
这道题一开始可能想不到双指针的想法。因为对每个区间,我们都要判断其是否能够通过替换从而变为一样的字符串。最开始的思路是每次都更新一下当前区间里最多的字符数量,若剩下的字符数量比k大,表明是不合法区间,移动左指针,一直到区间满足要求即可,然后更新结果。考虑方法可以是,合法状态下,要找最长的,而i向右是缩短的情况,这时应该对应的是不满足条件的情况,直到i向右移动到合法情况,此时满足条件且长度较大。
class Solution {
public:
int characterReplacement(string s, int k) {
vector<int> letter_map(128,0);
int maxlen=0;
for(int i=0,j=0;j<s.size();j++)
{
letter_map[s[j]]++;
//find max in letter_map;
int maxn=maxnum(letter_map);
while(j-i+1-maxn>k)
{
//update max
letter_map[s[i]]--;
i++;
maxn=maxnum(letter_map);
}
maxlen=max(maxlen,j-i+1);
// / cout<<i<<":"<<j<<endl;
}
return maxlen;
}
int maxnum(vector<int> t)
{
int res=t[0];
for(int i=1;i<t.size();i++)
res=max(res,t[i]);
return res;
}
};
这道题里,时间主要浪费在了更新max上,但实际上,由于每次都只增加一个新字符,因而新的max也只受该字符影响,假如该字符与之前最大值的字符不一样,则直接更新即可,假如一样的话,新的max同样是旧的max和新加入的对应字符数量求最大。总之,只有最大的max发生变化时,我们才需要考虑。
class Solution {
public:
int characterReplacement(string s, int k) {
vector<int> letter_map(128,0);
// int maxlen=0;
int maxn=0;int i=0,j=0;
for(;j<s.size();j++)
{
letter_map[s[j]]++;
//find max in letter_map;
maxn=max(maxn,letter_map[s[j]]);
if(j-i+1-maxn>k)
{
//update max
letter_map[s[i]]--;
i++;
}
// maxlen=max(maxlen,j-i+1);
}
return j-i;
}
};
不过值得注意的是,这里的while可以用if来实现,原因应该是上一个i和j停留的位置是合法的位置,对应长度已经是合法长度,而不满足条件情况下,i和j同时+1对结果不影响,因而不需要用while继续运行,否则所找的子区间长度更短,而比其更长的已经找到了。且这个过程中j和i的区间是不减的(无论是if还是while,因为j-i+1-maxn每次最多增加1),所以结果直接用j-i表示也可以。
五、找到字符串中所有字母异位词
这道题类似于之前的找覆盖子串的问题,但是相当于加了一个条件,长度等于目标串。我们可以考虑while循环条件是每个字符数量都大于目标串里对应字符的数量,这样退出循环时区间每个字符数量都小于等于目标串里对应数量,如果区间长度等于目标串,那么是合法情况。
另一种考虑方式是distance,当distance等于目标串长度时,需要减少对应长度,一直到不合法状态,这个过程中长度与目标串相同时,就是目标结果。
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
vector<int> pnum(128,0),snum(128,0);
for(int i=0;i<p.size();i++) pnum[p[i]]++;
int distance=0;
int i=0,j=0;
vector<int> res;
for(;j<s.size();j++)
{
if(snum[s[j]]<pnum[s[j]]) distance++;
snum[s[j]]++;
while(distance>=p.size())
{
if(j-i+1==p.size()) res.push_back(i);
if(snum[s[i]]<=pnum[s[i]]) distance--;
snum[s[i]]--;
i++;
}
}
return res;
}
};
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
vector<int> pnum(128,0),snum(128,0);
for(int i=0;i<p.size();i++) pnum[p[i]]++;
int distance=0;
int i=0,j=0;
vector<int> res;
for(;j<s.size();j++)
{
if(snum[s[j]]<pnum[s[j]]) distance++;
snum[s[j]]++;
while(j-i+1>p.size())
{
if(snum[s[i]]<=pnum[s[i]]) distance--;
snum[s[i]]--;
i++;
}
if(distance==p.size()) res.push_back(i);
}
return res;
}
};
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
int ht1[26] = {0}, ht2[26] = {0};
for (auto ch : p) {
ht2[ch - 'a']++;
}
vector<int> res;
for (int slow = 0, fast = 0; fast < s.size(); fast++) {
ht1[s[fast] - 'a']++;
while (ht1[s[fast] - 'a'] > ht2[s[fast] - 'a']) {//每个结果串里出现的数量最后都要小于等于目标串里出现的,因为目标串里没出现的最后的区间里也不会出现,从而保证结果正确
ht1[s[slow] - 'a']--;
slow++;
}
if (fast - slow + 1 == p.size())
res.push_back(slow);
}
return res;
}
};
六、 字符串的排列
这道题和上一道题思路相同,找目标串的排列的区间,即结果区间里每个字符数量都小于等于目标串里的数量,且区间长度和目标串相同。
当然用distance同样可以判断。
class Solution {
public:
bool checkInclusion(string s1, string s2) {
int i=0,j=0;
vector<int> s1map(128,0),s2map(128,0);
for(int i=0;i<s1.size();i++) s1map[s1[i]]++;
for(;j<s2.size();j++)
{
s2map[s2[j]]++;
while(s2map[s2[j]]>s1map[s2[j]])
{
s2map[s2[i]]--;
i++;
}
if(j-i+1==s1.size()) return true;
}
return false;
}
};
七、子数组最大平均数 I
固定长度的滑动窗口,一次循环即可,关键是用上一次的sum和本区间sum的关系简化计算。
//模板双指针
class Solution {
public:
double findMaxAverage(vector<int>& nums, int k) {
if(nums.size()==k)
{
double sum=0;
for(int i=0;i<k;i++) sum+=nums[i];
return sum/k;
}
int res=INT_MIN;
int sum=0;
int i=0,j=0;
for(;j<nums.size();j++)
{
sum+=nums[j];
while(j-i+1>k)
{
sum-=nums[i];
i++;
}
if(j-i+1==k) res=max(res,sum);
}
return res*1.0/k;
}
};
class Solution {
public://标准解法,速度较快
double findMaxAverage(vector<int>& nums, int k) {
int sum = 0;
int n = nums.size();
for (int i = 0; i < k; i++) {
sum += nums[i];
}
int maxSum = sum;
for (int i = k; i < n; i++) {
sum = sum - nums[i - k] + nums[i];
maxSum = max(maxSum, sum);
}
return static_cast<double>(maxSum) / k;
}
};
看到评论区也可以用前缀和预处理,计算方便一些。
七、 K 个不同整数的子数组
这道题比较精巧的思路在于,每个右端点都对应很多左端点区间,使得每个区间都满足k个不同整数子数组。思想是维护两个区间,一个是最靠左的最多k个不同整数子数组,另一个是最靠左的最多k-1个不同整数子数组,区间内部的就是满足k个不同整数的子数组数量。
class Solution {
public:
int subarraysWithKDistinct(vector<int>& nums, int k) {
int left1=0,left2=0,j=0;
int res=0;
vector<int> stat1(nums.size()+1,0),stat2(nums.size()+1,0);
int diffn1=0,diffn2=0;//这里注意每次对一个右边界,同时计算维护其最多k个不同整数的区间和最多k-1个不同整数的区间
while(j<nums.size())
{
stat1[nums[j]]++;
if(stat1[nums[j]]==1) diffn1++;
stat2[nums[j]]++;
if(stat2[nums[j]]==1) diffn2++;
while(diffn1>k)
{
stat1[nums[left1]]--;
if(!stat1[nums[left1]]) diffn1--;
left1++;
}
while(diffn2>k-1)
{
stat2[nums[left2]]--;
if(!stat2[nums[left2]]) diffn2--;
left2++;
}
res+=left2-left1;
j++;
}
return res;
}
};