滑动窗口算法框架解析

算法的大致逻辑如下:

int left = 0, right = 0;

while (right < s.size()) {
    // 增大窗口
    window.add(s[right]);
    right++;
    
    while (window needs shrink) {
        // 缩小窗口
        window.remove(s[left]);
        left++;
    }
}

这个算法技巧的时间复杂度是 O(N),比字符串暴力算法要高效得多。

框架:

/* 滑动窗口算法框架 */
void slidingWindow(string s, string t) {
    Map<char,Integer> need=new HashMap<>(), window=new HashMap<>();
    char[] tCh=t.toCharArray();
    for (char c : tCh)
    		map.put(c, map.getOrDefault(c, 0) + 1)
    
    int left = 0, right = 0;
    int valid = 0; 
    while (right < s.size()) {
        // c 是将移入窗口的字符
        char c = s[right];
        // 增大窗口
        right++;
        // 进行窗口内数据的一系列更新
        ...

        /*** debug 输出的位置 ***/
        printf("window: [%d, %d)\n", left, right);
        /********************/
        
        // 判断左侧窗口是否要收缩
        while (window needs shrink) {
            // d 是将移出窗口的字符
            char d = s[left];
            // 缩小窗口
            left++;
            // 进行窗口内数据的一系列更新
            ...
        }
    }
}

其中两处 ... 表示的更新窗口数据的地方,到时候你直接往里面填就行了。 这两个 ... 处的操作分别是扩大和缩小窗口的更新操作,等会你会发现它们操作是完全对称的。

一、最小覆盖子串

力扣第 76 题「 最小覆盖子串

在这里插入图片描述

滑动窗口算法的思路是这样:

  1. 我们在字符串 S 中使用双指针中的左右指针技巧,初始化 left = right = 0,把索引左闭右开区间 [left, right) 称为一个「窗口」。
  2. 我们先不断地增加 right 指针扩大窗口 [left, right),直到窗口中的字符串符合要求(包含了 T 中的所有字符)。
  3. 此时,我们停止增加 right,转而不断增加 left 指针缩小窗口 [left, right),直到窗口中的字符串不再符合要求(不包含 T 中的所有字符了)。同时,每次增加 left,我们都要更新一轮结果。
  4. 重复第 2 和第 3 步,直到 right 到达字符串 S 的尽头。

图解:
初始状态:

在这里插入图片描述

增加 right,直到窗口 [left, right) 包含了 T 中所有字符:

在这里插入图片描述

现在开始增加 left,缩小窗口 [left, right)

在这里插入图片描述

直到窗口中的字符串不再符合要求,left 不再继续移动:

在这里插入图片描述

之后重复上述过程,先移动 right,再移动 left…… 直到 right 指针到达字符串 S 的末端,算法结束。

接下来我们来分析一下框架

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

Map<char,Integer> need=new HashMap<>(), window=new HashMap<>();
char[] tCh=t.toCharArray();
for (char c : tCh)
    map.put(c, map.getOrDefault(c, 0) + 1)

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

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

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

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

完整代码:

class Solution {
    public String minWindow(String s, String t) {
        Map<Character,Integer> need=new HashMap<>(), window=new HashMap<>();
        char[] tCh=t.toCharArray();
        for (char c : tCh)
            need.put(c, need.getOrDefault(c, 0) + 1);

        // 记录最小覆盖子串的起始索引及长度
        int left=0,right=0;
        int valid=0;
        int start = 0, len = Integer.MAX_VALUE;
        while(right<s.length()){
            // rch 是将移入窗口的字符
            char rch=s.charAt(right);
            // 扩大窗口
            right++;
            // 进行窗口内数据的一系列更新
            if(need.containsKey(rch)){
                window.put(rch, window.getOrDefault(rch, 0) + 1);
                if(need.get(rch).equals(window.get(rch))) valid++;
            }
            // 判断左侧窗口是否要收缩
            while(valid==need.size()){
                // 在这里更新最小覆盖子串
                if (right - left < len) {
                    start = left;
                    len = right - left;
                }
                // lch 是将移出窗口的字符
                char lch=s.charAt(left);
                // 缩小窗口
                left++;
                // 进行窗口内数据的一系列更新
                if(need.containsKey(lch)){
                    if(window.get(lch).equals(need.get(lch))) valid--;
                    window.put(lch,window.get(lch)-1);
                }
                
            }
        }
        return len==Integer.MAX_VALUE?"":s.substring(start,start+len);
    }
}

