【算法】滑动窗口




前言


同向指针算法是一种使用两个指针,它们同时向同一个方向移动,以解决问题的算法技巧。这两个指针可以指向数组或字符串中的不同位置,根据问题的要求,它们可以以不同的速度移动。期间会产生一段区间,因此我们可以比喻成一个窗口,而我们可以在这个窗口内对数据进行一些我们想要的处理,由于这个区间一般都会向后移动,所以我们也可以称之为滑动窗口

例如,在数组中查找目标元素时,可以使用一个指针从数组的起始位置开始遍历,另一个指针从数组的末尾开始遍历,两个指针同时向数组的中间移动。当一个指针遇到目标元素时,另一个指针可以确定目标元素在数组中的位置。


1. 长度最小的子数组(medium)


1. 题目链接:209. 长度最小的子数组
2. 题目描述:
给定一个含有n个正整数的数组和一个正整数target
找出该数组中满足其总和大于等于target的长度最小的 连续子数组
[numsl, numsl + 1, ..., numsr - 1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回0

示例 1:
   输入:target = 7, nums = [2,3,1,2,4,3]
   输出:2
   解释:子数组[4,3]是该条件下的长度最小的子数组。
解法一:(暴力求解)(会超时)
算法思路:
「从前往后」枚举数组中的任意一个元素,把它当成起始位置。然后从这个「起始位置」开始,然后寻找一段最短的区间,使得这段区间的和「大于等于」目标值。将所有元素作为起始位置所得的结果中,找到「最小值」即可。
C++代码如下:

class Solution {
public:
	int minSubArrayLen(int target, vector<int>& nums) {
		// 记录结果
		int ret = INT_MAX;
		int n = nums.size();
		// 枚举出所有满⾜和⼤于等于 target 的⼦数组[start, end]
		// 由于是取到最⼩,因此枚举的过程中要尽量让数组的⻓度最⼩
		// 枚举开始位置
		for (int start = 0; start < n; start++)
		{
			int sum = 0; // 记录从这个位置开始的连续数组的和
			// 寻找结束位置
			for (int end = start; end < n; end++)
			{
				sum += nums[end]; // 将当前位置加上

				if (sum >= target) // 当这段区间内的和满⾜条件时
				{
					// 更新结果,start 开头的最短区间已经找到
					ret = min(ret, end - start + 1);
					break;
				}
			}
		}
		// 返回最后结果
		return ret == INT_MAX ? 0 : ret;
	}
};

解法二(滑动窗口):
请添加图片描述

算法思路:
由于此问题分析的对象是「⼀段连续的区间」,因此可以考虑「滑动窗口」的思想来解决这道题。让窗口满足:从i位置开始,窗口内所有元素的和小于 target (那么当窗口内元素之和第一次大于等于目标值的时候,就是i位置开始,满足条件的最小长度)。
做法:将右端元素划入窗口中,统计出此时窗口内元素的和:

  • 如果窗口内元素之和大于等于 target :更新结果,并且将左端元素划出去的同时继续判断是否满足条件并更新结果(因为左端元素可能很小,划出去之后依旧满足条件)
  • 如果窗口内元素之和不满足条件:right++,另下⼀个元素进入窗口。

为何滑动窗口可以解决问题,并且时间复杂度更低?

  • 这个窗口寻找的是:以当前窗口最左侧元素(记为left1)为基准,符合条件的情况。也就是在这道题中,从left1开始,满足区间和 sum >= target 时的最右侧(记为right1 )能到哪里。
  • 我们既然已经找到从left1开始的最优的区间,那么就可以大胆舍去left1。但是如果继续像方法一 一样,重新开始统计第二个元素(left2)往后的和,势必会有大量重复的计算(因为我们在求第一段区间的时候,已经算出很多元素的和了,这些和是可以在计算下次区间和的时候用上的)。
  • 此时,rigth1的作用就体现出来了,我们只需将left1这个值从sum中剔除。从right1 这个元素开始,往后找满足left2元素的区间(此时right1也有可能是满足的,因为left1可能很小。 sum 剔除掉 left1 之后,依旧满足大于等于target )。这样我们就能省掉大量重复的计算。
  • 这样我们不仅能解决问题,而且效率也会大大提升。

时间复杂度: 虽然代码是两层循环,但是我们的left指针和right指针都是不回退的,两者最多都往后移动n次。因此时间复杂度是O(N)

C++代码实现:

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int len = INT_MAX, n = nums.size(), sum = 0;
        for(int left = 0, right = 0;right < n;++right)
        {
            sum += nums[right]; // 进窗口
            while(sum >= target) // 判断条件
            {
                len = min(len, right - left + 1); // 更新结果
                sum -= nums[left++]; // 出窗口
            }
        }
        return len == INT_MAX ? 0 : len;
    }
};


