【Java数据结构与算法】滑动窗口思想及算法题解

滑动窗口

滑动窗口,一般以两个指针确定一个不是固定大小的窗体,向右滑动。

例题

剑指 Offer II 014. 字符串中的变位词

题目:给定两个字符串 s1 和 s2,写一个函数来判断 s2 是否包含 s1 的某个变位词。即第一个字符串的排列之一是第二个字符串的 子串 。


思路: 最简单的想法是穷举s2的所有子串,然后对比,也就是固定一个窗长,每一移动一位,对比,这样十分低效,且复杂。如果有一个字符s2中没有出现过,然后就会多比较很多包含这个字符的情况。很容易想到,遇到这样的情况,直接把窗移动到这个字符后面。但是还是进行比较的时候又得重新遍历子串,很浪费时间。能不能遍历一次就能完成子串选取和比较的过程呢?如下:
滑动窗口【不是固定长度,且一直往右边移动的窗口】

class Solution {
    public boolean checkInclusion(String s1, String s2) {
        int[] dic = new int[26 + 'a'];
        for(int i = 0; i < s1.length(); i++) {
            dic[s1.charAt(i)]++;
        }
        int right = 0,  left = 0;
        while(right < s2.length()) {
            char cur = s2.charAt(right);
            dic[cur]--;
            //如果小于了0, 有两种情况,一是,这个字符,s1中根本没有,二是这个字符s1中有,但是之前已经减掉,这个区间多了这种字符
            while(dic[cur] < 0) {
                //总的来说就是区间内多了这个字符,那就往右边移动,把字符补回来,一直移到多的这个字符后面
                //例如 abc  bcba, 到第三个字符发现多了一个d 这就移动到最前面一个d后一位,因为在这之前一直都是多个d不可能是变位词
                dic[s2.charAt(left)]++; //窗口往右移动,把减去的字符补回来
                left++;
            }
            //如果能顺利的减到right - left + 1 和 s1长度相等,说明这个窗口和s1是变位词,因为中间没有多一个没有的词
            if(right - left + 1 == s1.length()){
                return true;
            }
            //不要忘记往右走
            right++;
        }
        return false;
    }
}

类似题目:剑指 Offer II 015. 字符串中的所有变位词

剑指 Offer II 016. 不含重复字符的最长子字符串

题目:给定一个字符串 s ,请你找出其中不含有重复字符的 最长连续子字符串 的长度。
0 <= s.length <= 5 * 104
s 由英文字母、数字、符号和空格组成


思路:滑动窗口,窗口内某字符数目等于2,将窗口左边收缩到这个字符前一次出现的后一位。
注意::数字 32–126 ASCII值分配给了能在键盘上找到的字符

class Solution {
    public int lengthOfLongestSubstring(String s) {
        //滑动窗口,窗口内某字符数目等于2,将窗口左边移到第一个字符处
        int ret = 0;
        int[] dic = new int[128];
        int l = 0, r = 0;
        while(r < s.length()) {
            dic[s.charAt(r)]++;
            while(dic[s.charAt(r)] == 2) {
                dic[s.charAt(l)]--;
                l++;
            }
            ret = Math.max(ret, r - l + 1);
            r++;
        }
        return ret;
    }
}

剑指 Offer II 017. 含有所有字符的最短字符串

题目:
给定两个字符串 s 和 t 。返回 s 中包含 t 的所有字符的最短子字符串。如果 s 中不存在符合条件的子字符串,则返回空字符串 “” 。
如果 s 中存在多个符合条件的子字符串,返回任意一个。


思路:
我们很容易用滑动窗口找到包含t的所有字符的子字符串,但是找到子字符串后怎么将它缩成最短呢?
由于窗口是向右滑动的,当右边到达满足条件会停止,停止后就只能从左边进行删减,删减到依然满足情况即为最短。
怎么判断满足情况,即包含所有字符?
先将t中所有字符进行统计,用数组dic统计字符及其个数,遍历s将对应字符数进行-1,
当多的字符出现时,会把dic[x] 减成负数,当所有之前有值的dic[x]都变成0了以后说明满足情况
找到第一个满足情况的以后,之后每次往后移动窗口右边界后,都试图对左边界进行缩小。

class Solution {
   public String minWindow(String s, String t) {
       int l = 0, r = 0;
       int[] dic = new int[128];//统计t中字符及其个数
       int count = 0; //统计 dic[] 中不为0的个数, 用于后面判断是否满足包含所有字符情况
       int retL = 0, retR = s.length() - 1;
       boolean find = false;
       for(int i = 0; i < t.length(); i++) {
           if(dic[t.charAt(i)]++ == 0) count++;
       }
       while(r < s.length()) {
            if(--dic[s.charAt(r)] == 0) { //之前有这个字符,且这个字符的数量减为0了
               count--;
            }
            if(count == 0) {字符串t中所有字符都减完了,开始从左边缩成最短
//如果左边这个字符是没用的,那么字典中这个字符位置一定是小于0的,可能是后面有多的,也可能就不需要这个字符。
//左边这个字符减掉以后,给dic回一个字符,
               find = true;
               while(dic[s.charAt(l)] < 0) { 
                   dic[s.charAt(l)]++;
                   l++;
               }
               //缩完了,看新的这个短不短
               if(retR - retL + 1 > r - l + 1) {
                   retL = l;
                   retR = r;
               }
            }
            //找到第一个合适的以后,之后每次往后移动一个,就试图从左边缩小
           r++;
       }
       return find?s.substring(retL, retR + 1):"";
   }
}
上一篇:408. Valid Word Abbreviation


下一篇:301. 删除无效的括号