滑动窗口算法

算法思想

滑动窗口是一种基于双指针的一种思想,就是两个指针指向的元素之间形成一个窗口,并且左右指针方向一致的向前滑动(右端固定,左端滑动;左端固定,右端滑动)。因此,解决该问题的关键在于确定两个指针如何移动。

分类:窗口有两类,一种是固定大小类的窗口,一类是大小动态变化的窗口。

核心步骤

在序列中,使用双指针中的左右指针,步骤如下:

  1. 初始化left = right = 0,把索引区间[left, right)称为一个窗口(注意该窗口是左开右闭,因为初始窗口[0,0)区间没有任何元素)。
  2. 循环遍历,不断的增加right指针,扩大窗口[left, right),直到窗口中的序列符合要求。
  3. 满足要求后,停止增加right指针,转而不断增加left指针缩小窗口[left, right),直到窗口中的序列不再符合要求。同时,每次增加 left前,都要更新一轮结果
  4. 重复第 2 和第 3 步,直到right指针到达序列的尽头。

注意:步骤二类似于在寻找一个可行解,而步骤三则是在优化可行解,最终找到一个最优解
滑动窗口的求解步骤就是左右指针轮流前进,窗口大小增增减减,窗口不断向右滑动。
在这里插入图片描述

算法模板

双层循环

left,right := 0,0 // 左右指针

// 窗口右边界滑动
for right < length {
  window.add(s[right++])      // 右元素进窗,且右指针加1

  // 窗口满足条件,寻找最优解(右指针不动,左指针开始移动)
  for valid(window) && left<right {
    ...                      // 满足条件后的操作,可以记录或者更新全局数据
    window.remove(arr[left++]) // 左元素出窗,且左指针移动,直到窗口不满足条件
  }
}

单层循环

left,right := 0,0 // 左右指针

// 窗口右边界滑动
for right < length {
  // 不满足可行解条件,则扩大窗口
  if !valid(window){
    window.add(s[right++])      // 右元素进窗,且右指针加1
  }else{
    ...                      // 满足条件后的操作,可以记录或者更新全局数据
    window.remove(arr[left++]) // 左元素出窗,且左指针移动,直到窗口不满足条件
  }
}

例题

固定窗口:

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

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

输入: 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” 的异位词

分析:该问题为固定滑动窗口问题,对于p的异位词,其长度一定与字符串p的长度相等,因此,可以在字符串s中维护一个与字符串p长度相等且固定长度的滑动窗口。当窗口中每种字符的数量与字符串p中每种字符的数量相同时,则该窗口内的字符串与字符串p为异位词。

对于滑动窗口中每种字符的数量统计与字符串p中每种字符的数量统计可以利用字符的ASCII码来映射到数组中进行统计。如,对于字符b,可以通过 ′ b ′ − ′ a ′ = 1 'b'-'a'=1 ba=1 来映射到数组中。

那么在窗口滑动过程中,固定其窗口长度,并且分别统计窗口与字符串p中字符的数量。当两个数组中每个分量的数据相同时,则表示其为一个异位词。因此创建两个数组,window_count以及p_count用于统计。

public List<Integer> findAnagrams(String s, String p) {  
    int s_len = s.length(), p_len = p.length();  
    List<Integer> ans = new ArrayList<Integer>();//用于记录窗口滑动过程中的可行解  
  
    //字符s的长度一定要大于字符p的长度,否则不存在异位词  
    if (s_len < p_len) {  
        return ans;  
    }  
    
    int[] window_count = new int[26]; //用于维护窗口滑动过程中每个字符的数量  
    int[] p_count = new int[26]; //用于统计字符p的每个字符数量  
    
    //初始统计  
    for (int i = 0; i < p_len; i++) {  
        window_count[s.charAt(i) - 'a']++;  
        p_count[p.charAt(i) - 'a']++;  
    }  
    
    //如果窗口最初始的时候满足异位词,则将0加入到ans数组中  
    if (Arrays.equals(window_count, p_count))  
        ans.add(0);  
        
    //窗口开始滑动,左右都按照同频率滑动  
    for (int i = 0; i < s_len - p_len; i++) {  
        window_count[s.charAt(i) - 'a']--;    // 左指针移动  
        window_count[s.charAt(i + p_len) - 'a']++;  // 右指针移动  
        
        //判断是否满足异位词的条件,满足加入到ans中  
        if (Arrays.equals(window_count, p_count))  
            ans.add(i + 1);  
            
    }  
    
    return ans;  
}