2. 无重复字符的最长子串(medium)


1. 题目链接:3.无重复字符串的最长子串
2. 题目描述:
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
   输入: s = "abcabcbb"
   输出:3
   解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3

示例 2:
   输入: s = "bbbbb"
   输出:1
   解释: 因为无重复字符的最长子串是 "b",所以其长度为 1

提示:
   • 0 <= s.length <= 5 * 10^4
   •s由英文字目、数字、符号和空格组成 1

3. 解法一:(暴力求解)(不会超时,可以通过)
算法思路:
枚举「从每⼀个位置」开始往后,无重复字符的子串可以到达什么位置。找出其中长度最大的即可。

在往后寻找无重复子串能到达的位置时,可以利用「哈希表」统计出字符出现的频次,来判断什么时候子串出现了重复元素。

C++代码实现:

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int left = 0, n = s.size(), len = 0;
        while(left < n)
        {
            int hash[128] = {0}; // 创建一个数组来模拟哈希表
            int right = left;
            while(right < n)
            {
                hash[s[right]]++;
                if(hash[s[right]] > 1)
                {
                    break;
                }
                len = max(len, right - left + 1);
                ++right;
            }
            ++left;
        }
        return len;
    }
};

4. 解法二:(滑动窗口)

算法思路:
研究的对象依旧是一段连续的区间,因此继续使用「滑动窗口」思想来优化。
让滑动窗口满足:窗口内所有元素都是不重复的。
做法:右端元素 ch 进入窗口的时候,哈希表统计这个字符的频次:

  • 如果这个字符出现的频次超过1,说明窗口内有重复元素,那么就从左侧开始划出窗口,直到ch这个元素的频次变为1,然后再更新结果。
  • 如果没有超过1,说明当前窗口没有重复元素,可以直接更新结果。

请添加图片描述

C++代码实现:

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int hash[128] = {0}; // 创建数组来模拟哈希表
        int ret = 0, n = s.size();
        for(int left = 0, right = 0;right < n;++right)
        {
            hash[s[right]]++; // 进窗口
            while(hash[s[right]] > 1) // 判断条件
            {
                hash[s[left++]]--; // 出窗口
            }
            ret = max(ret, right - left + 1); // 更新结果
        }
        return ret;
    }
};


3. 最大连续 1 的个数 III(medium)


1. 题目链接:1004. 最大连续 1 的个数 III
2. 题目描述:
给定一个二进制数组nums和一个整数 k,如果可以翻转最多k0 ,则返回 数组中连续1 的最大个数 。

示例 1:
  输入:nums = [1,1,1,0,0,0,1,1,1,1,0], K = 2
  输出:6
  解释:[1,1,1,0,0,1,1,1,1,1,1]
  粗体数字从0翻转到 1,最长的子数组长度为 6

3. 解法(滑动窗口):
算法思路:
不要去想怎么翻转,不要把问题想的很复杂,这道题的结果无非就是⼀段连续的1中间塞了 k 0
因此,我们可以把问题转化成:求数组中一段最长的连续区间,要求这段区间内0的个数不超过k个。
既然是连续区间,可以考虑使用「滑动窗口」来解决问题。
算法流程:
a. 初始化一些变量left = 0 ,right = 0 , ret = 0
b. 当right小于数组大小的时候,一直下列循环:

  1. 让当前元素进入窗口
  2. 检查0的个数是否超标
    • 如果超标,依次让左侧元素滑出窗口,直到0的个数恢复正
      常;
  3. 程序到这里,说明窗口内元素是符合要求的,更新结果;
  4. right++ ,处理下一个元素;

c. 循环结束后,ret 存的就是最终结果。

请添加图片描述
C++代码如下:

class Solution {
public:
    int longestOnes(vector<int>& nums, int k) {
        int n = nums.size(), ret = 0;
        int count_zero = 0;
        for(int left = 0, right = 0;right < n;++right)
        {
            if(!nums[right])  // 进窗口时,遇到0,计数器++
                ++count_zero;
            while(count_zero > k)
                if(nums[left++] == 0) // 出窗口时,遇到0.计数器--
                    --count_zero;
            ret = max(ret, right - left + 1); // 更新结果
        }
        return ret;
    }
};


