算法学习之滑动窗口(java版)

算法学习之滑动窗口(java版)

文章目录

  • 算法学习之滑动窗口(java版)
    • 框架
    • 例题
      • 最小覆盖子串
      • 字符串排列
      • 找所有字母异位的词
      • 最长无重复子串
    • 总结

滑动窗口实际上是通过双指针实现的,[left,right]之间的范围就是窗口。通常用于解决字符串、数组相关的问题。比如最小子串等。

框架

public static String minWindow(String s, String t) {
     
    HashMap<Character, Integer> need = new HashMap<>();
    int t_len = t.length();
    for (int i = 0; i < t_len; i++) {
     
        need.put(t.charAt(i), need.getOrDefault(t.charAt(i), 0) + 1);
    }
    int left = 0, right = 0;
    int valid = 0;
    // 记录最小覆盖子串的起始索引及⻓度
    int start = 0, len = Integer.MAX_VALUE;
    while (right < s.length()) {
     
        // c 是将移入窗口的字符
        char c = s.charAt(right); // 右移窗口
        right++;
        // 进行窗口内数据的一系列更新
        ...
        // 判断左侧窗口是否要收缩
        while (...) {
     
            // d 是将移出窗口的字符
            char d = s.charAt(left);
            // 左移窗口
            left++;
            // 进行窗口内数据的一系列更新
        }
    }

    return ...;
}

关键是判断收缩以及扩展时的情况,如何向窗口中添加新元素,如何缩小窗口,在窗口移动的哪个阶段更新结果。

例题

最小覆盖子串

给你一个字符串S、一个字符串T,请在字符串S里面找出:包含T所有字母的最小子串
输入:S=“ADOBECODEBANC”, T=“ABC”
输出:“BANC”

思路:

  1. 维护好need和window两个Map,用于记录窗口内的元素,每次窗口扩展后,比较need和window就可以知道是否含有相同字母。

  2. 设置双指针,left和right。

  3. right一直增大,直到窗口[left,right)满足要求包含字符串T为止。

  4. left一直增大,直到窗口中的字符串不再满足条件。每次增加left都要更新一轮结果

  5. 重复3、4两步,直到right到尽头

可以理解为步骤2在找可行解,步骤3在优化这个解。

public static String minWindow(String s, String t) {
     
    HashMap<Character, Integer> need = new HashMap<>();
    HashMap<Character, Integer> window = new HashMap<>();
    int t_len = t.length();
    for (int i = 0; i < t_len; i++) {
     
        need.put(t.charAt(i), need.getOrDefault(t.charAt(i), 0) + 1);
    }
    int left = 0, right = 0;
    int valid = 0;
    // 记录最小覆盖子串的起始索引及⻓度
    int start = 0, len = Integer.MAX_VALUE;
    while (right < s.length()) {
     
        // c 是将移入窗口的字符
        char c = s.charAt(right); // 右移窗口
        right++;
        // 进行窗口内数据的一系列更新
        if (need.containsKey(c)) {
     
            int tmp = window.getOrDefault(c, 0);
            window.put(c, ++tmp);
            if (window.get(c) == need.get(c))
                valid++;
        }
        // 判断左侧窗口是否要收缩
        while (valid == need.size()) {
     
            // 在这里更新最小覆盖子串
            // len用于记录当前最佳的长度
            if (right - left < len) {
     
                start = left;
                len = right - left;
            }
            // d 是将移出窗口的字符
            char d = s.charAt(left);
            // 左移窗口
            left++;
            // 进行窗口内数据的一系列更新
            if (need.containsKey(d)) {
     
                if (window.get(d) == need.get(d))
                    valid--;
                int tmp = window.get(d);
                window.put(d, --tmp);
            }
        }
    }
// 返回最小覆盖子串
    return len == Integer.MAX_VALUE ? "" : s.substring(start, start + len);
}

字符串排列

给定两个字符串s1和s2,写一个函数来判断s2是否包含s1的排列。
输入:s1=“ab”, s2=“eidbaooo”
输出:True
因为"ba"在s2中

思路:
思路同上一道题,但是这道题中需要一直维持窗口大小为字符串s1的长度,这也是扩展和收缩的条件。

