LeetCode题解:套用模板解决 滑动窗口 问题

LeetCode题解:套用模板 解决滑动 窗口问题

自大学开始,我便陆陆续续的学习一些 算法和数据结构 方面的内容,同时也开始在一些平台刷题,也会参加一些大大小小的算法竞赛。但是平时刷题缺少目的性、系统性,最终导致算法方面进步缓慢。最终,为了自己的未来,我决定开始在LeetCode上进行系统的学习和练习,同时将刷题的轨迹整理记录,分享出来与大家共勉。


参考资料: 我写了套框架,把滑动窗口算法变成了默写题

参考资料: LeetCode社区官方提供的思路/题解 以及 评论区/题解区各路大神提供的思路/答案



000.模板框架

来源我写了套框架,把滑动窗口算法变成了默写题

Java版本

class Solution{
    /* 滑动窗口算法框架 */
    void slidingWindow(String s, String t) {
        // 字符需要出现的次数
        Map<Character, Integer> needs = new HashMap<Character, Integer>();
        // 滑动窗口中字符出现的次数
        Map<Character, Integer> window = new HashMap<Character, Integer>();

        for (char ch : t.toCharArray())
            needs.put(ch, needs.getOrDefault(ch, 0) + 1);

        int left = 0, right = 0;
        int valid = 0;
        while (right < s.length()) {
            // c 是将移入窗口的字符
            char c = s.charAt(right);
            // 如果满足条件就将 c移入窗口中,并进行窗口内数据的一系列更新
            if (...)
        	...
            // 右移窗口
            right++;


            /*** debug 输出的位置 ***/
            System.out.println("window: [" + left + ", " + right + ")\n");
            /********************/

            // 判断左侧窗口是否要收缩
            while (window needs shrink){
                // d 是将移出窗口的字符
                char d = s.charAt(left);
                // 如果满足一定条件,进行窗口内数据的一系列更新,使得退出这个 while循环
                if (...)
            	...
                // 左移窗口
                left++;
            }
        }
    }
}

详细参数说明:

初始化windowneed两个哈希表,记录窗口中的字符和需要凑齐的字符:

unordered_map<char, int> need, window;
for (char c : t) need[c]++;

然后,使用leftright变量初始化窗口的两端,不要忘了,区间[left, right)是左闭右开的,所以初始情况下窗口没有包含任何元素:

int left = 0, right = 0;
int valid = 0; 
while (right < s.size()) {
    // 开始滑动
}

其中valid变量表示窗口中满足need条件的字符个数,如果validneed.size的大小相同,则说明窗口已满足条件,已经完全覆盖了串T

现在开始套模板,只需要思考以下四个问题

**1、**当移动right扩大窗口,即加入字符时,应该更新哪些数据?

**2、**什么条件下,窗口应该暂停扩大,开始移动left缩小窗口?

**3、**当移动left缩小窗口,即移出字符时,应该更新哪些数据?

**4、**我们要的结果应该在扩大窗口时还是缩小窗口时进行更新?

如果一个字符进入窗口,应该增加window计数器;如果一个字符将移出窗口的时候,应该减少window计数器;当valid满足need时应该收缩窗口;应该在收缩窗口的时候更新最终结果。

076. 最小覆盖子串

难度: 困难

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 ""

**注意:**如果 s 中存在这样的子串,我们保证它是唯一的答案。

示例:

输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"

题解:

套用模板:15ms

class Solution {
    public String minWindow(String s, String t) {
        HashMap<Character,Integer> need = new HashMap();
        HashMap<Character,Integer> window = new HashMap();
        //need存放的不重复的字符出现的次数
        for(char c:t.toCharArray())
            need.put(c,need.getOrDefault(c,0)+1);

        //left,right 表示滑动窗口的左右指针
        int left = 0 , right = 0;
        //valid表示是否满足了t中的字符,不算重复的
        int valid = 0;
        //记录最小覆盖子串的起始索引及长度
        int start = 0 , len = Integer.MAX_VALUE;
        while(right < s.length()){
            char c = s.charAt(right);
            right++;
            //判断取出的字符是否在需要的Map中
            if(need.containsKey(c)){
                window.put(c,window.getOrDefault(c,0)+1);
                if(window.get(c).equals(need.get(c)))
                    valid++;
            }

            //判断是否需要收缩(即已经找到了合适的覆盖串)
            while(valid == need.size()){
                //更新最小覆盖子串
                if(right - left < len){
                    start = left;
                    len = right - left;
                }

                char c1 = s.charAt(left);
                //左移窗口
                left++;

                //进行窗口内数据的一系列更新
                //如果当前要移动的字符是包含在need中,我们需要进行讨论,如果该字符的次数刚好与我们需要的次数相等,则valid--,并同时更新window中这个值出现的次数
                if(need.containsKey(c1)){
                    if(window.get(c1).equals(need.get(c1)))
                        valid--;
                    window.put(c1,window.getOrDefault(c1,0)-1);
                }
            }
        }
        return len == Integer.MAX_VALUE ? "" : s.substring(start, start + len);
    }
}