4. 将 x 减到 0 的最小操作数 (medium)


1. 题目链接:1658. 将 x 减到 0 的最小操作数
2. 题目描述:
给你一个整数数组nums和一个整数x。每一次操作时,你应当移除数组nums最左边或最右边的元素,然后从x中减去该元素的值。请注意,需要 修改 数组以供接下来的操作使用。

如果可以将x恰好 减到0,返回 最小操作数 ;否则,返回- 1

示例 1:
  输入:nums = [1,1,4,2,3], x = 5
  输出:2
  解释:最佳解决方案是移除后两个元素,将x减到0

示例 2:
  输入:nums = [5,6,7,8,9], x = 4
  输出:-1

示例 3:
  输入:nums = [3,2,20,1,1,3], x = 10
  输出:5
  解释:最佳解决方案是移除后三个元素和前两个元素(总共 5 次操作),将x减到0

3. 解法:(滑动窗口)
算法思路:
题目要求的是数组「左端 + 右端」两段连续的、和为x的最短数组,信息量稍微多一些,不易理清思路;我们可以转化成求数组内一段连续的、和为sum(nums) - x的最长数组。此时,就是熟悉的「滑动窗口」问题了。
在这里插入图片描述

算法流程:
a. 转化问题:求target = sum(nums) - x。如果target < 0,问题无解;
b.初始化左右指针 l = 0, r = 0 (滑动窗口区间表时为[l, r] ,左右区间是否开闭很重要,必须设定与代码一致),记录当前滑动窗口内数组和的变量sum = 0,记录当前满足条件数组的最大区间长度 maxLen = -1
c. 当 r 小于等于数组长度时,一直循环:

  1. 如果sum < target,右移右指针,直至变量和大于等于target,或右指针已经移到头;
  2. 如果sum > target,右移左指针,直至变量和小于等于target,或左指针已经移到头;
  3. 如果经过前两步的左右移动使得sum == target,维护满足条件数组的最大长度,并让下个元素进入窗口;

d. 循环结束后,如果maxLen的值有意义,则计算结果返回;否则,返回 -1

C++代码实现:

class Solution {
public:
    int minOperations(vector<int>& nums, int x) {
        int sum_nums = 0, n = nums.size();
        for(int& e : nums)
            sum_nums += e; // 求出数组的总和
        if(sum_nums < x) // 处理细节问题
            return -1;
        int target = sum_nums - x, cur = 0, maxLen = -1; 
        for(int left = 0, right = 0;right < n;++right)
        {
            cur += nums[right]; // 入窗口
            while(cur > target) // 判断条件
                cur -= nums[left++]; // 出窗口
            if(cur == target)
                maxLen = max(maxLen, right - left + 1); // 满足条件,更新结果
        }
        return maxLen == -1 ? -1 : n - maxLen;
    }
};


5. 水果成篮(medium)


1. 题目链接:904.水果成篮
2. 题目描述:
你正在探访一家农场,农场从左到右种植了一排果树。这些树用一个整数数组fruits表示,其中fruits[i]是第i棵树上的水果 种类
你想要尽可能多地收集水果。然而,农场的主人设定了一些严格的规矩,你必须按照要求采摘水果:

  • 你只有 两个 篮子,并且每个篮子只能装 单一类型 的水果。每个篮子能够装的水果总量没有限制。
  • 你可以选择任意一棵树开始采摘,你必须从 每棵 树(包括开始采摘的树)上 恰好摘一个水果 。采摘的水果应当符合篮子中的水果类型。每采摘一次,你将会向右移动到下一棵树,并继续采摘。
  • 一旦你走到某棵树前,但水果不符合篮子的水果类型,那么就必须停止采摘。

给你一个整数数组fruits ,返回你可以收集的水果的 最大 数目。

示例 1:
  输入:fruits = [1,2,1]
  输出:3
  解释:可以采摘全部 3 棵树。

示例 2:
  输入:fruits = [0,1,2,2]
  输出:3
  解释:可以采摘 [1,2,2] 这三棵树。
  如果从第一棵树开始采摘,则只能采摘 [0,1] 这两棵树。

