滑动窗口(leetcode 76、567、438、3、1438、239)

字符串类的滑动窗口

leetcode 76. 最小覆盖子串

一般的题目: 有一个子串与一个主串, 找到这个串的起始地址、这个串是否存在、子串在主串是否存在并找出最小的串。
滑动窗口的思路:维护一个窗口,不断滑动,然后更新答案么,那么如何向窗口中添加新元素,如何缩小窗口,在窗口滑动的哪个阶段更新结果呢? 细节问题很烦人; 首先初始化左右窗口的值都是零(窗口就是双指针的另外一种表现形式),根据需要进行窗口的更新;

leetcode 76: 给你一个字符串 S、一个字符串 T,请在字符串 S 里面找出:包含 T 所有字符的最小子串。

输入: S = “ADOBECODEBANC”, T = “ABC”
输出: “BANC”

比如这题: 最简单的方法是使用二层for循环,第一层确定 i 的位置, 第二层从 i 的位置往后查找是否包含 T 所有的字符,一旦找到了T,就立马退出的二层循环,跟新答案;

你有没有发现:

					当 i = 0 时,  会找到” ADOBEC“ 这个串包含 T 所有字符;  答案为 ” ADOBEC“
					当 i  =  1时,  会找到” DOBECODEBA“ 这个串包含 T 所有字符; 答案还是为 ” ADOBEC“
					当 i  =  2时,  会找到” OBECODEBA“ 这个串包含 T 所有字符; 答案还是为 ” ADOBEC“
					当 i  =  3时,  会找到” BECODEBA“ 这个串包含 T 所有字符; 答案还是为 ” ADOBEC“
					当 i  =  4时,  会找到” ECODEBA“ 这个串包含 T 所有字符; 答案还是为 ” ADOBEC“
					当 i  =  5时,  会找到” CODEBA“ 这个串包含 T 所有字符; 答案还是为 ” ADOBEC“

第 1 ~ 5 步: 第 1 步找到的串, 包含后面 2 ~ 5步找到的串, 所以第2 ~ 5步的努力都是白费力气, 白做了, 如果这个过程可以优化掉,偷偷懒,那么时间复杂度就会降低不少

					当 i  =  6时,  会找到” ODEBANC“ 这个串包含 T 所有字符; 答案还是为 ” ADOBEC“
					当 i  =  7时,  会找到” DEBANC“ 这个串包含 T 所有字符; 答案还是为 ” ADOBEC“
					当 i  =  8时,  会找到” EBANC“ 这个串包含 T 所有字符; 答案跟新为 ” EBANC“
					当 i  =  9时,  会找到” BANC“ 这个串包含 T 所有字符; 答案跟新为 ” BANC“

第 6 ~ 9 步:与前面情况一样, 这里也可以优化;

优化策略就是加入两个指针(i , j), 称为滑动窗口;
两个指针首先指向0: 首先 j 先走(往右),直到在窗口的范围类(i,j)的串包含"BANC"; 然后 i 走(往右),直到在窗口的范围类(i,j)的串不包含"BANC"; 这时候跟新答案,答案字符串为窗口的范围覆盖的字符串; 简而言之,j 找符合条件的字符串,i 破环字符串直到不符合条件。

找符合条件的字符串,可以用散列表加一个 数量 nums(为T的长度) ,每找到一个字符nums就自减,等于零就是找到了; 破环字符串反过来,每找到一个字符nums就自加,nums大于零 字符串就被破环了;
这一题有个细节就是小写字母与大小写字母都有, 所以散列表要开255;

AC代码:

