滑动窗口问题

算法☞滑动窗口专题

无重复字符的最长子串

题意:

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

1.1 Java解法

解题思路:

由于需要找的是不含有重复字符的子串,同时要求子串最长,即求区间极值问题,于是我们不难想到滑动窗口问题,我们可以设定区间左边界的下标为left,右边界的下标为right,一开始left = right = 0,然后移动right,每次移动right的时候,都检查一下right指向的值是否在之前出现过,而这可以通过java中的HashMap来实现,如果之前出现过,那么left应该等于当前的left和Hashmap中这个字符的位置+1的最大值,至于为什么是这两个的最大值,我们可以举个栗子:比如字符串为:abba,如果left直接变为Hashmap中这个字符的位置+1。

此时代码如下:
class Solution {
	public int lengthOfLongestSubstring(String s) {
		int sLen = s.length();
        HashMap<Character, Integer> hash = new HashMap<Character, Integer>();
        int left = 0, maxLen = 0;
        for(int i = 0; i < sLen; i++){
            if(hash.containsKey(s.charAt(i)))
                left = hash.get(s.charAt(i))+1;
            maxLen = Math.max(maxLen, i - left + 1);
            hash.put(s.charAt(i), i);
        }
        return maxLen;
	}
}
则相关参数的变化如下:

| | | | | |
|-|-|-|-|-|-|
| left| 0| 0|2|1|
| right|0 |1|2|3|
| max|1 |2 |2|3|

我们可以看到,上面right移动到最后一个a时,left变成了Hashmap中a出现的位置+1=0+1=1,也就是left出现了往回走的现象,事实上,这个时候的区间为[bba],此时还是含有重复字符,而最大值已经更新成了3,造成错误,要避免这个问题,我们需要让left只能往右走,那么left必然应该等于本身与HashMap中这个字符的位置+1两者的最大值。

此时代码如下:
class Solution {
	public int lengthOfLongestSubstring(String s) {
		int sLen = s.length();
        HashMap<Character, Integer> hash = new HashMap<Character, Integer>();
        int left = 0, maxLen = 0;
        for(int right = 0; right < sLen; right++){
            if(hash.containsKey(s.charAt(right)))
                left = Math.max(left, hash.get(s.charAt(right))+1);
            maxLen = Math.max(maxLen, right - left + 1);
            hash.put(s.charAt(right), right);
        }
        return maxLen;
	}
}
则相关参数的变化如下:

| | | | | |
|-|-|-|-|-|-|
| left| 0| 0|2|2|
| right|0 |1|2|3|
| max|1 |2 |2|2|

最后,提醒一下,记得一定要把当前right扫描的点加入到HashMap中!HashMap<Character,Integer> map 加入键值对(right,s.charAt(right))的用法是map.put(s.charAt(right), right),注意先后顺序!

1.2 C++的解法

解题思路:

由于需要找的是不含有重复字符的子串,同时要求子串最长,我们可以设定一个vector容器,用来存放当前枚举的最长无重复字符的子串,如果枚举到一个s中的字符ch,且s[i]不在vec中,则只需将其加入vec中即可,而如果在vec中已经有ch,则我们需要首先将其加入vec中,然后记录一下此时的vec容器中字符个数(这里记得减1,因为刚加入了一个重复的字符,需要将它先减掉),同时更新咱们的最长无重复字符的子串的长度,记录完之后,对vec进行删除操作,需要删除第一个ch以及它之前的所有字符。最后,在我们遍历整个s之后,我们还需要计算vec中字符个数,也就是最后一次的无重复字符的子串的长度,同样地,这个时候我们还是需要更新max,最后返回最大长度max即可。

代码如下:

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int len = 0;
        int max = 0;
        vector<int> vec;//无重复字符的容器
        for(char ch:s){
            int t = -1;
            for(int i = 0; i < vec.size(); i++){
                if(ch == vec[i]){
                    t = i;  //得到第一个重复的字符的位置
                    break;
                }
            }
            vec.push_back(ch);
            
            if(t != -1){    //在当前的最长子串中存在重复字符
                
                len = vec.size() - 1;
                max = max > len ? max : len;
                for(int i = 0; i < vec.size(); i++){
                    if(vec[i] != ch) vec.erase(vec.begin()+i);
                    else{
                        vec.erase(vec.begin()+i);
                        break;
                    }
                    i--;
                }
            }
        }
        len = vec.size();
        max = max > len ? max : len;

        return max;
    }
};

最小覆盖子串