示例 3:
  输入:fruits = [1,2,3,2,2]
  输出:4
  解释:可以采摘 [2,3,2,2] 这四棵树。
  如果从第一棵树开始采摘,则只能采摘 [1,2] 这两棵树。

示例 4:
  输入:fruits = [3,3,3,1,2,1,1,2,3,3,4]
  输出:5
  解释:可以采摘 1,2,1,1,2] 这五棵树。

3. 解法(滑动窗口):
算法思路:
研究的对象是⼀段连续的区间,可以使用「滑动窗口」思想来解决问题。
让滑动窗口满足: 窗口内水果的种类只有两种。
做法: 右端水果进入窗口的时候,用哈希表统计这个水果的频次。这个水果进来后,判断哈希表的大小。

  • 如果大小超过 2:说明窗口内水果种类超过了两种。那么就从左侧开始依次将水果划出窗口,直到哈希表的大小小于等于 2,然后更新结果;
  • 如果没有超过 2,说明当前窗口内水果的种类不超过两种,直接更新结果 ret

算法流程:
a. 初始化哈希表 hash 来统计窗口内水果的种类和数量;
b. 初始化变量:左右指针 left = 0right = 0,记录结果的变量 ret = 0
c. 当 right 小于数组大小的时候,一直执行下列循环:

  1. 将当前水果放入哈希表中;

  2. 判断当前水果进来后,哈希表的大小:

    • 将左侧元素滑出窗口,并且在哈希表中将该元素的频次减一;
    • 如果这个元素的频次减一之后变成了 0,就把该元素从哈希表中删除;
    • 重复上述两个过程,直到哈希表中的大小不超过 2;
  3. 更新结果 ret

  4. ++right,让下一个元素进入窗口

d.循环结束后ret存的就是最终结果。

C++代码实现:

class Solution {
public:
    int totalFruit(vector<int>& fruits) {
        unordered_map<int, int> hash;
        int n = fruits.size(), ret = 0;
        for(int left = 0, right = 0;right < n;++right)
        {
            hash[fruits[right]]++; // 入窗口
            while(hash.size() > 2) // 判断条件
            {
                hash[fruits[left]]--;
                if(hash[fruits[left]] == 0)
                    hash.erase(fruits[left]);
                ++left; // 出窗口
            }
            ret = max(ret, right - left + 1); // 更新结果
        }
        return ret;
    }
};


6. 找到字符串中所有字母异位词(medium)


1. 题目链接:438.找到字符串中所有字母异位词
2. 题目描述:
给定两个字符串 sp,找到 s 中所有 p异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。

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

示例 1:
  输入: 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" 的异位词。

提示:

  • 1 <= s.length, p.length <= 3 * 104
  • sp 仅包含小写字母

3. 解法(滑动窗口 + 哈希表):
算法思路:

  • 研究对象是连续的区间,因此可以尝试使⽤滑动窗口的思想来解决。

  • 如何判断当前窗口内的所有字符是符合要求的呢?
    我们可以使用两个哈希表,其中⼀个将目标串的信息统计起来,另⼀个哈希表动态的维护窗口内字符串的信息。
    当动态哈希表中包含目标串中所有的字符,并且对应的个数都不小于目标串的哈希表中各个字
    符的个数,那么当前的窗口就是一种可行的方案。

算法流程:
a.定义两个哈希表: 1 号哈希表 hash1 用来记录子串的信息, 2 号哈希表 hash2 用来记录目标串 t 的信息;
b.实现一个接口函数check(),判断当前窗口是否满足要求:

  1. 遍历两个哈希表中对应位置的元素:
    如果 t 中某个字符的数量大于窗口中字符的数量,也就是 2 号哈希表某个位置大于1 号哈希表。说明不匹配,返回 false
  2. 如果全都匹配,返回 true

主函数中:
a. 先将 t 的信息放入 2 号哈希表中;
b.初始化⼀些变量:左右指针: left = 0right = 0 ;目标子串的长度: len =INT_MAX(也可以令它等于字符串的总长度,效果是一样的) ;目标子串的起始位置: retleft ;(通过目标子串的起始位置和长度,我们就能找到结果)
c. 当 right 小于字符串 s 的长度时,一直下列循环:

  1. 将当前遍历到的元素扔进 1 号哈希表中;
  2. 检测当前窗口是否满足条件:
    • 如果满足条件:
      • 判断当前窗口是否变小。如果变小:更新长度 len ,以及字符串的起始位置retleft
      • 判断完毕后,将左侧元素滑出窗口,顺便更新 1 号哈希表;
      • 重复上面两个过程,直到窗口不满足条件;
  3. right++ ,遍历下⼀个元素;

