//本文章为本人自己对该部分内容学习的理解,如有错误欢迎指正:>
双指针和滑动窗口都是算法的入门基础,两者有着异曲同工之处。
双指针的引入和介绍:
在对数组的特定子数组进行查找的题目中往往需要遍历整个数组,常常还不止多遍,面对数组数量过大的情况时间复杂度会大到惊人,利用两个指针不断调整区间从而求出最优解的方法就叫双指针,也称尺取法。
给个例题:2563. 统计公平数对的数目
给你一个下标从 0 开始、长度为 n
的整数数组 nums
,和两个整数 lower
和 upper
,返回 公平数对的数目 。
如果 (i, j)
数对满足以下情况,则认为它是一个 公平数对 :
0 <= i < j < n
,且lower <= nums[i] + nums[j] <= upper
示例 1:
输入:nums = [0,1,7,4,4,5], lower = 3, upper = 6 输出:6 解释:共计 6 个公平数对:(0,3)、(0,4)、(0,5)、(1,3)、(1,4) 和 (1,5) 。
示例 2:
输入:nums = [1,7,9,2,5], lower = 11, upper = 11 输出:1 解释:只有单个公平数对:(2,3) 。
面对这样的题目,双指针就能大展身手了。由于只需返回个数而不是位置,可以先进行排序,通过定下指针l和r的位置,我们只需要考虑这个区间内小于lower和小于upper的个数了,而公平数对就是这两个个数的差。
代码如下:
int cmp(const void *a, const void *b){
int va = *(int*)a;
int vb = *(int*)b;
return va < vb ? -1 : 1;
}
long long countlessThan(int* nums, int n, int val){
int l = 0, r = n - 1;
long long ans = 0;
while(l < r){
int sum = nums[l] + nums[r];
if(sum <= val){
ans += (r - l);
++l;
}else{
r--;
}
}
return ans;
}
long long countFairPairs(int* nums, int n, int lower, int upper){
int ans = 0, len = 0;
qsort(nums, n, sizeof(int), cmp);
long long a = countlessThan(nums, n, upper);
long long b = countlessThan(nums, n, lower - 1);
return a - b;
}
滑动窗口的引入和介绍:
在平时的解题中会遇见如求最长无重复元素的子数组这类问题,首先想到的就是遍历数组。给一串笨笨的代码。
int findmaxlen(char* s, int sSize){
int len = 0;
for(int i = 0; i < sSize; i++){
for(int j = i+1; j < numsSize; j++){
if(s[i]!=s[j]&&len < j - i + 1){
len = j - i + 1;
}else break;
}
}
}
不难发现这串代码的时间复杂度太大了如果面对numsSize过大的情况下很有可能超时。
不如换种想法:将一个左指针l放在0,一个右指针r放在-1,这个时候数组的长度为空,用哈希表hash[256]将子数组内各元素出现次数置零,再向右移动右指针。移动过程中,如果hash[s[r]]为0的话就自增;如果大于1的话就说明,在l~r这个区间内有了两个重复的元素,先将hash[s[l]]自减,再自增l直到hash[s[r]]小于等于1为止。之后再判断最长的长度。
该想法就是根据不断调整指针减少重复操作次数。
转换成代码:
int lengthOfLongestSubstring(char* s) {
int ans = 0, i = 0, j = -1, n = strlen(s);
int h[256];
memset(h, 0, sizeof(h));
while(j < n - 1){
++j;
++h[s[j]];
while(h[s[j]] > 1){
--h[s[i]];
++i;
}
if(j - i + 1 > ans)
ans = j - i + 1;
}
return ans;
}
现在给个新题目:1493. 删掉一个元素以后全为 1 的最长子数组
给你一个二进制数组 nums
,你需要从中删掉一个元素。
请你在删掉元素的结果数组中,返回最长的且只包含 1 的非空子数组的长度。
如果不存在这样的子数组,请返回 0 。
提示 1:
输入:nums = [1,1,0,1] 输出:3 解释:删掉位置 2 的数后,[1,1,1] 包含 3 个 1 。
示例 2:
输入:nums = [0,1,1,1,0,1,1,0,1] 输出:5 解释:删掉位置 4 的数字后,[0,1,1,1,1,1,0,1] 的最长全 1 子数组为 [1,1,1,1,1] 。
示例 3:
输入:nums = [1,1,1] 输出:2 解释:你必须要删除一个元素。
题目要求在一串连续的元素中只能有一个0且1的数量最多,根据滑动窗口的套路,将l,r初始化后再慢慢通过这个条件来更新窗口。
代码如下:
int longestSubarray(int* nums, int numsSize) {
int l = 0, r = -1, len = 1,zerocnt = 0;
while(r < numsSize - 1){
r++;
zerocnt += !nums[r];
while(zerocnt > 1){
zerocnt -= !nums[l];
l++;
}
if(len < r - l + 1){
len = r - l + 1;
}
}
return len - 1;
}
在双指针和滑动窗口的套路下,代码是不是显得简洁明了呢?还同时大大降低了时间复杂度。
两者定义与特点:
1.滑动窗口依靠左右指针的移动来标记窗口的边界,指针的移动就是窗口的滑动,而窗口的大小就可以根据指针移动来调整。滑动窗口常用于解决连续子串或连续数组类型的问题,即窗口内的元素是连续的,例如求无重复字符最长字符串,最大子数组等等。
2.双指针则是指在遍历数组或字符串中,使用两个及以上的指针来进行遍历,这两个指针常指向不同的元素,根据需要进行调整。双指针多用来降低算法的时间复杂度,在一次循环内进行多次操作,避免多重循环。关键在于指针的起始位置以及移动速度和方向。可用于解决数组中的两数之和、三数之和、回文子串等问题。
区别:
1.指针指向和移动:
·滑动窗口的左右指针移动方向可能相同可能相反用于改变窗口大小(如左指针向左移动,右指针向右移动,扩大窗口大小),具有窗口的概念。
·双指针的指向和移动较为自由随机,可能指向数组中的任意位置,根据问题要求进行调整。
2.窗口概念:
·滑动窗口中窗口根据指针调整,大小是动态变化的。
·双指针中没有明确的“窗口”概念,因为指针的位置是任意的,不一定构成连续的窗口。
3.应用场景:
·滑动窗口更适合解决连续子串或子数组的问题。
·双指针倾向于数组中元素的查找,排序,匹配问题。
4.复杂度:
二者都能有效的降低算法的时间复杂度,主要根据自身的移动策略。