对于以上的代码中,发现对于统计字符串p的字符数量数组p_count只统计了一次之后就没有变动。因此可以考虑只统计滑动窗口中字符和字符串p中每种字母数量的差,存于数组count中,当count数组中所有的差都为0的时候,表示滑动窗口中的字符与字符串p构成异位词。该方案减少了对字符串p的统计内存消耗,在空间上进行了优化。

当然,在考虑统计滑动窗口和字符串p中每种字母数量的差,存于数组count中后, 是否可以通过一个变量diff用于记录滑动窗口中的字符与字符串p中数量不同的字母的个数,也就是count数组中不为0的个数。当diff值等于0之后,表示滑动窗口中的字符和字符串p中每种字母数量的差都为0,即构成异位词。对于该方案,解决的关键在于如何在窗口滑动过程维护diff变量。

比如说,字符abbbbaaaaacount数组为 [ − 4 , 4 , 0 , . . . ] [-4,4,0,...] [4,4,0,...] ,该diff值为2。字符abbbb与字符abccccount数组为 [ 0 , 3 , − 3 , 0 , . . . ] [0,3,-3,0,...] [0,3,3,0,...] ,该diff值也为2。

diff变量的变化主要在于窗口滑动过程中。而字符串p不发生改变,那么对于窗口滑动过程中,主要根据窗口中左右指针所指向的字母的变化从而改动count数组,进而改变diff的值:

  1. 左指针指向的字符s[left]导致diff变化情况有:如果在数组count中,字符s[left]所对应的值为1或者0时,会导致窗口滑动一个单位长度,会使得count数组中0的个数会变化,进而导致diff值发生改变。
  2. 右指针指向的字符s[right]导致diff变化情况有:如果在数组count中,字符s[right]所对应的值为-1或者0时,也会导致窗口滑动一个单位长度,会导致count数组中0的个数发生变化,进而导致diff值发生改变。
    于是,代码如下:
public List<Integer> findAnagrams(String s, String p) {  
  
    int s_len = s.length(), p_len = p.length();  
    List<Integer> ans = new ArrayList<Integer>();//用于记录窗口滑动过程中的可行解  
    
    //字符s的长度一定要大于字符p的长度,否则不存在异位词  
    if (s_len < p_len) {  
        return ans;  
    }  
    
    int[] count = new int[26]; //用于维护窗口滑动过程中窗口中的字符与字符p中每个字符的差值  
    int diff = 0; // 用于维护count数组中不为0的个数  
    
    //初始统计  
    for (int i = 0; i < p_len; i++) {  
        count[s.charAt(i) - 'a']++;  
        count[p.charAt(i) - 'a']--;  
    }  
    
    // 统计count数组中不为0的个数,并赋值给diff  
    for (int i = 0; i < count.length; i++) {  
        if (count[i] != 0) diff++;  
    }  
    
    //如果窗口最初始的时候满足异位词,则将0加入到ans数组中  
    if (diff == 0) ans.add(0);  
    
    //窗口开始滑动,左右都按照同频率滑动  
    for (int i = 0; i < s_len - p_len; i++) {  
        //左指针移动导致diff变化的情况  
        if (count[s.charAt(i) - 'a'] == 1) {  
            diff--;  
        } else if (count[s.charAt(i) - 'a'] == 0) {  
            diff++;  
        }  
        count[s.charAt(i) - 'a']--;    // 左指针移动  
          
        //右指针移动导致diff变化的情况  
        if (count[s.charAt(i + p_len) - 'a'] == -1) {  
            diff--;  
        } else if (count[s.charAt(i + p_len) - 'a'] == 0) {  
            diff++;  
        }  
        count[s.charAt(i + p_len) - 'a']++;  // 右指针移动  
        
        //判断是否满足异位词的条件,满足加入到ans中  
        if (diff == 0) ans.add(i + 1);  
    }  
    return ans;  
}