d. 判断 len 的长度是否等于 INT_MAX

  1. 如果相等,说明没有匹配,返回空串;
  2. 如果不想等,说明匹配,返回 s 中从 retleft 位置往后 len 长度的字符串。

【注意】
这里题目说明,s 和 p 仅包含小写字母,所以我们可以定义一个数组来记录每个字符出现频次。

C++代码实现:

class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        vector<int> ret;
        // 创建两个数组模拟哈希表
        int hash1[26] = { 0 }; // 存放 p 字符串中的字符
        int hash2[26] = { 0 }; // 存放 s 字符串中的字符
        for(char e : p)  // 将 p 字符串中的字符出现的个数存入到数组中
            hash1[e - 'a']++; 
        int sn = s.size(), pn = p.size();
        int count = 0; // 定义一个计数器来判断是否更新结果
        for(int left = 0, right = 0;right < sn;++right)
        {
            // 存入的是hash1中有的字符(先存储再比较),计数器++
            char in = s[right];
            if(++hash2[in - 'a'] <= hash1[in - 'a']) 
                ++count;
            if(right - left + 1 > pn) // 保持我们维护的部分不超过pn
            {
                char out = s[left]; 
                // 如果超过,判断我们减的数是否为hash1中的数,如果是count--,不是就直接将left向右移
                if(hash2[out - 'a']-- <= hash1[out - 'a'])
                    --count;
                ++left;
            }
            if(count == pn)
                ret.push_back(left);
        }
        return ret;
    }
};


7. 串联所有单词的子串(hard)


1. 题目链接:串联所有单词的子串
2. 题目描述:
给定一个字符串 s 和一个字符串数组 wordswords 中所有字符串 长度相同
s 中的 串联子串 是指一个包含 words 中所有字符串以任意顺序排列连接起来的子串。

  • 例如,如果 words = ["ab","cd","ef"], 那么 "abcdef""abefcd""cdabef""cdefab""efabcd", 和 "efcdab" 都是串联子串。 "acdbef" 不是串联子串,因为他不是任何 words 排列的连接。

返回所有串联子串在 s 中的开始索引。你可以以 任意顺序 返回答案。

示例 1:
  输入: s = "barfoothefoobarman", words = ["foo","bar"]
  输出:[0,9]
  解释: 因为 words.length == 2 同时 words[i].length == 3,连接的子字符串的长度必须为 6。
     子串 "barfoo" 开始位置是 0。它是 words 中以 ["bar","foo"] 顺序排列的连接。
     子串 "foobar" 开始位置是 9。它是 words 中以 ["foo","bar"] 顺序排列的连接。
     输出顺序无关紧要。返回 [9,0] 也是可以的。

示例 2:
  输入:s = "wordgoodgoodgoodbestword", words = ["word","good","best","word"]
  输出:[]
  解释: 因为 words.length == 4 并且 words[i].length == 4,所以串联子串的长度必须为16
     s 中没有子串长度为 16 并且等于 words 的任何顺序排列的连接。所以我们返回一个空数组。

示例 3:
  输入s = "barfoofoobarthefoobarman", words = ["bar","foo","the"]
  输出[6,9,12]
  解释:因为 words.length == 3 并且 words[i].length == 3,所以串联子串的长度必须为 9。
     子串 "foobarthe" 开始位置是 6。它是 words 中以 ["foo","bar","the"] 顺序排列的连接。
     子串 "barthefoo" 开始位置是 9。它是 words 中以 ["bar","the","foo"] 顺序排列的连接。
     子串 "thefoobar" 开始位置是 12。它是 words 中以 ["the","foo","bar"] 顺序排列的连接。

3. 解法:(滑动窗口 + 哈希表)
算法思路:
如果我们把每⼀个单词看成⼀个⼀个字母,问题就变成了找到「字符串中所有的字母异位词」。无非就是之前处理的对象是⼀个⼀个的字符,我们这里处理的对象是一个一个的单词。

C++代码实现:

