前言
一些相向双指针训练题目的题解
题单(力扣链接,题单来源于b站up灵茶山艾府):
209.长度最小的子数组
713.乘积小于k的子数组
3.无重复字符的最小子串
1004.最大连续1的个数|||
1234.替换子串得到平衡字符串
1658.将x减到0的最小操作数
相向双指针的个人感悟
相向双指针,有些人也叫滑动窗口,是一种经典的解题方法。叫相向双指针是因为它通过两个移动方向相同的指针扫一遍数组得到有效解空间,叫滑动窗口是因为两个指针扫过数组看起来像一个滑动的窗口遍历了数组,而我们要做的就是维护这个窗口以获得解空间。那么一般来说对于相向双指针可以解决的问题,也可以通过i和j两个变量开一个双层循环去解决问题,双层循环的时间复杂度是O(n²)的,相向双指针跟这种常规方法比起来时间复杂度降为了O(n),因为这种方法巧妙地利用了其中一个指针不会回退的性质,通过枚举一个指针扫过数组,又由于另一个指针不会回退,也只扫了一遍数组,所以时间复杂度是O(n)的。那么我们用相向双指针解题,主要就是去探求体会那种指针不会回退的性质,思考指针移动的条件。
209.长度最小的子数组
题面:给定一个含有 n 个正整数的数组和一个正整数 target 。找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
这算是双指针一道经典的模板题了,常规做法是用变量j枚举每一个点,然后变量i从j开始往前移动,一直到符合sum>=target或者i<0为止,如果sum>=target了,那么就获得了以变量j对应点为右端点的最小连续子数组,此时j-i+1就是这个子数组所对应的解,如果sum<target,那么说明以变量j所对应这个点为右端点没有解,这样扫一遍下来,不断将ans更新为最小,扫完过后判断一下ans,按要求返回0或ans即可。那么分析这种常规解法我们发现,对一段符合要求的i~j的区间,这个区间的和加起来已经>=target了,如果右指针j再向右移动一步,由于数组都是正数,所以此时的和sum必然大于target,此时对于左指针i,没必要再退回去从头枚举(这是造成O(n²)的主要原因),而是只需要判断是否可以前进(判断条件为前进后子数组是否还满足要求),这样j和i扫过数组后,由于j和i都只扫了一遍,所以复杂度是O(n)的,具体代码如下
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int left = 0;
int right = 0;
int ans = 0;
int res = 1000000;
int biao = 0;
for(right = 0;right < nums.size();right++) {
ans += nums[right];
while(ans - nums[left] >= target) {
ans -= nums[left];
left++;
}
if(ans >= target)
res = min(res,right-left+1);
}
return res == 1000000? 0:res;
}
};
713.乘积小于k的子数组
题面:给你一个整数数组 nums 和一个整数 k ,请你返回子数组内所有元素的乘积严格小于 k 的连续子数组的数目。
这道题的常规做法我们可以用变量j去枚举数组每一个端点,然后用变量i枚举j之前的点来求出以变量j所对应点为右端点的满足条件的最大数组[l,r],容易知道像[l+1.r],[l+2,r],…,[r,r]这些数组一定也是符合条件的,所以我们可以根据这个最大数组求出该右端点对应的所有符合条件的数组数量就是r-l+1,(右端点是固定的),根据这一点,我们只需要求出以每个点为右端点有多少个符合条件的数组再加起来就是答案的解。然后我们思考双指针法的解法,对于r对应的最大数组[l,r],l不能往左移动,因为此时数组已经是“最大”了,所以左指针l是不会回退的,那么思考左指针向右移动的条件,当右指针r向右移动一个后,乘积肯定会增大但不一定大于k,当大于等于k时左指针就要开始向右移动到区间符合规则了,当小于k时左指针不会移动,此时该区间就是以r所对应点为右端点的最大区间。想明白了这些问题后就可以写代码了:
class Solution {
public:
int numSubarrayProductLessThanK(vector<int>& nums, int k) {
if(k == 0 || k == 1)
return 0;
else {
int res = 0;
int left = 0;
int right = 0;
int mul = 1;
int biao = 0;
for(right = 0;right < nums.size();right++) {
mul *= nums[right];
while(mul >= k) {
mul /= nums[left];
left++;
}
if(mul < k)
res += right - left + 1;
}
return res;
}
}
};
3.无重复字符的最小字串
题面:给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
子串的意思就是连续子数组,这道题可以用哈希数组的办法来存储区间中已经出现过的字符,然后右指针一个一个向右遍历,遍历到一个点就把该点所对应的字符存储起来,然后检测加入这个字符后会不会导致区间内出现重复字符,如果不会,那左指针自然不移动,无事发生,直接得到以该点为右端点对应的一个答案解,如果导致区间内出现了重复字符,那么左指针就要移动了,怎么移动呢?容易想到,区间内重复的那个字符是右端点对应的字符,且在区间内该端点左边只有一个字符,那我们让左指针一直移动移动到把那个字符踢出去就好,然后得到以该点为右端点对应的一个答案解,题目求最长字串,遍历过程中不断更新答案,然后返回答案即可。代码如下:
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int left = 0;
int right = 0;
int a[2560] = {0};
int res = 0;
for(right = 0;right < s.size();right++) {
a[s[right]]++;
while(a[s[right]] != 1 && right < s.size()) {
a[s[left]]--;
left++;
}
res = max(res,right-left+1);
}
return res;
}
};
1004.最大连续1的个数|||
题面:给定一个二进制数组 nums 和一个整数 k,如果可以翻转最多 k 个 0 ,则返回 数组中连续 1 的最大个数 。
这个题目的意思是,你有k次机会可以把0变成1,再翻译一下,就是求连续1的最大区间(该区间内可以包含k个0),那么本质上还是滑动窗口,那么我们还是用右指针去遍历每一个点,如果右指针对应点的值是1,那自然无事发生,直接求出以该端点为右端点的最大区间就好;如果对应的点是0,那么你得判断加入该点后区间内有几个0,如果加入后还是小于等于k个,那也无事发生,左指针不移动;如果加入后大于k个0了,那么就得移动左指针了,怎么移动呢?一直移到移除一个0到区间外就行了。代码如下:
class Solution {
public:
int longestOnes(vector<int>& nums, int k) {
int num0 = 0;
int i = 0;
int j = 0;
int ans = 0;
for(j = 0;j < nums.size();j++) {
if(nums[j]) ;
else {
num0++;
if(num0 > k) {
while(nums[i]) i++;
i++;
}
else ;
}
ans = max(ans,j-i+1);
}
return ans;
}
};
1234.替换字串得到平衡字符串
题面:有一个只含有 ‘Q’, ‘W’, ‘E’, ‘R’ 四种字符,且长度为 n 的字符串。假如在该字符串中,这四个字符都恰好出现 n/4 次,那么它就是一个「平衡字符串」。给你一个这样的字符串 s,请通过「替换一个子串」的方式,使原字符串 s 变成一个「平衡字符串」。你可以用和「待替换子串」长度相同的 任何 其他字符串来完成替换。请返回待替换子串的最小可能长度。如果原字符串自身就是一个平衡字符串,则返回 0。
这个题不是那么好想,做法应该也挺多,我介绍一下我的做法和我怎么思考的。根据题目需要我们可以开四个哈希数组还分别统计滑动区间外(注意是区间外的,初始区间为0,所以初始的滑动区间外就是整个串)每个字符出现的次数,然后我们思考滑动区间怎样移动,我们让右指针遍历每一个端点,当右指针移动一个后,右指针所对应点的字符数量要减少,然后我们思考左指针的移动。题目要求是最终让每个字符都恰好出现n/4次,那么如果左指针所对应的字符数量加上一后仍然不大于n/4,那么我们可以把这个字符扔到区间外面去(就是左指针向右移动),因为把它拿进区间后也是修改成它不如不修改减少区间长度;如果加上一后大于n/4了,那显然是移动不了的。在确定下左右端点后我们来思考这个区间,显然这个区间是不一定符合要求的,所以我们要判断一下此时这个区间是否满足要求,怎么判断呢?区间里的字符就是“万金油”,缺哪补哪,此时只要区间里的字符数量大于等于区间外缺的字符,那么这个区间就是符合要求的,可以更新答案,否则就不能更新答案。代码如下:
class Solution {
public:
int balancedString(string s) {
int a[256] = {0};
for(int i = 0;i < s.size();i++) {
a[s[i]]++;
}
const char q = 'Q';
const char w = 'W';
const char e = 'E';
const char r = 'R';
int can = s.size() / 4;
int i = 0,j = 0;
int dan = 0;
int ans = 1000000;
for(j = 0;j < s.size();j++) {
a[s[j]]--;
while((a[s[i]]+1) <= can && i <= j ) {a[s[i]]++; i++;}
if(j-i+1 >= (abs(a[q]-can)+abs(a[w]-can)+abs(a[e]-can)+abs(a[r]-can)))
ans = min(ans,j-i+1);
}
return ans;
}
};
1658.将x减到0的最小操作数
题面: 给你一个整数数组 nums 和一个整数 x 。每一次操作时,你应当移除数组 nums 最左边或最右边的元素,然后从 x 中减去该元素的值。请注意,需要 修改 数组以供接下来的操作使用。如果可以将 x 恰好 减到 0 ,返回 最小操作数 ;否则,返回 -1 。
这个题我同样简单介绍一下我的做法。首先想到,操作数只能从左边和右边去,且是依次取,意思就是要取左边第二个数那么一定取到了左边第一个数。因为两边取,所以让i指针指向左边,j指针指向右边,根据之前做题经验,通常是遍历一个指针,然后另一个不会回退的指针移动。那么我们用i遍历每一个数,用j指针去移动,那么再一想,j移动方向应该是从左向右移(向着减少区间长度的方向),但j已经在最右边了啊?所以我们要初始化让j向左移动到合适的位置,那么什么位置合适呢?j及其右边的数加起来大于等于x的时候合适,当j移动到指定位置后,如果此时和can等于x,那么这是一个解,如果j移动到0,也就是数组所有值加起来也小于x,那么显然是无解的,返回-1。那么初始化操作做好了之后我们来从左向右一点点遍历i,遍历到一个i后加上该点对应值,然后向右移动左指针直到区间和小于等于x为止,判断是否等于x,若是,得到一个解,若不是,端点i无解。这样不断更新答案,最后对答案判断一下返回解即可。(有可能在左端点右移动一个后can仍然小于x,此时为什么j不会向左回退呢?如果你有这个疑惑的话可以思考一下j是怎么移过来的),题解代码如下:
class Solution {
public:
int minOperations(vector<int>& nums, int x) {
int n = nums.size();
int i = 0;
int j = n;
int ans = 1000000;
int can = 0;
while(can < x && j > 0) {
--j;
can += nums[j];
}
if(can == x) ans = n - j;
else if(can < x) return -1;
for(i = 0; i < n;i++) {
can += nums[i];
while(can > x && j < n) {
can -= nums[j];
j++;
}
if(can == x) ans = min(ans,i+n+1-j);
}
return ans == 1000000?-1:ans;
}
};
以上就是关于这六道题的题解(或者感悟吧),如有不对还请批评指正!