范例: 2ms

class Solution {
    public String minWindow(String s, String t) {
        String res="";
        if (s.length() < t.length()) return res;
        int[] need = new int[128];
        char[] S=s.toCharArray();
        int cnt=0;
        for (int i = 0; i < t.length(); i++) {
            need[t.charAt(i)]++;
        }
        for (int a : need) {
            if (a > 0) cnt ++;
        }
        for(int i=0,j=0,c=0;i<s.length();i++){
            if(need[S[i]]==1) c++;
            need[S[i]]--;
            while(c==cnt&&need[S[j]]<0) need[S[j++]]++;
            if(c==cnt){
                if(res==""||res.length()>i-j+1)
                    res=s.substring(j,i+1);
            }
        }
        return res;
    }
}
567. 字符串的排列

难度: 中等

给定两个字符串 s1s2,写一个函数来判断 s2 是否包含 s1 的排列。

换句话说,第一个字符串的排列之一是第二个字符串的子串。

示例:

输入: s1 = "ab" s2 = "eidbaooo"
输出: True
解释: s2 包含 s1 的排列之一 ("ba").

对于这道题的解法代码,基本上和最小覆盖子串一模一样,只需要改变两个地方:

**1、**本题移动left缩小窗口的时机是窗口大小大于t.size()时,因为排列嘛,显然长度应该是一样的。

**2、**当发现valid == need.size()时,就说明窗口中就是一个合法的排列,所以立即返回true

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

题解:

套用模板: 30ms

class Solution {
    public boolean checkInclusion(String t, String s) {
        HashMap<Character, Integer> need = new HashMap<>();
        HashMap<Character, Integer> window = new HashMap<>();

        for (char c : t.toCharArray()) {
            need.put(c, need.getOrDefault(c, 0) + 1);
        }

        int left=0,right=0;
        int valid=0;
        
        while (right<s.length()){
            char c=s.charAt(right);
            right++;
            if (need.containsKey(c)){
                window.put(c,window.getOrDefault(c,0)+1);
                if (window.get(c).equals(need.get(c)))
                    valid++;
            }
            //更改:判断左侧窗口是否收缩
            while (right-left>=t.length()){
                //更改:判断是否找到了合适的子串
                if (valid==need.size())
                    return true;

                char c1=s.charAt(left);
                left++;

                if (need.containsKey(c1)) {
                    if (window.get(c1).equals(need.get(c1)))
                        valid--;
                    window.put(c1, window.getOrDefault(c1, 0) - 1);
                }


            }

        }
        return false;

    }
}

范例:2ms

class Solution {
    public boolean checkInclusion(String s1, String s2) {
        char[] str = s2.toCharArray();
        char[] target = s1.toCharArray();
        int sn = str.length;
        int tn = target.length;
        int left = 0;
        int right = 0;
        int l = -1;
        int r = sn;
        int[] winFreq = new int[26];
        int[] needFreq = new int[26];
        for (char c : target){
            needFreq[c - 'a']++;
        }
        while (right < sn){
            int index = str[right] - 'a';
            if (needFreq[index] == 0){
                right++;
                left = right;
                tn = target.length;
                for (int i = 0; i < 26; i++){
                    winFreq[i] = 0;
                }
                continue;
            }
            winFreq[index]++;
            tn--;
            while (winFreq[index] > needFreq[index]){
                winFreq[str[left++] - 'a']--;
                tn++;
            }
            if (tn == 0){
                return true;
            }
            right++;
        }
        return false;
    }
}
438. 找到字符串中所有字母异位词

难度: 中等

给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引。

字符串只包含小写英文字母,并且字符串 sp 的长度都不超过 20100。

说明:

  • 字母异位词指字母相同,但排列不同的字符串。
  • 不考虑答案输出的顺序。

示例:

输入:
s: "cbaebabacd" p: "abc"