题意

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。

2.1 java解法

解题思路:

由于是求解区间极值问题,我们可以优先考虑滑动窗口的解法,定义所求的最小区间左右端点的下标为left和right,首先让右端点right移动,如果当right移动到某个时刻,区间【left, right】刚好包含了t中所有字符,此时向右移动left,当发现left在移动某一步之后,刚好不能包含t中所有字符时,需要记录一下此时的位置,然后用begin记录当前left位置,同时更新最小长度,而当我们遍历完整个字符串s之后,我们需要判断minLen是不是与之前我们设定的不可能达到的长度相等,如果相等,说明,直到遍历完整个字符串s,我们都无法找到一个能包含字符串t中所有字符的子串,因此返回“”,否则,我们只需返回s.substring(begin, begin+ minLen)即可。

代码:
class Solution {
    public String minWindow(String s, String t) {
        int sLen = s.length(), tLen = t.length();
        if(sLen == 0 || tLen == 0 || sLen < tLen) return "";
        int distance, right, left, begin, minLen = sLen+1;
        distance = right = left = begin = 0;
        int[] tFreq = new int[128];
        int[] winFreq = new int[128];
        char[] sCharArray = s.toCharArray();
        char[] tCharArray = t.toCharArray();
        for(char ch : tCharArray){
            tFreq[ch]++;
        }
        
        while(right < sLen){
            if(tFreq[sCharArray[right]] == 0){
                right++;
                continue;
            }
            if(winFreq[sCharArray[right]] < tFreq[sCharArray[right]]){
                distance++;
            }
            winFreq[sCharArray[right]]++;
            right++;
           
            while(distance == tLen){
                if(minLen > right - left){
                    minLen = right - left;
                    begin = left;
                }
            if(tFreq[sCharArray[left]] == 0){
                left++;
                continue;
            }
            if(winFreq[sCharArray[left]] == tFreq[sCharArray[left]]){
                distance--;
            }
            winFreq[sCharArray[left]]--;
            left++;
            
            }
        }
        if(minLen == sLen + 1) return "";
        return s.substring(begin, begin + minLen);
    }
}

在这里插入图片描述

2.2 C++解法

代码:
class Solution {
public:
    string minWindow(string s, string t) {
        int slen = s.length(), tlen = t.length();
        if(slen == 0 || tlen == 0 || slen < tlen) return "";
        int left, right, minlen, begin,distance;
        left = right = distance = begin = 0;
        minlen = slen+1;
        int tFreq[128]={0}, winFreq[128] = {0};
        for(char c : t){
            tFreq[c]++;
        } 
        while(right < slen){
            if(tFreq[s[right]] == 0){
                right++;
                continue;
            }
            if(tFreq[s[right]] > winFreq[s[right]]) distance++;
            winFreq[s[right]]++;
            right++;
            while(distance == tlen){
                if(minlen > right - left){
                    minlen = right - left;
                    begin = left;
                }
                if(tFreq[s[left]] == 0){
                    left++;
                    continue;
                }
                if(winFreq[s[left]] == tFreq[s[left]]) distance--;
                winFreq[s[left]]--;
                left++;
            }
        }
        if(minlen == slen+1) return "";
        string ans = "";
       for(int i = begin; i < begin + minlen; i++) ans += s[i];
        return ans;
    }
};

在这里插入图片描述

总结

这个解法中,由于t中可能出现重复字符,出现这种情况时,我们需要计算s所包含的所有t中的字符的次数是否比t中的大,也就是说t中的字符在s中所求的区间内要求一个都不能少,于是我们可以用一个distance来维护t中每个元素出现的频数,只要当s中某个区间的distance与字符串t的长度相等,这个区间就一定是包含了t中所有的字符。一样的代码,使用java的内存消耗是相当大的,主要原因是使用java时,我们需要将string用toCharArray转为char数组。

长度最小的子数组

题意

给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。

解题思路

首先,一看到区间求和,立马想到前缀和,后面又是求最小区间,即区间极值问题,于是我们毫不犹豫地想到了滑动窗口解法,于是我们可以定义左指针left和右指针right,我们首先移动right,直到区间和满足刚好大于target的条件,此时,我们更新区间最短长度并移动left,直到区间和刚好不满足大于target的条件,我们不断地对区间进行遍历,直到right指针遍历整个nums数组,此时得到的最小区间长度即为答案。