class Solution {
public:
	string minWindow(string s, string t) {
		int m[255];  //开散列表
		int i, j, nums; // i , j 双指针; nums : 长度
		string outc = "";  //答案字符串
		memset(m, 0, sizeof(m));  //初始化散列表每一个元素为零
		for (auto ch : t) m[ch - 'A']++;  //对应的字母位的数量+1
		nums = t.size();  //复制t的长度
		i = j = 0;  //复制为 0
		while (true) {
			while (nums != 0 && j < s.size()) {  //找符合条件的字符串,nums == 0 就是找到了
				if(m[s[j] - 'A'] > 0)   //判断s[j]这个字符是否已经被找到了, 如果已经被找到了就不需要nums--了
					nums--;
				m[s[j] - 'A']--;//减少s[j] 这个字母的数量
				j++;
			}
			if (nums != 0) break;
			while (nums == 0 && i < j) {//破环字符串,nums > 0 就表示字符串被破坏了
				m[s[i] - 'A']++;  //将s[i]这个字符的数量+1
				if(m[s[i] - 'A'] > 0)  //如果s[i]这个字符的数量大于零,说明已经破环了字符串, nums 加 1;
					nums++;
				i++;
			}
			//更新答案字符串窗口的范围覆盖的字符串
			if (outc.size() == 0) outc = s.substr(i - 1, j - i + 1);  //第一次跟新字符串;
			outc = j - i < outc.size()? s.substr(i - 1, j - i + 1) : outc;  //比之前的字符串短才跟新
		}
		return outc;
	}
};

接下来的 leetcode 567、438 都可以套用这个模板写;

leetcode 567. 字符串的排列

在这里插入图片描述

class Solution {
public:
	bool checkInclusion(string s1, string s2) {
		if (s1.size() == 0) return true;
		int m[255];
		int i, j, nums;
		memset(m, 0, sizeof(m));
		for (auto ch : s1) m[ch - 'A']++;
		nums = s1.size();
		i = j = 0;
		while (true) {
			while (nums != 0 && j < s2.size()) {
				if (m[s2[j] - 'A'] > 0)
					nums--;
				m[s2[j] - 'A']--;
				j++;
			}
			if (nums != 0) break;
			while (nums == 0 && i < j) {
				m[s2[i] - 'A']++;
				if (m[s2[i] - 'A'] > 0)
					nums++;
				i++;
			}
			//更新答案字符串窗口的范围覆盖的字符串
			//if (outc.size() == 0) outc = s.substr(i - 1, j - i + 1);  //第一次跟新字符串;
			//outc = j - i < outc.size()? s.substr(i - 1, j - i + 1) : outc;  //比之前的字符串短才跟新
			//上一题的这个地方的代码
			
			//这题修改后的代码
			if (j - i + 1 == s1.size()) return true;
		}
		return false;
	}
};

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

在这里插入图片描述

class Solution {
public:
	vector<int> findAnagrams(string s, string p) {
		if (p.size() == 0) return {};
		int m[255];
		int i, j, nums;
		vector<int> outc;
		memset(m, 0, sizeof(m));
		for (auto ch : p) m[ch - 'A']++;
		nums = p.size();
		i = j = 0;
		while (true) {
			while (nums != 0 && j < s.size()) {
				if (m[s[j] - 'A'] > 0)
					nums--;
				m[s[j] - 'A']--;
				j++;
			}
			if (nums != 0) break;
			while (nums == 0 && i < j) {
				m[s[i] - 'A']++;
				if (m[s[i] - 'A'] > 0)
					nums++;
				i++;
			}
			//相对于上一题, 只是将答案插入数组中;
			if (j - i + 1 == p.size()) outc.push_back(i - 1);
		}
		return outc;
	}
};

leetcode 3. 无重复字符的最长子串

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        // 哈希集合,记录每个字符是否出现过
        unordered_set<char> occ;
        int n = s.size();
        // 右指针,初始值为 -1,相当于我们在字符串的左边界的左侧,还没有开始移动
        int rk = -1, ans = 0;
        // 枚举左指针的位置,初始值隐性地表示为 -1
        for (int i = 0; i < n; ++i) {
            if (i != 0) {
                // 左指针向右移动一格,移除一个字符
                occ.erase(s[i - 1]);
            }
            while (rk + 1 < n && !occ.count(s[rk + 1])) {
                // 不断地移动右指针
                occ.insert(s[rk + 1]);
                ++rk;
            }
            // 第 i 到 rk 个字符是一个极长的无重复字符子串
            ans = max(ans, rk - i + 1);
        }
        return ans;
    }
};

数值类的滑动窗口

leetcode 1438. 绝对差不超过限制的最长连续子数组

在这里插入图片描述