非常需要注意的是使用 Java 的读者要尤其警惕语言特性的陷阱。Java 的 Integer,String 等类型判定相等应该用 equals 方法而不能直接用等号 ==,这是 Java 包装类的一个隐晦细节。所以在缩小窗口更新数据的时候,不能直接写为 window.get(d) == need.get(d),而要用 window.get(d).equals(need.get(d)),之后的题目代码同理。

当然,代码还可以简化,使用128长度的数组代替HashMap能达到相同的效果

二、字符串排列

力扣第 567 题「 字符串的排列
在这里插入图片描述

class Solution {
    public boolean checkInclusion(String t, String s) {
        Map<Character,Integer> need=new HashMap<>(), window=new HashMap<>();
        char[] tCh=t.toCharArray();
        for (char c : tCh)
            need.put(c, need.getOrDefault(c, 0) + 1);

        // 记录最小覆盖子串的起始索引及长度
        int left=0,right=0;
        int valid=0;
        //int start = 0, len = Integer.MAX_VALUE;
        while(right<s.length()){
            // rch 是将移入窗口的字符
            char rch=s.charAt(right);
            // 扩大窗口
            right++;
            // 进行窗口内数据的一系列更新*
            if(need.containsKey(rch)){
                window.put(rch, window.getOrDefault(rch, 0) + 1);
                if(need.get(rch).equals(window.get(rch))) valid++;
            }
            // 判断左侧窗口是否要收缩
            while(right-left>=t.length()){
                // 在这里更新最小覆盖子串
                if(valid==need.size()) return true;
                // lch 是将移出窗口的字符
                char lch=s.charAt(left);
                // 缩小窗口
                left++;
                // 进行窗口内数据的一系列更新*
                if(need.containsKey(lch)){
                    if(window.get(lch).equals(need.get(lch))) valid--;
                    window.put(lch,window.get(lch)-1);
                }
                
            }
        }
        return false;
    }
}


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

  1. 本题移动 left 缩小窗口的时机是窗口大小大于 t.size() 时,应为排列嘛,显然长度应该是一样的。
  2. 当发现 valid == need.size() 时,就说明窗口中就是一个合法的排列,所以立即返回 true。至于如何处理窗口的扩大和缩小,和最小覆盖子串完全相同。

三、找所有字母异位词

力扣第 438 题「 找到字符串中所有字母异位词
**相当于,输入一个串 S,一个串 T,找到 S 中所有 T 的排列,返回它们的起始索引。**和上一道题几乎一模一样

默写框架:

class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        List<Integer> res=new ArrayList<>();
        if(s.length()<p.length()) return res;

        Map<Character,Integer> need=new HashMap<>(), window=new HashMap<>();
        char[] pCh=p.toCharArray();
        for (char c : pCh)
            need.put(c, need.getOrDefault(c, 0) + 1);
        int left=0,right=0,valid=0;
        int len=p.length();
        while(right<s.length()){
            char rch=s.charAt(right);
            right++;
            if(need.containsKey(rch)){
                window.put(rch,window.getOrDefault(rch, 0) + 1);
                if(need.get(rch).equals(window.get(rch))) valid++;
            }

            while(right-left>=p.length()){
                if(valid==need.size()){
                    res.add(left);
                }
                char lch=s.charAt(left);
                left++;
                if(need.containsKey(lch)){
                    if(need.get(lch).equals(window.get(lch))) valid--;
                    window.put(lch,window.get(lch) - 1);
                }
            }
        }
        return res;
    }
}

四、最长无重复子串

力扣第 3 题「 无重复字符的最长子串

在这里插入图片描述

class Solution {
    public int lengthOfLongestSubstring(String s) {
        Map<Character,Integer> window=new HashMap<>();
        int left=0,right=0,max=0;
        while(right<s.length()){
            char rch=s.charAt(right);
            right++;
            window.put(rch,window.getOrDefault(rch,0)+1);
            while(window.get(rch)>1){
                char lch=s.charAt(left);
                left++;
                window.put(lch,window.get(lch)-1);
            }
            max=Math.max(right-left,max);
        }
        return max;
    }
}

这就是变简单了,连 needvalid 都不需要,而且更新窗口内数据也只需要简单的更新计数器 window 即可。
window[c] 值大于 1 时,说明窗口中存在重复字符,不符合条件,就该移动 left 缩小窗口了嘛。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值