数组双指针之滑动窗口

目录

1.无重复字符的最长子串

2.最小覆盖子串

3.字符串排列

4.找到字符串中所有字母异位词

5.滑动窗口最大值


滑动窗口的思路非常简单,就是维护一个窗口,不断滑动,然后更新答案。算法中最难的是各种细节问题,比如如何在窗口中添加元素、更新结果。本文给出滑动窗口的代码框架,再通过几道题来套用框架。其实将框架的原理明白了,这些题闭着眼睛都能写出来。

```Java
//滑动窗口框架
public static int lengthOfLongestSubstring(String s){
        HashMap<Character,Integer> window=new HashMap<>();//计数窗口
        int left = 0, right = 0;
        int res=0;
        while (right < s.length()) {
           //缩小窗口
            if (window.containsKey(s.charAt(right))) {
                left=Math.max(window.get(s.charAt(right))+1,left);
            }
            // 增大窗口
            window.put(s.charAt(right),right);
            //更新结果
            res=Math.max(res,right-left+1);
            right++;
        }
        return res;
    }
       
    }
```


1.无重复字符的最长子串

leetcode3题
要求:给定⼀个字符串 s,请你找出其中不含有重复字符的最⻓⼦串的⻓度。
基本思路:当窗口有重复元素的时候,窗口左指针收缩到最近相同元素的下一个元素,没有重复元素右指针右移,有了新的窗口之后重新更新结果。

本道题是最简单的滑动窗口题,具体解法在框架中已经给出一种。只针对本题,window哈希表中存储的值可以是字符的索引,这样左指针就不需要每次移动一步。window哈希表中存储的值也可以是字符出现的数量,如果右指针指向的字符在哈希表中大于1,左指针指向的字符在哈希表中的值减1,左指针右移,直到右指针指向的字符在哈希表中不大于1。

就本题而言后者肯定比前者效率低,因为每次只移动一个字符。但后者在window中能记录字符的数量。

```Java
public static int lengthOfLongestSubstring(String s){
        HashMap<Character,Integer> window=new HashMap<>();//计数窗口
        int left = 0, right = 0;
        int res=0;
        while (right < s.length()) {
            char c=s.charAt(right);
            // 增大窗口
            if(window.get(c)==null){
                window.put(c,0);
            }
            window.put(c,window.get(c)+1);
            //缩小窗口
            while (window.get(c)>1) {
                window.put(s.charAt(left),window.get(s.charAt(left))-1);
                left++;
            }
            res=Math.max(res,right-left+1);
            right++;
        }
        return res;
    }
```

2.最小覆盖子串

leetcode76题
要求:给你⼀个字符串 s 、⼀个字符串 t,返回 s 中涵盖 t 所有字符的最⼩⼦串;如果 s 中不存在涵盖 t 所有字符的⼦串,则返回空字符串 ""。

解题思路:这道题可以用上题中的第二种框架,定义两个哈希表,一个记录窗体中的字符个数,一个记录t中的字符个数,当窗体可以覆盖子串的时候,移动左指针直至窗体不可以覆盖,再接着移动右指针。

现在开始套模板,只需要思考以下四个问题:
1、当移动right扩大窗口,即加入字符时,应该更新哪些数据?
2、什么条件下,窗口应该暂停扩大,开始移动left缩小窗口?
3、当移动left缩小窗口,即移出字符时,应该更新哪些数据?
4、我们要的结果应该在扩大窗口时还是缩小窗口时进行更新?

1. 本题window只计数need中有的字符,加入字符更新字符的个数
2. 当window与need中所有相同字符的个数相同,即可以覆盖时窗口停止扩大,开始缩小窗口
3. 每次移除字符,如果该字符window中存在,则计数减1,当该字符的window中计数值等于need中的数量,valid减1,因为减完1之后,该字符的计数减1会不满足覆盖。重复步骤1加入字符串。
4. 我们要的结果是在缩小窗口时更新,因为缩小窗口时满足覆盖条件。

