数组双指针---滑动窗口
序言:
遇到子数组/子串相关的问题,只要能回答出来以下几个问题,就能运用滑动窗口算法:
1、什么时候应该扩大窗口?
2、什么时候应该缩小窗口?
3、什么时候得到一个合法的答案?
一、最小覆盖字串
题目:给定一个字符串 s
、一个字符串 t
。返回 s
中涵盖 t
所有字符的最小子串。如果 s
中不存在涵盖 t
所有字符的子串,则返回空字符串 ""
。
示例1:
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
示例2:
输入: s = "a", t = "aa"
输出: ""
解释: t 中两个字符 'a' 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。
题1解题思路:
1、键值对:字符-->数量HashMap<char,int>
2、开两个窗口,need与windows窗口,首先初始化need窗口,记录目标串所需的所有字符以及对应的个数。
3、左右两个指针,left和right。首先right向右移动,直到匹配到need窗口内的所有字符,然后开始移动left指针缩小窗口,并更新 len
长度。
class Solution {
public String minWindow(String s, String t) {
HashMap<Character,Integer> need = new HashMap<Character, Integer>();
HashMap<Character,Integer> windows = new HashMap<Character, Integer>();
for(int i=0;i<t.length();i++){
char c = t.charAt(i);
need.put(c,need.getOrDefault(c,0)+1);
}
int left = 0 , right = 0;
int valid = 0;
int start = 0 , len = Integer.MAX_VALUE;
//移动right指针直到指针到达终点
while(right<s.length()){
//字符加入窗口操作****
char rc = s.charAt(right);
right++;
if(need.containsKey(rc)){
windows.put(rc,windows.getOrDefault(rc,0)+1);
if(windows.get(rc).equals(need.get(rc))) valid++; //这句很重要!!!
}
//开始移动left指针,缩小窗口
while(valid == need.size()){
//判断是否需要缩小窗口
if(right-left<len){
start = left;
len = right-left;
}
//字符弹出窗口操作****
char lc = s.charAt(left);
left++;
if(need.containsKey(lc)){
if(windows.get(lc).equals(need.get(lc))) valid--;//这句很重要!!!
windows.put(lc,windows.getOrDefault(lc,0)-1);
}
}
}
return len==Integer.MAX_VALUE ? "":s.substring(start,start+len);
}
}
二、字符串排列
题目:给定两个字符串 s1
和 s2
,写一个函数来判断 s2
是否包含 s1
的排列。如果是,返回 true ;否则,返回 false 。换句话说,s1
的排列之一是 s2
的 子串 。
示例 1:
输入:s1 = "ab" s2 = "eidbaooo"
输出:true
解释:s2 包含 s1 的排列之一 ("ba").
示例 2:
输入:s1= "ab" s2 = "eidboaoo"
输出:false
题2解题思路:
一开始简单的以为:只需要一个need字典,将
s1
有的字符一股脑放进need字典里,在s2
维护右指针看看有没有符合的字串就行,每当遇到非法字符,right直接跳到当前位置下一个。但这样是不对的:第一、没有考虑合法字符重复的问题;第二、由于没有考虑到第一点,所以也没有考虑到【合法字符数量超过导致匹配失败后】应该从起始位置下一个字符开始重新遍历!
class Solution{
public boolean checkInclusion(String s1,String s2){
HashMap<Character,Integer> need = new HashMap<>();
HashMap<Character,Integer> windows = new HashMap<>();
for(int i=0;i<s1.length();i++){
char c = s1.charAt(i);
need.put(c,need.getOrDefault(c,0)+1);
}
int left=0,right=0;
int valid=0;
while(right<s2.length()){
char rc = s2.charAt(right);
right++;
if(need.containsKey(rc)){
windows.put(rc,windows.getOrDefault(rc,0)+1);
if(windows.get(rc).equals(need.get(rc))) valid++;
}
if(right-left>s1.length()){
char lc = s2.charAt(left);
left++;
if(need.containsKey(lc)){
if(windows.get(lc).equals(need.get(lc))) valid--;
windows.put(lc,windows.get(lc)-1);
}
}
if(valid==need.size()) return true; //放在这里因为窗口大小一定得合法!!
}
return false;
}
}
三、找所有字母异位词
题目:给定两个字符串 s
和 p
,找到 s
中所有 p
的 异位词的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
异位词指由相同字母重排列形成的字符串(包括相同的字符串)。
示例1:
输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。
示例2:
输入: s = "abab", p = "ab"
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 "ab", 它是 "ab" 的异位词。
起始索引等于 1 的子串是 "ba", 它是 "ab" 的异位词。
起始索引等于 2 的子串是 "ab", 它是 "ab" 的异位词。
题3解题思路:
这题就是上面第二题的升级版,这个所谓的字母异位词,不就是排列吗,搞个高端的说法就能糊弄人了吗?
class Solution{
public List<Integer> findAnagrams(String s, String p){
List<Integer> list = new ArrayList<>();
HashMap<Character,Integer> need = new HashMap<>();
HashMap<Character,Integer> windows = new HashMap<>();
for(int i=0;i<p.length();i++){
char c = p.charAt(i);
need.put(c,need.getOrDefault(c,0)+1);
}
int left=0,right=0;
int res=0;
int valid=0;
while(right<s.length()){
char rc = s.charAt(right);
right++;
if(need.containsKey(rc)){
windows.put(rc,windows.getOrDefault(rc,0)+1);
if(windows.get(rc).equals(need.get(rc))) valid++;
}
if(right-left>p.length()){ //注意是目标子串的长度!
char lc = s.charAt(left);
left++;
res=left;
if(need.containsKey(lc)){
if(windows.get(lc).equals(need.get(lc))) valid--;
windows.put(lc,windows.getOrDefault(lc,0)-1);
}
}
if(valid==need.size()) list.add(res);
}
return list;
}
}
四、无重复字符的最长子串
题目:给定一个字符串 s
,请你找出其中不含有重复字符的最长子串的长度。
示例1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
题4解题思路:
class Solution{
public int lengthOfLongestSubstring(String s) {
HashMap<Character,Integer> windows = new HashMap<>();
int left=0,right=0;
int len=0;
while(right<s.length()){
char rc = s.charAt(right);
right++;
windows.put(rc,windows.getOrDefault(rc,0)+1);
while(windows.get(rc)>1){
char lc = s.charAt(left);
left++;
windows.put(lc,windows.getOrDefault(lc,0)-1);
}
if(right-left>len) len = right-left;
}
return len;
}
}
五、滑动窗口的最大值
题目:给定一个数组 nums
和滑动窗口的大小 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
题4解题思路:
- 特殊数据结构:单调队列(
monotonicQueue
)
在⼀堆数字中,已知最值,如果给这堆数添加⼀个数,那么比较⼀下就可以很快算出最值;但如果减少⼀个数,就不一定能很快得到最值了,而是要遍历所有数重新找最值。 回到这道题的场景,每个窗口前进的时候,要添加⼀个数同时减少⼀个数, 所以想在 O(1) 的时间得出新的最值,就需要「单调队列」这种特殊的数据结构来辅助了。
class MonotonicQueue {
void push(int n); //在队尾添加元素n
int max(); //返回当前队列中的最⼤值
void pop(int n); //队头元素如果是n,删除它
}
单调队列的 push 方法依然在 队尾添加元素,但是要把前面比新元素小的元素都删掉:相当于加入数字的大小代表人的体重,把前面体重不足的都压扁了,直到遇到更大的量级才停住。
基于上述,pop方法就是弹出队头元素,但是要加判断
data.front() == n
,因为我们想删除的队头元素 n 可能已经被「压扁」了,如果被「压扁」了就不用再进行删除操作,因为该元素已经不在队列里面了。
class Solution{
class monotonicQueue{
LinkedList<Integer> list = new LinkedList<>();
//在队尾添加元素n,重要!!!
public void push(int n){
while(!list.isEmpty()&&list.getLast()<n){
list.pollLast();
}
list.addLast(n);
}
//返回当前队列中的最⼤值
public int max(){
return list.getFirst();
}
//队头元素如果是n,删除它; 若不是,则代表该元素已经被“压扁”了,不用进行删除操作
public void pop(int n){
if(list.getFirst()==n) list.pollFirst();
}
}
public int[] maxSlidingWindow(int[] nums, int k) {
monotonicQueue window = new monotonicQueue();
List<Integer> res = new ArrayList<>();
for(int i=0;i<nums.length;i++){
if(i<k-1){
window.push(nums[i]);
}else{
window.push(nums[i]);
res.add(window.max());
window.pop(nums[i - k + 1]);
}
}
int[] ans = new int[res.size()];
for(int i=0;i<res.size();i++){
ans[i]=res.get(i);
}
return ans;
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律