力扣438题——关于滑动窗口的一点思考

344 阅读2分钟

1. 题目

给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。

异位词 指由相同字母重排列形成的字符串(包括相同的字符串)

来源:力扣(LeetCode) 链接:leetcode-cn.com/problems/fi…

2. 示例

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

输出: [0,6]

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

3. 题解

3.1 暴力方法

由于题目所出现的字符都是小写字母,而题目中的异位词指的是字符相同但是顺序不同的字符串,这时我们可以想到使用一个长度为26(26个小写字母)的数组作为字符串的key,这样一来,只要key相同的字符串它们就互为异位词

public List<Integer> findAnagrams(String s, String p) {
    int sLen = s.length();
    int pLen = p.length();
    String pKey = getKey(p);
    List<Integer> res = new ArrayList<>();
    for (int i = 0; i <= sLen - pLen; i++) {
        String sub = s.substring(i, i + pLen);
        // O(pLen)
        String key = getKey(sub);
        // O(pLen)
        if (key.equals(pKey)) {
            res.add(i);
        }
    }

    return res;
}
// O(pLen)
private String getKey(String str) {
    int[] count = new int[26];
    for (int i = 0; i < str.length(); i++) {
        count[str.charAt(i) - 'a']++;
    }

    StringBuilder builder = new StringBuilder();
    for (int i = 0; i < count.length; i++) {
        if (count[i] != 0) {
            builder.append((char) (i + 'a')).append(count[i]);
        }
    }

    return builder.toString();
}

3.2 滑动窗口

上述将字符串生成的数组转换成key(也是字符串),再进行比较,效率十分低下。因此,我们可以考虑使用滑动窗口。

这样一来,我们只需要维护窗口内出现的字符是字符串p中的字符并且各个字符的数量也需要和p中一致。

但是,如何维护窗口内字符和字符数量同时一致呢?

这时,我们可以沿用上述暴力方法中的长度为26的数组来维护我们当前遍历到的窗口内的字符串。也使用索引值表示字符,数组索引处的值来表示该字符出现的次数。初始化时,根据字符串p生成数组原始值。之后开始遍历字符串s,当字符进入窗口时则数组对应位置值-1,当弹出窗口时则数组对应位置值 +1(还原)

1. 第一版的滑动窗口

// 滑动窗口
public List<Integer> findAnagrams(String s, String p) {
    int sLen = s.length();
    int pLen = p.length();
    List<Integer> res = new ArrayList<>();
    int[] pCounts = new int[26];
    for (int i = 0; i < pLen; i++) {
        pCounts[p.charAt(i) - 'a']++;
    }

    int left = 0;
    int right = 0;
    while (right < sLen) {
        // 加入窗口的字符的索引
        int inWin = s.charAt(right) - 'a';
        // 这里直接将遍历到的(右边界)字符加入到窗口中
        pCounts[inWin]--;
        while (pCounts[inWin] < 0) {
            int outWin = s.charAt(left++) - 'a';
            pCounts[outWin]++;
        }

        if (right - left + 1 == pLen) {
            res.add(left);
        }

        right++;
    }

    return res;

}

2. 第二版的滑动窗口

public List<Integer> findAnagrams(String s, String p) {
    int sLen = s.length();
    int pLen = p.length();
    List<Integer> res = new ArrayList<>();
    int[] pCounts = new int[26];
    for (int i = 0; i < pLen; i++) {
        pCounts[p.charAt(i) - 'a']++;
    }

    int left = 0;
    int right = 0;
    while (right < sLen) {
        // 准备加入窗口的字符的索引
        int inWin = s.charAt(right) - 'a';
        // 这里先试探性的查看加入的字符是否影响窗口内字符的正确性
        while (pCounts[inWin] - 1 < 0) {
            int outWin = s.charAt(left++) - 'a';
            pCounts[outWin]++;
        }
        // 在处理完成后,再加入对应的字符
        pCounts[inWin]--;

        if (right - left + 1 == pLen) {
            res.add(left);
        }

        right++;
    }

    return res;
}

上述两种代码其主体思路是一样的,唯一不同点在于当前遍历到的入窗字符(即右边界)是先直接加入窗口进行处理还是处理后再加入

使用滑动窗口有时还会用到哈希表(这里主要指的是Set),如求一个给定字符串中最长的字符全部相异的长度。这种情况下,在遍历右边界时就不能先直接加入窗口后处理,而是试探性的检查当前入窗字符的正确性并处理后再加入,这是因为哈希表中的每个元素具有唯一性