```Java
//2.最小覆盖子串
    public static String minCoSubSring(String s,String t){
        HashMap<Character,Integer> window=new HashMap<>();//窗口计数
        HashMap<Character,Integer> need=new HashMap<>();//t字符串计数
        //统计t字符串中不同字符的个数
        for(int i=0;i<t.length();i++){
            char c=t.charAt(i);
            if(need.get(c)==null){
                need.put(c,0);
            }
            need.put(c,need.get(c)+1);
        }
        //滑动窗口
        int left = 0, right = 0;
        int valid=0;//满足need条件的字符个数
        int start = 0, len = s.length()+1;//记录起始索引和长度
        while (right < s.length()) {
            char c=s.charAt(right);
            // 增大窗口
            //数据更新
            if(need.containsKey(c)){
                if(window.get(c)==null){
                    window.put(c,0);
                }
                window.put(c,window.get(c)+1);
                if(window.get(c).intValue()==need.get(c).intValue()){
                    valid++;//满足need条件的字符
                }
            }
            //更新输出:在扩大窗口满足条件之后
            //缩小窗口:满足need条件的字符的数量等于need中字符的种类
            while (valid==need.size()) {
                //更新输出:在扩大窗口满足条件之后
                if(right-left+1<len){//保证是最小覆盖子串
                    start=left;
                    len=right-left+1;
                }
                char d=s.charAt(left);
                if(window.containsKey(d)){
                    if(window.get(d).intValue()==need.get(d).intValue()){
                        valid--;
                    }
                    window.put(d,window.get(d)-1);
                }
                left++;
            }
            right++;
        }
        return len==s.length()+1?"":s.substring(start,start+len);
    }
```

3.字符串排列

leetcode567题
要求:给定两个字符串t和s,写一个函数来判断s中是否包含t的排列,换句话来说,第一个字符串的排列之一是第二个字符串的子串。

基本思路:对于这道题基本和最小覆盖一样,只有两个条件有点变化,第一,缩小窗口的时机是窗口大小等于t.size()时,即窗口中的长度等于t中的长度,第二,当发现valid==need.size()时,说明窗口中就有一个合法的排列,所以立即返回true。

至于如何处理窗口的扩大和缩小,和最小覆盖子串完全相同。
 

```Java
 public static boolean checkInclusion(String t,String s){
        HashMap<Character,Integer> window=new HashMap<>();//窗口计数
        HashMap<Character,Integer> need=new HashMap<>();//t字符串计数
        //统计t字符串中不同字符的个数
        for(int i=0;i<t.length();i++){
            char c=t.charAt(i);
            if(need.get(c)==null){
                need.put(c,0);
            }
            need.put(c,need.get(c)+1);
        }
        //滑动窗口
        int left = 0, right = 0;//定义左右指针
        int valid=0;//满足need条件的字符个数
        while (right < s.length()) {
            char c=s.charAt(right);
            // 增大窗口
            if(need.containsKey(c)){
                if(window.get(c)==null){
                    window.put(c,0);
                }
                window.put(c,window.get(c)+1);
                if(window.get(c).intValue()==need.get(c).intValue()){
                    valid++;//满足need条件的字符
                }
            }
            //缩小窗口:重排列长度肯定得一样
            while (right-left+1==t.length()) {
                //更新输出:长度一样之后有满足和不满足重排列两种情况
                if(need.size()==valid){//满足重排列
                    return true;
                }
                //窗口收缩
                char d=s.charAt(left);
                if(window.containsKey(d)){
                    if(window.get(d).intValue()==need.get(d).intValue()){
                        valid--;
                    }
                    window.put(d,window.get(d)-1);
                }
                left++;
            }
            right++;
        }
        return false;
    }
```

熟练框架之后是不是觉得这种难题瞬间变简单了,只要分析清楚窗口扩大window、valid怎么更新,满足什么条件窗口缩小,窗口搜小window、valid怎么更新,最终得输出结果是在扩大窗口满足条件之后更新,还是缩小窗口之后满足条件更新。

4.找到字符串中所有字母异位词

leetcode438题
要求:给定两个字符串s和p,找到s中所有p的异位词子串,返回这些子串的起始索引。不考虑答案输出的顺序。异位词指由相同字母重排列形成的字符串(包括相同的字符串)。

聪明的同学可能就发现了异位词不就是重排列嘛,当满足重排列的条件之后记录子串的起始索引即可。