class Solution
{
public:
	vector<int> findSubstring(string s, vector<string>& words)
	{
		vector<int> ret;
		unordered_map<string, int> hash1; // 保存 words ⾥⾯所有单词的频次
		for (auto& s : words) hash1[s]++;
		int len = words[0].size(), m = words.size();
		for (int i = 0; i < len; i++) // 执⾏ len 次
		{
			unordered_map<string, int> hash2; // 维护窗⼝内单词的频次
			for (int left = i, right = i, count = 0; right + len <= s.size();
				right += len)
			{
				// 进窗⼝ + 维护 count
				string in = s.substr(right, len);
				hash2[in]++;
				if (hash1.count(in) && hash2[in] <= hash1[in]) count++;
				// 判断
				if (right - left + 1 > len * m)
				{
					// 出窗⼝ + 维护 count
					string out = s.substr(left, len);
					if (hash1.count(out) && hash2[out] <= hash1[out]) count--;
					hash2[out]--;
					left += len;
				}
				// 更新结果
				if (count == m) ret.push_back(left);
			}
		}
		return ret;
	}
};


8. 最小覆盖子串(hard)


1. 题目链接:最小覆盖子串
2. 题目描述:
给你一个字符串 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 的子串中,
     因此没有符合条件的子字符串,返回空字符串。

3. 解法:(滑动窗口 + 哈希表)
算法思路:

  • 研究对象是连续的区间,因此可以尝试使用滑动窗口的思想来解决。
  • 如何判断当前窗口内的所有字符是符合要求的呢?
    我们可以使用两个哈希表,其中一个将目标串的信息统计起来,另⼀个哈希表动态的维护窗口内字符串的信息。
    当动态哈希表中包含目标串中所有的字符,并且对应的个数都不小于目标串的哈希表中各个字符的个数,那么当前的窗口就是一种可行的方案。

算法流程:
a. 定义两个的哈希表: 1 号哈希表 hash1 用来记录子串的信息, 2 号哈希表 hash2用来记录目标串 t 的信息;
b. 定义一个键值对pos_len,用来记录在目标串中符合要求的位置和长度。
c. 定义一个count变量来统计目标串t中的元素种类(不是每个元素出现的频次)

  • 如果遇到hash1的元素,并且出现频次相同(频次小于或大于都不行),count++(进窗口);
  • 判断count是否等于hash1中元素个数(不含元素频次),如果相等且为最小值,保存在键值对pos_len中。
  • 如果在目标串中找到了第一次满足要求的子串,那么就移动left减小目标串(出窗口),看是否符合要求,在left移动的过程中,还要判断出窗口的元素是否与hash1中该元素的种类和频次是否相同,如果相同,count--

C++代码实现:

class Solution {
public:
    string minWindow(string s, string t) {
        if(t.size() > s.size()) // 如果t字符串的长度比s大,那么可以直接返回空串
            return "";
        unordered_map<char, int> hash1; // 统计字符串 t 中每个字符出现的频次
        unordered_map<char, int> hash2; // 统计窗口内每个字符出现的频次
        string ret;
        int n = s.size();
        pair<int,int> pos_len(-1,n + 1); // 定义一个键值对,记录最小的子串的起始位置以及长度
        for(char e : t) // 将字符串 t 中的字符映射到哈希表中
            hash1[e]++;
        // count 变量用来统计字符的种类(不是次数),
        // 如果种类相同,则说明窗口内包含t中的所以字符(频次在下面的操作可控制)
        for(int left = 0, right = 0, count = 0;right < n;++right)
        {
            char in = s[right];
            ++hash2[in]; // 进窗口
            // 判断进入窗口的字符的频次是否等于hash1中对应该字符的频次,如果是count++
            if(hash1.count(in) && hash2[in] == hash1[in]) ++count;
            while(count == hash1.size())
            {
                // 如果count==hash1.size(),说明此时窗口内的已包含有t中所有字符
                // 此时再看最小长度是否需要更新即可
                if(count == hash1.size() && (pos_len.second > right - left + 1))
                    pos_len = pair<int,int>(left, right - left + 1);
                char out = s[left];
                // 判断出窗口的字符的频次是否等于hash1中对应该字符的频次,--count
                if(hash1.count(out) && hash2[out]-- == hash1[out]) --count;
                ++left; // 出窗口
            }
        }
        if(pos_len.second != n + 1)
            ret = s.substr(pos_len.first, pos_len.second);
        return ret;
    }
};
  • 20
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Hyt的笔记本

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

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

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

打赏作者

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

抵扣说明:

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

余额充值