这题就是正常滑动窗口的思路, 但我们需要维护一个最大值与最小值, 确保这个最大值与最小值在窗口中;
j 往右滑动,找到满足条件的最大的窗口(绝对值小于等于限制值就一直往右滑动,大于就等下); j 不动了, 说明窗口中的绝对值大于限制值了。
i 往右滑动(重新找到一个满足条件的)窗口, 找到满足条件的最大的窗口(绝对值大于限制值就一直往右滑动,小于或等于就停下)。
先 j 动, 后 i 动。

这里我们维护一个最大值与最小值, 首先想到的是排序, 但之后我们又要删掉出窗口的值,就得查找 跟 删除, 这样我们就可以使用set集合,set 自身实现排序 并 自带 查找与删除。 set会自动去重, 选用 multiset;

class Solution {
public:
	int longestSubarray(vector<int>& nums, int limit) {
		int i = 0, j = 0, len = 1;
		multiset<int> s;
		while (j < nums.size()) {
            //j向右扩大窗口
            //绝对值的差小于 limit 就可以一直扩大窗口
			while (j < nums.size() && (s.empty() || abs(*(s.begin()) - *(s.rbegin())) <= limit)) {
				s.insert(nums[j++]);
			}
            //右边界的处理
			if (abs(*(s.begin()) - *(s.rbegin())) <= limit) len = max(len, j - i);
			else len = max(len, j - i - 1);
            //i 缩小窗口.
            //绝对值的差大于 limit 就停止缩小窗口
			while (i < j && abs(*(s.begin()) - *(s.rbegin())) > limit) {
				auto it = s.find(nums[i++]);
				if (it != s.end()) {
					s.erase(it);
				}
			}
		}
		return len;
	}
};

leetcode 239. 滑动窗口最大值

在这里插入图片描述

这题用固定窗口写, 只需维护一个最大值就好了, 可以用map<int,int,greater<>>;

class Solution {
public:
	vector<int> maxSlidingWindow(vector<int>& nums, int k) {
		map<int, int, greater<int>> m;
		vector<int> v;
		for (int i = 0; i < k; i++)
			m[nums[i]]++;
		for (int i = 0, j = k - 1; j < nums.size(); ) {
			v.push_back(m.begin()->first);
			m[nums[i]]--;
			if (m[nums[i]] == 0) m.erase(nums[i]);
			i++;
			j++;
			if(j < nums.size())m[nums[j]]++;
		}
		return v;
	}
};

总结:
双指针类的滑动窗口问题思维复杂度并不高,但是出错点往往在细节。记忆常用的解题模版还是很有必要的,特别是对于这种变量名多,容易混淆的题型。有了这个框架,思考的点就转化为 “什么条件下移动左指针”,无关信息少了,思考加实现自然不是问题。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
滑动窗口是一种常用的算法技巧,可以用于解决一类问题,其中包括一些LeetCode上的题目。通过维护一个窗口,我们可以在线性时间内解决一些需要处理连续子数组或子字符串的问题。以下是一些常见的滑动窗口问题: 1. 最小覆盖子串(Minimum Window Substring):给定一个字符串S和一个字符串T,在S中找出包含T所有字符的最小子串。 2. 字符串的排列(Permutation in String):给定两个字符串s1和s2,判断s2是否包含s1的排列。 3. 找到字符串中所有字母异位词(Find All Anagrams in a String):给定一个字符串s和一个非空字符串p,找到s中所有是p的字母异位词的子串。 4. 替换后的最长重复字符(Longest Repeating Character Replacement):给定一个只包含大写英文字母的字符串s,你可以将一个字母替换成任意其他字母,使得包含重复字母的最长子串的长度最大化。 5. 至多包含两个不同字符的最长子串(Longest Substring with At Most Two Distinct Characters):给定一个字符串s,找出至多包含两个不同字符的最长子串的长度。 以上只是几个例子,滑动窗口可以应用于更多类型的问题。在解决这些问题时,我们通常使用两个指针来表示窗口的左右边界,并根据具体问题的要求移动窗口。在每次移动窗口时,我们可以更新窗口的状态,例如统计字符出现次数、判断窗口是否满足条件等。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值