java细节优化,在java版本19.0.2中,对于charAt()的实现中有对数组下标进行越界判断:

public static <X extends RuntimeException>  
int checkIndex(int index, int length,  
               BiFunction<String, List<Number>, X> oobef) {  
    if (index < 0 || index >= length)  
        throw outOfBoundsCheckIndex(oobef, index, length);  
    return index;  
}

如果要考虑这部分的性能优化,在java中对于字符串的遍历,考虑将字符串通过方法toCharArray()将字符串转化为字符数组。

不固定大小的窗口

LC76. 最小覆盖子串

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。
注意:
对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
如果 s 中存在这样的子串,我们保证它是唯一的答案。

示例 1:
输入:s = “ADOBECODEBANC”, t = “ABC”
输出:“BANC”
解释:最小覆盖子串 “BANC” 包含来自字符串 t 的 ‘A’、‘B’ 和 ‘C’。

示例 2:
输入:s = “a”, t = “a”
输出:“a”
解释:整个字符串 s 是最小覆盖子串。

示例 3:
输入: s = “a”, t = “aa”
输出: “”
解释: t 中两个字符 ‘a’ 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。

public String minWindow(String s, String t) {
    // 定义一个长度为128的int数组,用于记录每个字符出现的次数
    //每个数组的下标代表着该字符的ASCII数值
    int[] chars = new int[128];

    // 定义一个长度为128的boolean数组,用于标记每个字符是否在t字符串中出现
    boolean[] flag = new boolean[128];

    // 统计字符t中的字符情况
    for (int i = 0; i < t.length(); i++) {
        // 标记t字符串中出现的字符
        flag[t.charAt(i)] = true;
        // 统计t字符串中每个字符出现的次数
        ++chars[t.charAt(i)];
    }
    // 定义变量cnt、l、min_l、min_size,分别表示当前滑动窗口中包含t字符串的字符个数、窗口左端点、最小子串的左端点和长度。
    int cnt = 0, l = 0, min_l = 0, min_size = s.length() + 1;

    // 滑动窗口
    for (int r = 0; r < s.length(); ++r) {
        // 如果当前字符在t字符串中出现
        if (flag[s.charAt(r)]) {

            if (--chars[s.charAt(r)] >= 0) {
                ++cnt;
            }
            // 若目前滑动窗口已包含T中全部字符,
            // 则尝试将l右移, 在不影响结果的情况下获得最短子字符串
            while (cnt == t.length()) { 
	            // 如果当前滑动窗口已经包含t字符串中的所有字符
	            // 判断当前子串的长度是否小于之前求得的最小子串长度
                if (r - l + 1 < min_size) { 
                    min_l = l; // 更新最小子串的左端点
                    min_size = r - l + 1; // 更新最小子串的长度
                }
                // 将窗口左端点l右移,缩小窗口大小,如果l对应的字符在t字符串中出现,并且其在chars数组中对应的值加1后大于0,则表示当前窗口中已经不包含该字符的所有出现次数
                if (flag[s.charAt(l)] && ++chars[s.charAt(l)] > 0) { 
                    --cnt; 
                }
                ++l; // 将窗口左端点l右移
            }
        }
    }

    // 返回最小子串,如果不存在,则返回空字符串。
    return min_size > s.length() ? "" : s.substring(min_l, min_l + min_size);
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值