代码
class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int sum[100005];
        int n = nums.size();
        sum[1] = nums[0];
        for(int i = 1; i < n; i++){
            sum[i+1] = sum[i]+nums[i];
        }
        if(sum[n] < target || n == 0) return 0;
        int right, left, minLen=n+1;
        right = left = 0;

        while(right < n){
            right++;
            while(sum[right]- sum[left]>= target){
                if(minLen > right - left) minLen = right - left;
                left++;
            }

        }
        if(minLen == n+1) return 0;
        return minLen;
    }
};
总结

需要特别注意的是,最小区间长度为0的情况,首先在一开始,如果整个区间和都小于target或者给定数组长度为空,都返回0。至于minLen为什么更新为right-left,而不是right-left+1,是因为我们在之前就已经将right多右移了一位,即做了right++操作,如果我们用的是for(;;right++),minLen同样应该更新为right-left,原因同上。

替换后的最长重复字符

题意

给你一个仅由大写英文字母组成的字符串,你可以将任意位置上的字符替换成另外的字符,总共可最多替换 k 次。在执行上述操作后,找到包含重复字母的最长子串的长度。

解题思路

首先,我们需要明确我们求的子串中只能包含一个字母,其次,由于需要求最长区间问题,即区间极值问题,因此,我们不得不首先想到滑动窗口的解法,首先我们定义左右指针left和right,下面我们首先移动right指针,直到left和right指针形成的区间刚好不满足题意。但是这里一个难点就是我们需要分析什么叫不满足题意。首先,我们直到给定的替换次数k是固定的,我们一般不会让k++或者k–,即不会改变k值,那我们该如何利用k呢,仔细思考后,我们不难发现,其实我们从right扫描开始,就需要确定好当前[left,right)内出现次数最大的字符,因为我们要求的就是最长的且只含有一个字母的子串,于是我们需要定义一个频数数组winFreq以及一个最大频数字符maxcount,right指针每扫描一个新的字符,这个频数数组就需要更新,即让winFreq[ch-‘A’]++,然后看新的字符是不是当前子串中频数最大的字符,如果是,就将最大频数maxcount更新,得到了最大频数之后,我们想一下,要想替换k次得到最长的只含一个字符的子串,必须满足maxcount + k >= 当前区间长度,如果不满足这样的条件,我们就称之为上面的:“不满足题意”,此时,我们就需要更新left,这里只需要将left右移一格即可,因为left同时往右移动几格会缩短区间长度。left右移一格之前,需要将winFreq[left]减掉,之后,right就可以继续往右移,可以看下一个字符是不是最大频数字符,即是否满足题意,如果是就更新maxcount,继续地循环操作,那我们什么时候得到最长的只含有一个字母的子串呢?其实我们每次讲移动right之后,不管left是不是移动,我们在最后直接更新此时的区间即可。

Java代码

class Solution {
    public int characterReplacement(String s, int k) {
        int len = s.length();
        if(len < 2) return len;
        int left, right, maxCount, res;
        left = right = maxCount = res = 0;
        int[] winFreq = new int[26];
        char[] sCharArray = s.toCharArray();
        while(right < len){
            winFreq[sCharArray[right] - 'A']++;
            maxCount = Math.max(maxCount, winFreq[sCharArray[right]-'A']);
            right++;
            if(maxCount + k < right - left){
                winFreq[sCharArray[left]-'A']--;
                left++;
            }
            res = Math.max(res, right - left);
        }
        return res;
    }
}

C++代码

class Solution {
public:
    int characterReplacement(string s, int k) {
        int len = s.length();
        if(len < 2){
            return len;
        }
        int left, right, maxCount,winFreq[26]={0}, res;
        left = right = maxCount = res = 0;
        while(right < len){
            winFreq[s[right] - 'A']++;
            maxCount = max(maxCount, winFreq[s[right] - 'A']);
            right++;
            if(maxCount + k < right - left){
                winFreq[s[left]-'A']--;
                left++;
            }
        }
        return right-left;
    }
};

总结:

在找到最长的目标子串之后,right指针每往前走一步,left指针都会跟上,因此,其实right-left在我们找到最长目标子串之后就不会再改变了,于是我们最后直接返回right-left也是可以的。

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

题意:

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

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

解题思路:

5.1 JAVA题解
解题思路

由于涉及到求解区间类问题,我们优先考虑滑动窗口解法,题目要求我们统计给定字符串s中所含的所有字符串p的重排字符串起始索引,于是我们先定义一个数组c1统计给定的字符串p中所有字符即其出现的个数。然后再定义另一个滑动窗口数组c2,统计滑动窗口内所有字符即其出现的个数,当c1和c2对应字符的个数相等时,此时的滑动窗口即为所求的p的异位词,将其左端点加入结果中即可。