输出:
[0, 6]

解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的字母异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的字母异位词。

题解:

套用模板: 34ms

class Solution438{
    public List<Integer> findAnagrams(String s, String t) {
        HashMap<Character, Integer> need = new HashMap<>();
        HashMap<Character, Integer> window = new HashMap<>();

        for (char c : t.toCharArray()) {
            need.put(c, need.getOrDefault(c, 0) + 1);
        }

        int left=0,right=0;
        int valid=0;

        //更改:记录结果集
        List<Integer> res=new ArrayList<>();

        while (right<s.length()){
            char c=s.charAt(right);
            right++;
            if (need.containsKey(c)){
                window.put(c,window.getOrDefault(c,0)+1);
                if (window.get(c).equals(need.get(c)))
                    valid++;
            }

            while (right-left>=t.length()){

                //更改:记录结果
                if (valid==need.size()){
                    res.add(left);
                }


                char c1=s.charAt(left);
                left++;

                if (need.containsKey(c1)) {
                    if (window.get(c1).equals(need.get(c1)))
                        valid--;
                    window.put(c1, window.getOrDefault(c1, 0) - 1);
                }


            }

        }
        return res;

    }
}

范例: 3ms

class Solution {
   public List<Integer> findAnagrams(String s, String p) {
        /**
         * 固定长度滑动窗口,固定长度为p的长度
         * 右侧延伸与左侧收缩同时进行
         *用哈希存储字符频率以做比较
         */
        int n1 = s.length() , n2 = p.length();
        List<Integer> ans = new ArrayList<>();
        if(n2 > n1){
            return ans;
        }
        int[] charCount = new int[26];
        /*初始化,p里所需字符设定为负数,表示需要满足*/
        for(int i = 0 ; i < n2 ; i ++){
            charCount[p.charAt(i) - 'a'] --;
        }
        /*设定固定长度滑动窗口*/
        int left = 0 , right = left - 1 + n2;
        /*由于窗口长度固定而不是慢慢延伸,需要先把初始窗口字符更新进哈希*/
        for(int i = left ; i <= right ; i ++){
            charCount[s.charAt(i) - 'a'] ++;
        }
        /*滑动窗口*/
        while(right < n1){
            /*哈希全是0时即为满足要求,将left索引加入list*/
            boolean isFit = true;
            for(int count : charCount){
                if(count != 0){
                    isFit = false;
                    break;
                }
            }
            if(isFit){
                ans.add(left);
            }
            /*移动窗口,左侧收缩与右侧延伸同时进行*/
            right ++;
            /*右侧延伸后要判断是否越界*/
            if(right < n1){
                charCount[s.charAt(right) - 'a'] ++;
            }
            charCount[s.charAt(left) - 'a'] --;
            left ++;
        }
        return ans;
    }

}
003. 无重复字符的最长子串

难度中等4978收藏分享切换为英文接收动态反馈

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

示例:

输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

题解:

套用模板: 11ms

class Solution003 {
    public 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);
            right++;

            window.put(c, window.getOrDefault(c, 0) + 1);

            //判断收缩
            while (window.get(c) > 1) {

                char c1 = s.charAt(left);
                left++;


                window.put(c1, window.getOrDefault(c1, 0) - 1);
              

            }
            //更新答案
            res=Math.max(res,right-left);

        }
        return res;

    }
}

这就是变简单了,连needvalid都不需要,而且更新窗口内数据也只需要简单的更新计数器window即可。

window[c]值大于 1 时,说明窗口中存在重复字符,不符合条件,就该移动left缩小窗口了嘛。

唯一需要注意的是,在哪里更新结果res呢?我们要的是最长无重复子串,哪一个阶段可以保证窗口中的字符串是没有重复的呢?

这里和之前不一样,要在收缩窗口完成后更新res,因为窗口收缩的 while 条件是存在重复元素,换句话说收缩完成后一定保证窗口中没有重复嘛。

范例: 2ms

class Solution {
    public int lengthOfLongestSubstring(String s) {
        //解法一:滑动窗口
        char[] arr = s.toCharArray();
        int start = 0,end = 0,max = 0;
        while(end < arr.length){
            int index = start;
            while(arr[index] != arr[end] && index < end){
                index++;
            }
            if(index != end){
                max = max > (end - start) ? max : (end - start);
                start = index + 1;
            }
            end++;
       }
        return max > (end - start ) ? max : (end - start);
    }
}

作者:耿鬼不会笑
时间:2021年2月
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值