本篇就是第一个小专题的更多题目,我会将前一篇提到的letcode中的题目在这里解答,附上思路及代码,上一篇关于滑动窗口的讲解链接在此—>2021新年算法小专题—1.滑动窗口(Java代码)。
1.和为s的连续正数序列(Easy)
题目
输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。
序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。
示例 :
输入:target = 15
输出:[[1,2,3,4,5],[4,5,6],[7,8]]
分析
显然是在一个正整数序列中维护一个窗口,从1
开始滑动,窗口中的数只有在相加和为target
的时候才满足题意。需要注意这个序列的终点并不是无穷的,因为题中要求结果至少应该有两个数,而两数相加等于target
则要求两个数一个大于target/2
,一个小于target/2
,所以说我们最多只需要把左边界滑动到target/2
的位置就好了。因此确定:while循环的退出条件是i>target/2
。
因为我们是要统计数字的和,所以这里就不关注滑动窗口的大小size
了,而是关注窗口中数字的和sum
。在滑动中,我们需要根据sum来进行判断:当sum小于target时,说明窗口内的数字和还不够,因此扩大窗口,右边界指针j加一 ;当sum大于target时,说明窗口内的数字和已经超过要求的值了,因此尝试收缩窗口,左边界指针i加一 ;当sum和target相等时,说明当前窗口内的子串就是我们需要的,使用合适的数据结构(数组)将他们记录下来,记录完成后我们还要继续进行循环,因此要把窗口左指针i加一,使窗口中的数字和小于要求的值,以再下次循环中继续扩大窗口。
参考代码
class Solution {
public int[][] findContinuousSequence(int target) {
int i=1,j=1;// 滑动窗口的左右边界,为方便,直接把数值为1的起始位置下标视为1,下标=值
int sum=0;// 当前窗口中的数字之和
List<int[]> ans=new ArrayList<>();
while(i<=target/2){// 窗口滑动的条件
if(sum<target){// 窗口中数字的和小于需求,扩大窗口(右边界右移)
sum+=j;
j++;
}
else if(sum>target){// 窗口中数字的和大于需求,收缩窗口(左边界右移)
sum-=i;
i++;
}
else{// 窗口中数字的和等于需求了,记录下窗口中的各个值
int []curAns=new int[j-i];// 记录本次窗口的各个值
for(int k=i;k<j;k++) curAns[k-i]=k;// 循环填入
ans.add(curAns);// 加入结果集合
sum-=i;// 本次记录完成,窗口收缩继续判断
i++;
}
}
return ans.toArray(new int[ans.size()][]);
}
}
2.滑动窗口最大值(Hard)
题目
给你一个整数数组 nums
,有一个大小为 k
的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k
个数字。滑动窗口每次只向右移动一位。
返回滑动窗口中的最大值。
示例 :
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
分析
本题就是一种训练滑动窗口思想的题目。题目本身的思路比较简单,我们只需要在sum
数组中维护一个大小固定为k
的窗口,每次移动都让右边界扩大一个数字,左边界收缩个数字即可。每个窗口位置都要记录当前窗口中的最大值。那么如何快速找到不同状态下的窗口中最大值呢?可以使用队列这种数据结构。我们维护一个全局的单调队列queue
,保证最大值一直在队首,这样每次取出当前窗口最大值的时间复杂度就是O(1)。
现在我们考虑如何在移动窗口的同时更新这个队列queue
。首先这个过程应该分为窗口形成之前和窗口形成以后:因为我们从第一个元素开始走,需要走k
个元素后才能形成大小为k
的窗口;窗口形成后就可以每次移出一个旧的左边界、移入一个新的右边界了。
在窗口形成之前,即填充k
个元素到窗口的过程中,每次填进一个元素,如果队列中存在元素,就使用队尾元素与新元素比较:如果新元素比队尾大,就删除这个队尾元素,直到队列为空或者新元素小于等于队尾元素为止,将新元素入队。
在窗口形成后,即窗口滑动的过程中,每次滑动一个位置,就会涉及到两个发生变化的位置:左边界和右边界(在本题中,我们根据题意把窗口设为双闭的,不再是左闭右开了),左边界移出,右边界移入,需要进行两个判断:一个是移出的元素是否在队列queue
的队头位置?如果在,要将它移除(因为当前窗口已经不再包含该元素);另一个是移入的元素是否应该入队?进行如上一步(窗口形成前)的判断即可。
每次窗口滑动后我们都把队列queue
的队首元素拿出来记录,这就是本次窗口滑动后的最值。
以示例数据为例,整个过程图解如下:
初始时,窗口大小为0,队列为空,开始形成窗口。
![image-20210212225427354](https://wang612.oss-cn-beijing.aliyuncs.com/img/image-20210212225427354.png)
将1移入窗口,判断队列为空,将1入队。此时窗口大小为1,不满足窗口大小3,继续扩大。
![image-20210212225635847](https://wang612.oss-cn-beijing.aliyuncs.com/img/image-20210212225635847.png)
将3移入窗口,判断队列的队尾元素1比新元素3小,因此移除1,然后队列为空,把3入队。此时窗口大小为2,继续扩大。
![image-20210212225811479](https://wang612.oss-cn-beijing.aliyuncs.com/img/image-20210212225811479.png)
将-1移入窗口,判断队列队尾元素3比-1大,因此直接把-1入队。此时窗口大小为3,满足题目要求,本次的最值是队列的队首3,记录下来。然后开始滑动。
![image-20210212230009999](https://wang612.oss-cn-beijing.aliyuncs.com/img/image-20210212230009999.png)
第一次滑动,1移出,-3移入。1不在队首,-3比队尾-1小,把-3入队。本次滑动结果如下:
![image-20210212234550321](https://wang612.oss-cn-beijing.aliyuncs.com/img/image-20210212234550321.png)
第二次滑动,3移出,5移入。3在队首,因此移出队首3;5比队尾-3大,-3出队,然后队尾-1、3也都小于5,都出队,最后队列为空,5入队。本次滑动结果如下:
![image-20210212234838236](https://wang612.oss-cn-beijing.aliyuncs.com/img/image-20210212234838236.png)
第三次滑动,-1移出,3移入。-1不在队首,3比队尾小,因此直接入队。本次滑动结果如下:
![image-20210212235046910](https://wang612.oss-cn-beijing.aliyuncs.com/img/image-20210212235046910.png)
第四次滑动,-3移出,6移入。-3不在队首;6大于队尾3,3出队,也大于队尾5,5出队,队列为空,6入队。本次滑动结果如下:
![image-20210212235246781](https://wang612.oss-cn-beijing.aliyuncs.com/img/image-20210212235246781.png)
第五次滑动,5移出,7移入。5不在队首,7比队尾6大,6出队,7入队。本次滑动结果如下:
![image-20210212235429874](https://wang612.oss-cn-beijing.aliyuncs.com/img/image-20210212235429874.png)
最终答案是:[3,3,5,5,6,7]。(第一次形成窗口时的最值没有画在max数组中)
参考代码
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
// 数组为空或者窗口大小为0 返回空数组
if(nums.length == 0 || k == 0) return new int[0];
Deque<Integer> deque = new LinkedList<>();// 单调队列 用于O(1)时间找到最值
int[] res = new int[nums.length - k + 1];// 最大值结果(max)
// 还未形成窗口时
for(int i = 0; i < k; i++) {
// 如果队尾元素比新元素小,就将队尾出队
while(!deque.isEmpty() && deque.peekLast() < nums[i])
deque.removeLast();
deque.addLast(nums[i]);// 最后要把新元素入队
}
res[0] = deque.peekFirst();// 第一个滑动窗口的最大值
// 形成窗口后
for(int i = k; i < nums.length; i++) {
if(deque.peekFirst() == nums[i - k])// 如果当前队首是要移出的值,就将它出队
deque.removeFirst();
// 如果队尾元素比新元素小,就将队尾出队(同未形成窗口时的操作)
while(!deque.isEmpty() && deque.peekLast() < nums[i])
deque.removeLast();
deque.addLast(nums[i]);// 最后要把新元素入队
res[i - k + 1] = deque.peekFirst();//记录本次滑动窗口的最值
}
return res;
}
}
3.字符串的排列(Medium)
题目
给定两个字符串 s1 和 s2,写一个函数来判断 s2 是否包含 s1 的排列。
换句话说,第一个字符串的排列之一是第二个字符串的子串。
示例 :
输入: s1 = "ab" s2 = "eidbaooo"
输出: True
解释: s2 包含 s1 的排列之一 ("ba").
分析
本题其实跟上一篇讲解滑动窗口文章中的例题(最小覆盖子串)很相似,为了更好理解本题建议回去看一下,传送门在这。
在最小覆盖子串中,我们用到了两个集合need
、window
分别记录子串中所需要的字母及个数、当前窗口中包含的所需字母及个数。本题中与最小覆盖子串的区别就在于——找到这个子串就停下,返回true
。至于题中的子串“排列”,其实和覆盖字串同理,只需要要求字母及出现次数都相同,就能达到子串的排列效果。只不过在覆盖子串中,窗口大小可以比子串大,即允许中间存在其他字母,如"abcd"
匹配"acd"
;而在本题中,窗口必须等于子串,不能有其他字母,如"abcd"
匹配"bdac"
,而不可以是"bdaec"
。
首先初始化need
、window
集合,意义同上述。然后把s1
子串的字母及出现次数写入need
。左右边界i
、j
初始化为0
,同时声明变量valid
记录满足的字母数,然后开始while
循环滑动。
首先要扩大窗口,j
加一,判断新移入的字母是否包含在need
集合中,如果是,更新window
;如果更新后window
的这个字母的个数满足need
要求了,valid
加一。如果此时窗口大小等于或者大于了s1
的长度,就要开始判断满足或者进行收缩了,因为大于s1
一定不会有解了,当need
中所有字母的个数都满足时,直接返回true
,否则就收缩窗口,i
加一,判断移出的字母是否在window
中,以及是否导致need
中这一字母的个数不再满足(valid
减一),这些步骤都与覆盖子串题目相同。
参考代码
class Solution {
public boolean checkInclusion(String s1, String s2) {
HashMap<Character,Integer> need=new HashMap<>();
HashMap<Character,Integer> window=new HashMap<>();
// 把s1的字母及出现个数统计到need中
for(int i=0;i<s1.length();i++){
need.put(s1.charAt(i),need.getOrDefault(s1.charAt(i),0)+1);
}
int i=0,j=0;// 左右边界
int valid=0;// 满足的字母数(字母的数量满足)
while(j<s2.length()){
// 首先扩大窗口,让字母进来
char cur=s2.charAt(j);// 本次移入的新字母
j++;// 移入新字母,右边界+1
if(need.containsKey(cur)){// 新字母是所需要的,进行window更新
window.put(cur,window.getOrDefault(cur,0)+1);// window更新
if(need.get(cur).equals(window.get(cur))) valid++;// 匹配上一个字母了(某字母数量满足),匹配的字母数加一
}
// 判断当窗口大小 大于等于匹配串的大小,收缩窗口
while(j-i>=s1.length()){
if(valid==need.size()) return true;// 如果匹配的字母数等于所需字母数了,说明成功匹配到了,返回true
char del=s2.charAt(i);// 要移出的元素
if(window.containsKey(del)){
if(need.get(del).equals(window.get(del))) valid--;// 如果当前该字母数量已经满足要求,那么删除后就不再满足了,因此valid要减一
window.put(del,window.get(del)-1);// 窗口中该元素数量减一
}
i++;// 收缩窗口--左边界+1
}
}
return false;
}
}
4.找到字符串中所有字母异位词(Medium)
题目
给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引。
字符串只包含小写英文字母,并且字符串 s 和 p 的长度都不超过 20100。
说明:
- 字母异位词指字母相同,但排列不同的字符串。
- 不考虑答案输出的顺序。
示例 :
输入:
s: "cbaebabacd" p: "abc"
输出:
[0, 6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的字母异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的字母异位词。
分析
本题和上题(字符串的排列)十分相似,仍然是在一个串s
中找到子串p
的排列,返回匹配成功时的起始索引(左边界i
)。题目的叙述有一些问题,实际与子串p
字母顺序相同的子串也是答案(如原题中示例2:输入s:"abab"
p:"ab"
,输出[0,1,2])。
本题需要额外的一个集合ans
来记录匹配成功的答案。其他基本同上题,见参考代码。
参考代码
class Solution {
public List<Integer> findAnagrams(String s, String p) {
// 记录字母的出现次数 在need集合中
HashMap<Character,Integer> need=new HashMap<>();
HashMap<Character,Integer> window=new HashMap<>();
for(int i=0;i<p.length();i++){
need.put(p.charAt(i),need.getOrDefault(p.charAt(i),0)+1);
}
int i=0,j=0;
int valid=0;
List<Integer> ans=new ArrayList<>();
while(j<s.length()){
// 扩大窗口
char cur=s.charAt(j);
j++;
if(need.containsKey(cur)){
window.put(cur,window.getOrDefault(cur,0)+1);
if(need.get(cur).equals(window.get(cur))){
valid++;
}
}
while((j-i)>=p.length()){// 判断相等及收缩窗口
if(valid==need.size()){
ans.add(i);
}
char del=s.charAt(i);
i++;
if(window.containsKey(del)){
if(need.get(del).equals(window.get(del))){
valid--;
}
window.put(del,window.get(del)-1);
}
}
}
return ans;
}
}
5.无重复字符的最长子串(Medium)
题目
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例 :
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
分析
本题比较简单,我们只需保证窗口window
中的每个字母只能出现一次即可。具体为:从i=0
、j=0
时开始滑动(while
循环),滑动(循环)条件是右边界j<s.length()
。先扩大窗口(j++
),将本次新进入窗口的字母放入window
中,然后判断这个字母在window
中的出现次数是否大于1
,如果大于1
就要收缩窗口以去除重复的字母,使用while
循环,直到这个字母在window
中的出现次数为1
时停止。
经过上面的处理后就能保证当前窗口中所有元素都只会出现一次了(每次新进入window
的字母都这样去判断,进来一个判断一个,最终能保证窗口中元素都不重复),每次滑动后都判断本次窗口大小是否是更大的,记录下这个最大值maxLength
即可。
参考代码
class Solution {
public int lengthOfLongestSubstring(String s) {
HashMap<Character,Integer> window=new HashMap<>();// 记录当前窗口中字母(key)及出现次数(val)
int i=0,j=0;// 左右边界
int maxLength=0;// 最大不重复子串长度
// 滑动
while(j<s.length()){
char cur=s.charAt(j);// 新移入的元素
j++;// 窗口扩大
window.put(cur,window.getOrDefault(cur,0)+1);// 新元素记录在window中
// 当新元素的val大于1(含有多个)时收缩窗口 直到只含一个(多余的该字母被移出窗口)
while(window.get(cur)>1){
char del=s.charAt(i);// 要移出的元素
i++;// 窗口缩小
window.put(del,window.get(del)-1);// 更新window
}
maxLength=Math.max(maxLength,j-i);// 更新最值
}
return maxLength;
}
}
更多
更多题目有待后续更新,喜欢的话点个赞或收藏吧!
感谢您的喜欢 ~ 祝新年快乐呀 ~