代码
class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        List<Integer> ans = new ArrayList<>();
        int n = s.length(), m = p.length();
        int[] c1 = new int[26], c2 = new int[26];
        for (int i = 0; i < m; i++) c2[p.charAt(i) - 'a']++;
        for (int l = 0, r = 0; r < n; r++) {
            c1[s.charAt(r) - 'a']++;
            if (r - l + 1 > m) c1[s.charAt(l++) - 'a']--;
            if (check(c1, c2)) ans.add(l);
        }
        return ans;
    }
    boolean check(int[] c1, int[] c2) {
        for (int i = 0; i < 26; i++) {
            if (c1[i] != c2[i]) return false;
        }
        return true;
    }
}

复杂度分析

时间复杂度:令 s 和 p 的长度分别为 n 和 m,C = 26 为字符集大小。统计 p 词频(构建 c2 数组)的复杂度为 O(m);使用双指针检查 s 串的复杂度为 O(C * n)。整体复杂度为 O(C*n + m)
空间复杂度:O©

Check函数的优化

上述解法中每次对滑动窗口的检查都不可避免需要检查两个词频数组,复杂度为 O©。

事实上,我们只关心两个数组是否完全一致,因而我们能够只维护一个词频数组 cnt来实现。

起始处理 p 串时,只对 cnt进行词频字符自增操作。当处理 s 的滑动窗口子串时,尝试对 cnt中的词频进行「抵消/恢复」操作:

当滑动窗口的右端点右移时(增加字符),对 cnt执行右端点字符的「抵消」操作;
当滑动窗口的左端点右移时(减少字符),对 cnt 执行左端点字符的「恢复」操作。
同时,使用变量 a统计 p 中不同字符的数量,使用变量 b 统计滑动窗口(子串)内有多少个字符词频与 p 相等。

当滑动窗口移动( 执行「抵消/恢复」)时,如果「抵消」后该字符词频为 0,说明本次右端点右移,多产生了一位词频相同的字符;如果「恢复」后该字符词频数量为 1,说明少了一个为词频相同的字符。当且仅当 a = b时,我们找到了一个新的异位组。

优化后的代码:

class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        List<Integer> ans = new ArrayList<>();
        int n = s.length(), m = p.length();
        int[] cnt = new int[26];
        for (int i = 0; i < m; i++) cnt[p.charAt(i) - 'a']++;
        int a = 0;
        for (int i = 0; i < 26; i++) if (cnt[i] != 0) a++;
        for (int l = 0, r = 0, b = 0; r < n; r++) {
            // 往窗口增加字符,进行词频的抵消操作,如果抵消后词频为 0,说明有一个新的字符词频与 p 完全相等
            if (--cnt[s.charAt(r) - 'a'] == 0) b++; 
            // 若窗口长度超过规定,将窗口左端点右移,执行词频恢复操作,如果恢复后词频为 1(恢复前为 0),说明少了一个词频与 p 完全性相等的字符
            if (r - l + 1 > m && ++cnt[s.charAt(l++) - 'a'] == 1) b--;
            if (b == a) ans.add(l);
        }
        return ans;
    }
}


优化后的复杂度分析

时间复杂度:令 s 和 p 的长度分别为 n 和 m,C = 26为字符集大小。构造 cnt的复杂度为 O(m),统计 p 中不同的字符数量为 O©,对 s 进行滑动窗口扫描得出答案的复杂度为 O(n)。整体复杂度为 O(m + C + n)
空间复杂度:O©

在这里插入图片描述

5.2 C++解法

思路同上

代码:

class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        int slen = s.length(), plen = p.length();
        vector<int>ans;
        int pFreq[26] = {0}, a = 0, b = 0;
        for(auto ch : p) pFreq[ch - 'a']++;
        for(int i = 0 ; i < 26; i++) if(pFreq[i] != 0) a++;
        for(int l = 0, r = 0; r < slen; r++){
            if(--pFreq[s[r] - 'a'] == 0) b++;
            if(r - l + 1 > plen && ++pFreq[s[l++] - 'a'] == 1) b--;
            if(a == b) ans.push_back(l);
        }
        return ans;
    }
};

在这里插入图片描述
另外,如果实在想不出办法了,可以采用暴力法。

暴力法代码如下:

class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        int slen = s.length(), plen = p.length();
        vector<int>ans;
        if(slen < plen || slen == 0 || plen == 0) return ans;
        int left, right, distance;
        int pFreq[128]={0};
        for(auto ch : p) pFreq[ch]++;
        for(int i = 0; i+plen<= slen; i++){
            int winFreq[128]={0}, ok = 0, t; 
            for(int j = i; j < i + plen; j++){
                winFreq[s[j]]++;
            }
            for(int j = i; j < i + plen; j++){
                if(winFreq[s[j]] != pFreq[s[j]]){
                    ok = 1;
                    break;
                }
            }
            if(ok == 0){
                ans.push_back(i);

            } 
        }
        return ans;
    }
};

总结

这道题其实可以使用暴力解法,虽然时间有点慢(700ms),但题目给定数据还是比较小,因此,实在想不出办法时,暴力也是最容易想到的方法。如果能想到咱们的这种滑动窗口解法,其实还是蛮快的,而且代码非常地简短,但是需要好好思考,尤其是check函数的优化,需要两个变量来统计抵消和恢复的值,并与字符串p中出现的字符个数和频数进行比较,定义a来存放p中字符种数,b来统计通过滑动窗口抵消的p中字符的个数,当a==b时,我们的滑动窗口中就得到了一个异位组,将左端点加入结果ans中即可。

字符串的排列

题意:

给你两个字符串 s1 和 s2 ,写一个函数来判断 s2 是否包含 s1 的排列。如果是,返回 true ;否则,返回 false 。
换句话说,s1 的排列之一是 s2 的 子串 。

解题思路

本题和前面一题的思路基本一致,我们可以使用上面check优化后的版本,首先,定义一个cnt数组来存放s1中每一个字符出现的次数,然后一定要记得扫描一下所有26个字符,看cnt数组中有没有该字符,如果有该字符,则a++,其实就是统计cnt数组中的字符种数,也就是s1中字符种数,然后循环r,此时记得初始化三个数l、r和b为0,然后r指针每扫描一个字符后,先判断当前扫描的字符是否已经被抵消为0,如果是,则b++,即滑动窗口中又有一个字符的次数和s1中该字符出现的次数相等,然后判断r-l+1是否大于n,如果是就需要移动l指针,同时,还需要判断移动之后滑动窗口中的该字符种数是否恢复为了1,如果是,说明当前将l指针往后移的操作使得该字符在滑动窗口中出现的次数刚和该字符在s1中出现次数相等的情况,变成了该字符在s1中出现次数>该字符在滑动窗口中出现的次数,即移除l指针指向的字符之后,使得cnt数组中该字符的次数变为1,即滑动窗口中还差一个该字符,才能和s1中该字符完全匹配,因此,此时需要将b–,说明滑动窗口中和s1中出现次数相等的字符又少了一个。而最后,当a=b时,说明滑动窗口中所有字符出现的次数都和s1中所有字符出现的次数完全相等,当然顺序可能不一样,而这,就是我们的答案。一旦出现了a=b,说明s2中存在s1的排列,否则,在最后返回false。

代码

class Solution {
public:
    bool checkInclusion(string s1, string s2) {
        int n = s1.length(), m = s2.length();
        if(n == 0 || m == 0 || n > m) return false;
        int cnt[26]={0},a=0;
        for(auto ch : s1) cnt[ch-'a']++;
        for(int i = 0; i < 26; i++) if(cnt[i] != 0) a++;
        for(int l = 0, r = 0, b = 0; r < m; r++){
            if(--cnt[s2[r]-'a'] == 0) b++;
            if(r - l + 1 > n && ++cnt[s2[l++]-'a'] == 1) b--;
            if(a == b) return true;
        }
        return false;
    }
};

串联所有单词的子串

题意

给定一个字符串 s 和一些 长度相同 的单词 words 。找出 s 中恰好可以由 words 中所有单词串联形成的子串的起始位置。
注意子串要与 words 中的单词完全匹配,中间不能有其他字符 ,但不需要考虑 words 中单词串联的顺序。

解题思路

思路一:首先讲一下最基本的思路,也是最容易想到的思路,就是先用map记录一下words中每个单词出现的次数,然后从给定字符串中枚举目标字符串,这里我们是已经知道了目标字符串的长度的,直接枚举定长的所有字符串,然后截取每个单词,检查其是否在words的map中出现过,如果没有出现过,则直接跳出循环,否则,记录到新的map-cur中,然后比较map和cur是否相同,如果相同,说明满足题意,将对应的左右端点加入ans中,否则返回空就OK了。

