一、长度最小的子数组
思路:
- 设置左右窗口 l,r,连续子数组和为sum,返回的最小子数组长度为len;
- 遍历数组,每次将 nums[r]元素 加入sum,如sum<target,则一直往右遍历。
- 若sum>=target,则说明找到了>=target的子数组,那么尝试缩小窗口来找到满足条件的最小子数组,之后更新len。
- 判断len值进行返回
int minSubArrayLen(int target, int* nums, int numsSize) {
//设置窗口指针
int left=0; //左侧窗口
int right=-1; //右侧窗口
int s=0; //存储子数组的总和,用于判断target
int len=INT32_MAX;
for(int i=0;i<numsSize;i++)
{
s+=nums[i];
right++; //每次遍历新元素加入s
while(s>=target) //满足条件
{
if(s-nums[left]>=target) //尝试缩小窗口
{
s-=nums[left];
left++;
}
else
{
break; //已经不能在缩小
}
}
if(s>=target) //如果满足子数组条件判断len是否需要更新
len=len>(right-left+1)?right-left+1:len;
}
return len==INT32_MAX?0:len;
}
二、无重复字符的最长子串
思路:
-
对于S的任意一个字符,都可以转化为0-255的整数,因此建立字符表,用于查找字符的上一次出现位置用于查找字符的上一次出现位置,如last[97]记录了字符’a’的上一次出现位置。
-
遍历S,如果S[r]在窗口中不存在(窗口记录了无重复的字符串),则加入窗口中,更新字符表。
-
如果S[r]在窗口中存在并且上一次出现位置在l-r内,则将l移到其下一位并更新字符表。
(abc)a——>a(bca); 上一次位置在l-r中,l缩小至下一位。
-
如果S[r]在字符表中存在,但其上次出现在(l-r)外,则维持不变。
ab(cbe)a ->ab(cbea);上一次位置在l-r外,l不变。
-
长度维护与上题同理
int max(int x,int y)
{
return x>y?x:y;
}
int lengthOfLongestSubstring(char* str) {
int n = strlen(str); //计算字符串长度
int last[256]; //定义字符数组,每一个字符都可以转化为在0~255内的整型变量。如;'a'的ascill码
//为97,故该数组的last[97]记录了字符'a'在str字符串中上一次出现的位置。
int ans = 0; //返回答案
for (int i = 0; i < 256; i++) {
last[i] = -1; //进行初始化为-1,表示此时各字符均未出现
}
for (int l = 0, r = 0; r < n; r++)
{
l=max(l,last[str[r]]+1); //如果上一次出现字符位置在l-r外(在l左边),则l不变。
//如果在l-r内有是s[r]则,窗口更新,进行收缩,l位置来的该字符上一次出现的下一位,保证无重复字符。(abc)a->a(bca)
ans=max(ans,r-l+1); //比较此时是否比上一次更长
last[str[r]]=r; //更新字符上一次出现位置
}
return ans;
}
三.最小覆盖子串
思路:(想像欠债还钱过程)
- 首先,对建立字符表debts,然后用 t 字符串对字符表初始化,该字符表记录 t 的欠债数,t 中的字符每出现一次,欠债数就增加1,字符数组对于值减1,同时记录总欠债数 debt。定义start和len分别记录目标字符串的起始位置与长度。同样,定义左右窗口。
- 遍历字符串S,如果 debts[S[r]]<0,则债务还一份,字符数组相应位置值加一,总债务减少1。
- 若遇到了非欠债字符串,则其字符数组对于值加一,总债务不变。
- 若总债务=0,说明已经还清债务,即已经找到了符合条件的字符串。
- l 尝试向前移动,如果debts[S[l]]值大于0,说明该元素无关紧要,可以缩减。如果debts[S[l]]=0,说明此时已经不能移动,否则会欠款。
- 该过程更新len与start,最终返回目标字符串即可。
char* minWindow(char* s, char* t) {
if(strlen(s)<strlen(t))
return ""; //s长度比t小,不满足题意
int debts[256]={0}; //字符表,记录t数组的各元素出现次数,一开始全为0
int debt=strlen(t); //总次数
int start=0; //记录覆盖子串起始位置
int len=INT_MAX; //记录覆盖子串长度
for(int i=0;i<debt;i++)
{
debts[t[i]]--; //初始化字符表数组,每出现一次,对应字符位值-1;
}
for(int left=0,right=0;right<strlen(s);right++)
{
if(debts[s[right]]++<0) //
debt--;
//如果s[r]的元素在字符表中值为负,说明t字符串中有该元素,之后总次数减1
if(debt==0)
{
while(debts[s[left]]>0) //尝试缩小窗口
{
debts[s[left]]--;
left++;
}
if(right-left+1<len) //判断是否是更小的子串
{
len=right-left+1;
start=left;
}
}
}
if(debt!=0)
return "";
char* str=malloc((len+1)*sizeof(char)); //构造子串
for(int i=0;i<len;i++)
{
str[i]=s[start+i];
}
str[len]='\0';
return str;
}
四. 加油站
思路:
与前面不同,该题从子数组开头开始讨论。
- 从头开始遍历每个加油站 l(窗口左边界),每次窗口加入新元素,计算其累加和是否>=0,如果符合,说明可以到达该位置,窗口继续扩大,如果再次回到起点,则符合题意返回起点。
- 否则不可达,l 到下一站点,窗口大小减少,累加和更新(少了一个元素),直到sum>=0。
- 遍历完成说明均不可达,返回-1
int canCompleteCircuit(int* gas, int gasSize, int* cost, int costSize) {
//r:即将进来的数
//len:窗口大小
//sum窗口累加和
int n=gasSize;
for(int l=0,r=0,sum=0,len=0;l<n;l++)
{
while(sum>=0)
{
if(len==n) //即能够到达起点
return l;
r=(l+(len++))%n; //实现一圈
sum+=gas[r]-cost[r]; //窗口增大,累加和更新
}
len--; //当sum<0时,该站点不符合题意,到下一站点,窗口减小
sum-=gas[l]-cost[l]; //窗口减小,累加和减去去掉的余量
}
return -1; //遍历完说明均不符合题意
}
总结
滑动窗口:
维持左右边界都不退回的一段范围,可以求解子数组/串的问题。
关键:
找到窗口扩大/缩小情况下是否存在某种单调关系。 如:缩小时和减少,扩大时和增大。缩小时次数增大,扩大时次数减小等。