```Java
public List<Integer> findAnagrams(String s, String p){
        HashMap<Character,Integer> window=new HashMap<>();
        HashMap<Character,Integer> need=new HashMap<>();
        List<Integer> flag=new ArrayList<>();
        //统计p中字符的个数
        for(int i=0;i<p.length();i++){
            char c=p.charAt(i);
            if(need.get(c)==null){
                need.put(c,0);
            }
            need.put(c,need.get(c)+1);
        }
        //滑动窗口
        int left=0,right=0;
        int valid=0;
        while(right<s.length()){
            //增大窗口
            char c=s.charAt(right);
            if(need.containsKey(c)){
                if(window.get(c)==null){
                    window.put(c,0);
                }
                window.put(c,window.get(c)+1);
                if(window.get(c).intValue()==need.get(c).intValue()){
                    valid++;
                }
            }
            //缩小窗口
            while(right-left+1==p.length()){
                if(valid==need.size()){
                    flag.add(left);
                }
                char d=s.charAt(left);
                if(window.containsKey(d)){
                    if(window.get(d).intValue()==need.get(d).intValue()){
                        valid--;
                    }
                    window.put(d,window.get(d)-1);
                }
                left++;
            }
            right++;
        }
        return flag;
    }
```

以上答案是不参考上面的答案仅凭框架思维写出来的,非常快速,弄懂原理之后无需记忆无需背诵,随随便便手撕代码。

5.滑动窗口最大值

leetcode239题
要求:给你⼀个整数数组 nums,有⼀个⼤⼩为 k 的滑动窗⼝从数组的最左侧移动到数组的最右侧,返回滑动窗⼝中的最⼤值。

对于本题不需要计数窗口,只需要一个长度为k的单调队列来保存窗口中的数。其它的按照框架来写即可。

```Java
// 实现单调队列
    static class MonotonicQueue{
        LinkedList<Integer> q = new LinkedList<>();
        public void push(int n){
            //将小于n的元素全部删除
            while(!q.isEmpty()&&q.getLast()<n){
                q.pollLast();
            }
            //然后将n加入尾部
            q.addLast(n);
        }
        public void pop(int n){
            if(n==q.getFirst()){//n在队列中就删除,也有可能n不是最大值被覆盖了
                q.pollFirst();
            }
        }
        public int max(){return q.getFirst();}
    }
    public static int[] maxSlidingWindow(int[] nums,int k){
        MonotonicQueue window=new MonotonicQueue();
        List<Integer> res=new ArrayList<>();
        int left=0,right=0;
        while (right<nums.length){
            //增大窗口
            int a=nums[right];
            window.push(a);
            //缩小窗口
            while(right-left+1==k){
                //更新结果
                res.add(window.max());
                window.pop(nums[left]);
                left++;
            }
            right++;
        }
        //需要转成 int[]数组返回
        int[] arr=new int[res.size()];
        for(int i=0;i<res.size();i++){
            arr[i]=res.get(i);
        }
        return arr;
    }
```

### 双指针滑动窗口算法的应用 双指针滑动窗口算法是一种高效的线性扫描方法,在处理数组字符串问题时特别有用。该算法能够有效地解决涉及连续子序列或子串的问题,而不需要嵌套循环带来的高时间复杂度。 #### 应用场景 1. **寻找特定条件下的最小子串** 当面对需要找出满足某些约束条件下最小长度的子串问题时,滑动窗口可以通过动态调整窗口大小来高效求解[^1]。 2. **计算不重复字符的最大数量** 对于像“无重复字符的最长子串”的题目,利用哈希表配合滑动窗口技术可以在一次遍历中完成查找工作[^2]。 3. **两数之和等于目标值** 如果是在已排序或者部分有序的数据结构上操作,则可以考虑使用对撞指针形式的双指针法;而在未排序情况下也可以先进行预处理再应用此策略。 4. **固定宽度内的极值查询** 需要频繁访问区间内元素特性(如最大/最小值)的情况下,借助单调队列优化后的滑窗能提供更优性能表现[^3]。 ### 实现示例 下面给出一段 Python 代码作为例子,用于解决问题:“给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。” ```python def lengthOfLongestSubstring(s: str) -> int: char_map = {} left, max_length = 0, 0 for right in range(len(s)): if s[right] in char_map and char_map[s[right]] >= left: left = char_map[s[right]] + 1 char_map[s[right]] = right current_window_size = right - left + 1 max_length = max(max_length, current_window_size) return max_length ``` 这段程序实现了上述提到的功能,并且采用了字典 `char_map` 来追踪已经遇到过的字符及其最新位置,以此决定何时应该收缩左侧边界以保持当前窗口内部不含重复项。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值