public static boolean checkInclusion(String s, String t) {
     
    HashMap<Character, Integer> need = new HashMap<>();
    HashMap<Character, Integer> window = new HashMap<>();
    int t_len = t.length();
    for (int i = 0; i < t_len; i++) {
     
        need.put(t.charAt(i), need.getOrDefault(t.charAt(i), 0) + 1);
    }
    int left = 0, right = 0;
    int valid = 0;
    // 记录最小覆盖子串的起始索引及⻓度
    int start = 0, len = Integer.MAX_VALUE;
    while (right < s.length()) {
     
        // c 是将移入窗口的字符
        char c = s.charAt(right); // 右移窗口
        right++;
        // 进行窗口内数据的一系列更新
        if (need.containsKey(c)) {
     
            int tmp = window.getOrDefault(c, 0);
            window.put(c, ++tmp);
            if (window.get(c) == need.get(c))
                valid++;
        }
        // 判断左侧窗口是否要收缩
        while (right-left>=t_len) {
     
            // 在这里更新最小覆盖子串
            if (valid==need.size()) {
     
                return true;
            }
            // d 是将移出窗口的字符
            char d = s.charAt(left);
            // 左移窗口
            left++;
            // 进行窗口内数据的一系列更新
            if (need.containsKey(d)) {
     
                if (window.get(d) == need.get(d))
                    valid--;
                int tmp = window.get(d);
                window.put(d, --tmp);
            }
        }
    }
// 返回最小覆盖子串
    return false;
}

此题的解进在判断左侧窗口是否要收缩的地方有了较大的改变,其他地方基本一致,可见框架是很有用的。

找所有字母异位的词

给定一个字符串s和一个非空字符串p,找到s中所有是p的字母异位词的子串,返回这些子串的起始索引
输入:s:“cbaebabacd”, p:“abc”
输出:[0,6]

思路:
这一题的思路与上一题是一模一样的,也是要维护好固定大小的窗口,需要做的改动就是需要判断一下必须是异位词,不能和字符串p一模一样。

public static ArrayList<Integer> checkInclusion(String s, String t) {
     
    HashMap<Character, Integer> need = new HashMap<>();
    HashMap<Character, Integer> window = new HashMap<>();
    ArrayList<Integer> res = new ArrayList<>();
    int t_len = t.length();
    for (int i = 0; i < t_len; i++) {
     
        need.put(t.charAt(i), need.getOrDefault(t.charAt(i), 0) + 1);
    }
    int left = 0, right = 0;
    int valid = 0;
    // 记录最小覆盖子串的起始索引及⻓度
    int start = 0, len = Integer.MAX_VALUE;
    while (right < s.length()) {
     
        // c 是将移入窗口的字符
        char c = s.charAt(right); // 右移窗口
        right++;
        // 进行窗口内数据的一系列更新
        if (need.containsKey(c)) {
     
            int tmp = window.getOrDefault(c, 0);
            window.put(c, ++tmp);
            if (window.get(c) == need.get(c))
                valid++;
        }
        // 判断左侧窗口是否要收缩
        while (right-left>=t_len) {
     
            // 在这里更新最小覆盖子串
            if (valid==need.size()&&!s.substring(left,right).equals(t)) {
     
                res.add(left);
            }
            // d 是将移出窗口的字符
            char d = s.charAt(left);
            // 左移窗口
            left++;
            // 进行窗口内数据的一系列更新
            if (need.containsKey(d)) {
     
                if (window.get(d) == need.get(d))
                    valid--;
                int tmp = window.get(d);
                window.put(d, --tmp);
            }
        }
    }
// 返回最小覆盖子串
    return res;
}

最长无重复子串

给定一个字符串,请你找出其中不含有重复字符的最长子串的长度
输入:“abcabcbb”
输出:3

思路:
这一题与第一题的思路一样,窗口大小不固定,一直扩展直到不符合条件出现重复字符为止,然后开始收缩,直到又满足条件了。反复重复,记得更新最新长度。

public static int lengthOfLongestSubstring(String s) {
     
    HashMap<Character, Integer> window = new HashMap<>();
    int res = 0;
    int left = 0, right = 0;
    while (right < s.length()) {
     
        // c 是将移入窗口的字符
        char c = s.charAt(right); // 右移窗口
        int count = window.getOrDefault(c, 0);
      // 进行窗口内数据的一系列更新
        window.put(c, ++count);
        right++;
        // 判断左侧窗口是否要收缩
        while (window.getOrDefault(c, 0)>1) {
     
            count = window.get(s.charAt(left));
          // 将字符移除
            window.put(s.charAt(left), --count);
          // 左移窗口
            left++;
        }
        if(res<right-left){
     
            res= right-left;
        }
    }
// 返回最小覆盖子串
    return res;
}

总结

滑动窗口问题的关键:

  • 窗口大小的维护
  • 收缩与扩展的情况判断
  • 更新状态。

在框架中需要灵活更改的几个地方也对应了提到的几个关键点。

申明:本博文是看了labuladong的算法小抄之后个人的理解以及总结。

你可能感兴趣的:(算法学习,字符串,java,算法)