代码1

class Solution {
    public List<Integer> findSubstring(String s, String[] words) {
        List<Integer> ans = new ArrayList<>();
        if(words.length == 0) return ans;
        int n = s.length(), m = words.length, w = words[0].length();
        Map<String, Integer> map = new HashMap<>();
        for(String word : words){
            map.put(word, map.getOrDefault(word, 0)+1);
        }
        out:for (int i = 0; i + m * w <= n; i++){
            Map<String, Integer> cur = new HashMap<>();
            String sub = s.substring(i, i + m * w);
            for(int j = 0; j < sub.length(); j += w){
                String item = sub.substring(j, j + w);
                if (!map.containsKey(item)) continue out;
                cur.put(item, cur.getOrDefault(item, 0)+1);
            }
            if(cmp(cur, map)) ans.add(i);
        }
        return ans;
    }

    boolean cmp(Map<String, Integer> m1, Map<String, Integer> m2){
        if(m1.size() != m2.size()) return false;
        for(String k1 : m1.keySet()){
            if(!m2.containsKey(k1) || !m1.get(k1).equals(m2.get(k1))) return false;
        }
        for (String k2 : m2.keySet()){
            if(!m1.containsKey(k2) || !m1.get(k2).equals(m2.get(k2))) return false;
        }
        return true;
    }
}

复杂度分析:
时间复杂度:将 words 中的单词存入哈希表,复杂度为 O(m);然后第一层循环枚举 s 中的每个字符作为起点,复杂度为 O(n);在循环中将 sub 划分为 m 个单词进行统计,枚举了 m - 1 个下标,复杂度为 O(m);每个字符串的长度为 w。整体复杂度为 O(n * m * w)
空间复杂度:O(m * w)

思路二:滑动窗口 & 哈希表
事实上,我们可以优化这个枚举起点的过程。
我们可以将起点根据当前下标与单词长度的取余结果 进行分类,这样我们就不用频繁的建立新的哈希表和进行单词统计。

代码二:

class Solution {
    public List<Integer> findSubstring(String s, String[] words) {
        List<Integer> ans = new ArrayList<>();
        if (words.length == 0) return ans;

        int n = s.length(), m = words.length, w = words[0].length();

        // 统计 words 中「每个目标单词」的出现次数
        Map<String, Integer> map = new HashMap<>();
        for (String word : words) {
            map.put(word, map.getOrDefault(word, 0) + 1);
        }

        for (int i = 0; i < w; i++) {
            // 构建一个当前子串对应 map,统计当前子串中「每个目标单词」的出现次数
            Map<String, Integer> curMap = new HashMap<>();
            // 滑动窗口的大小固定是 m * w
            // 每次将下一个单词添加进 cur,上一个单词移出 cur
            for (int j = i; j + w <= n; j += w) {   
                String cur = s.substring(j, j + w);
                if (j >= i + (m * w)) {
                    int idx = j - m * w;
                    String prev = s.substring(idx, idx + w);
                    if (curMap.get(prev) == 1) {
                        curMap.remove(prev);
                    } else {
                        curMap.put(prev, curMap.get(prev) - 1);
                    }
                }
                curMap.put(cur, curMap.getOrDefault(cur, 0) + 1);
                // 如果当前子串对应 map 和 words 中对应的 map 相同,说明当前子串包含了「所有的目标单词」,将起始下标假如结果集
                if (map.containsKey(cur) && curMap.get(cur).equals(map.get(cur)) && cmp(map, curMap)) {
                    ans.add(j - (m - 1) * w);
                }
            }
        }

        return ans;
    }
    // 比较两个 map 是否相同
    boolean cmp(Map<String, Integer> m1, Map<String, Integer> m2) {
        if (m1.size() != m2.size()) return false;
        for (String k1 : m1.keySet()) {
            if (!m2.containsKey(k1) || !m1.get(k1).equals(m2.get(k1))) return false;
        }
        for (String k2 : m2.keySet()) {
            if (!m1.containsKey(k2) || !m1.get(k2).equals(m2.get(k2))) return false;
        }
        return true;
    }
}


复杂度分析:
时间复杂度:将 words 中的单词存入哈希表,复杂度为 O(m);然后枚举了取余的结果,复杂度为 O(w);每次循环最多处理 n 长度的字符串,复杂度为 O(n)。整体复杂度为 O(m + w * n)
空间复杂度:O(m * w)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

追梦_赤子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值