优选算法---滑动窗口 题目及算法分析 代码实现
滑动窗口是一种通过双指针(左右边界)动态维护数组或字符串中连续子序列的算法技巧 利用问题的单调性 将暴力解法的时间复杂度优化至其本质是避免重复计算
通过八道题目练习一下滑动窗口
一.长度最小的子数组
题目解析
给了一个target 和一个数组 要求在数组中找到连续的一段数据的和大于等于target 返回满足这样情况的最短长度 不存则返回0
算法分析
1.暴力求解
两层for循环遍历所有情况 时间复杂度为O(N^2)
2.滑动窗口
和双指针法的核心一致 通过控制left和right两个指针来控制字符串中连续子序列范围 分析之后可以发现 其实就是把暴力求解中无效的计算给省去了
每一次循环 left++或者right++ 最坏的情况下就是2N 那么时间复杂度也就是O(N)
如果不存在满足条件的情况 最终需返回0
根据题目要求数据长度上限为 10^5,可直接将最终要返回的变量len初始化为 10^5 + 1。如此一来,当存在满足条件的情况时 len的值就会被更新 若不存在满足条件的情况 len的值不会改变 在返回最终结果前,判断len的值是否发生改变,若未改变,则返回 0。
代码实现
class Solution {
public:int lengthOfLongestSubstring(string s) {int n=s.size(),recon=0;int left=0,right=0;unordered_set<char> s1;while(right<n){while(!(s1.insert(s[right]).second))s1.erase(s[left++]); //如果插入失败了在while循环结束之后 left已经到了正确位置recon = max(right-left+1, recon);right++;}return recon;}
};
或者可以用数组来模拟哈希表 会比直接使用哈希表更快
class Solution {
public:int lengthOfLongestSubstring(string s) {int n=s.size(),recon=0;int left=0,right=0;int hax[128]={0};while(right<n){hax[s[right]]++;while(hax[s[right]]>1)hax[s[left++]]--;recon=max(right-left+1,recon);right++;}return recon;}
};
二.无重复字符的最长子串
算法分析
暴力求解
两层for循环遍历所有情况 而且每次增加新字符时候还需要判断一下这个字符是否出现过还需要一层循环 所以时间复杂度为O(N^3)
使用哈希表 可以把判断数据是否重复的过程省略掉 时间复杂度变为O(N^2)
滑动窗口
和第一道题目类型 其实都是把暴力求解中不可能的情况给省略掉了
每一次right++或者left++ 最坏情况2N 时间复杂度为O(N)
代码实现
class Solution {
public:int lengthOfLongestSubstring(string s) {int n=s.size(),recon=0;int left=0,right=0;unordered_set<char> s1;while(right<n){while(!(s1.insert(s[right]).second)) //insert返回值为pair<iterator,bool>类型s1.erase(s[left++]); recon = max(right-left+1, recon);right++;}return recon;}
};
数组模拟哈希表实现
class Solution {
public:int lengthOfLongestSubstring(string s) {int n=s.size(),recon=0;int left=0,right=0;int hax[128]={0};while(right<n){hax[s[right]]++;while(hax[s[right]]>1)hax[s[left++]]--;recon=max(right-left+1,recon);right++;}return recon;}
};
三.最大连续1的个数III
题目解析
给了一个二进制的数组 里面只有0或者1 统计里面连续1的最长长度 在此基础上给了一个k 代表可以把0变为1的次数
算法分析
代码实现
class Solution {
public:int longestOnes(vector<int>& nums, int k) {int n = nums.size();int left = 0, right = 0;int recon = 0; //最终返回的结果int z = 0; //统计0的个数while (right < n){if (nums[right] != 1){if (z == k) //0已经满了的情况{while (nums[left] == 1)left++; //从左边开始去数据需要去掉一个0才能停止left++;z--;}z++;}recon = max(recon, right-left+1);right++;}return recon;}
};
四.将x减到0的最小操作数
题目解析
给了一个数组 和一个x 每一次在最左或者最右边去掉一个数 一直到去掉数的和等于x 返回操作次数的最小值
算法分析
这个题目其实可以转换一下 转换为和等于target的最长连续数组 这个target就是数组数据的和减去x
如果target是小于0的 那么题目的要求就不可能满足直接返回-1
要先用while循环判断是否大于target 大于则需要从左开始出窗口 出了数据之后可能还大于target 还需要判断 一直到不大于为止 从左出了一个数据后可能此时就等于了 要在等于时候更新数据
另外也有可能target虽然大于0了 但是也没有符合题目要求的情况 如果recon到最后都没有改变就说明没有找到 也返回-1
每一次 判断后 left++或者right++ 最坏情况2N 时间复杂度O(N)
代码实现
class Solution {
public:int minOperations(vector<int>& nums, int x) {int add = 0;for (int m : nums)add += m;int target = add - x, n = nums.size(); //先转换为和为target最长子串的问题if (target < 0) return -1;if (target == 0) return n;int left = 0, right = 0, recon = 0,sum = 0;while (right < n){sum += nums[right];while (sum > target) //大于的情况需要出数据sum -= nums[left++];if (sum == target) //如果等于符合要求了 更新结果{recon = max(recon, right - left + 1);sum -= nums[left];left++;}right++;}if (recon == 0) //如果在结束之后recon没有改变 说明没有符合要求的条件return -1;elsereturn n-recon;
}
};
五.水果成篮
题目解析
其实就是找到最长的连续子数组 不过要求里面只能有两种不同的数 返回最长的符合要求的长度
算法分析
用到哈希表 统计出现水果的种类及水果的个数 插入一个新的水果后 判断此时的种类是否大于2
如果不大于 就正常用sum来更新此时最大值的情况
如果大于2了 需要从left位置删水果 直到水果的种类重新减少到2 如果删除left位置水果后水果个数还大于2 说明刚刚删掉的水果个数不止一个 还需要继续删除left位置的水果 直到把一种水果给删完了 水果的种类重新变为为2 此时在哈希表中把这个水果给去掉 为了之后再次进行
这里更新sum是在插入一个水果之后就更新一次
代码实现
class Solution {
public:int totalFruit(vector<int>& fruits) {unordered_map<int,int>m1;int left=0,right=0;int n=fruits.size(), kind=0,sum=0;while(right<n){m1[fruits[right]]++;while(m1.size()>2){m1[fruits[left]]--;if(m1[fruits[left]]==0)m1.erase(fruits[left]); //一直到把一种水果删除为止left++;}sum=max(sum,right-left+1);right++;}return sum;}
};
六.找到字符串中所有字母异位词
题目解析
给了两个字符串 s和p 找到s中p的变位词 并返回索引即成立的第一个字母的下标 如下图
算法分析
用到类似计数排序那里的方式 把26个英文字母映射到数组下标0到25的位置 先遍历p 用hax1统计p中每一个字符出现的次数 并用con1来统计里面的字符种类
然后再用一个hax2 用left和right来控制s区间子串 用right遍历s一个一个字符插入 如果在hax2中该字符出现次数和hax1中相同了 那么此时con2++ 当con1==con2 说明此时left到right区间的值就是符合要求的 此时left的位置就是返回值的一种情况
如果在hax2中该字符出现次数小于hax1中 不需要处理
大于的话就需要从left位置开始删除 直到这个新插入的字符在hax2中的次数和hax1中相同了 left位置删除的有两种情况 一种就是该字符在hax2中存在 这种情况下 需要把该字符删除到和hax2该字符次数相同位置 另一种就是在hax2中不存在 那此时前面情况已经不可能了 需要一直删除到把这个字符也删掉为止 相当于left right区间内的值清空了
代码实现
class Solution {
public:vector<int> findAnagrams(string s, string p) {vector<int>v;int hax[26] = { 0 };int con1 = 0; //统计p中字符种数for (char x : p){hax[x - 'a']++;if (hax[x - 'a'] == 1)con1++;}int n1 = s.size(), n2 = p.size();int left = 0, right = 0, con2 = 0; //con2为此时窗口内满足p中字符个数int hax2[26] = { 0 };while (right < n1){hax2[s[right]-'a']++;if (hax2[s[right] - 'a'] == hax[s[right] - 'a']) //相等 则当前满足的多一con2++;//小于的情况不需要处理 大于的情况需要从left开始减少到新的符合为止while ((hax2[s[right]-'a'] > hax[s[right]-'a'])){if (hax2[s[left] - 'a'] == hax[s[left] - 'a']) //删除的是满足的 则con--{con2--;}hax2[s[left++]-'a']--; //左边的删除}right++;if (con2 == con1) //此时满足的符合v.push_back(left);}return v;
}
};
七.串联所有单词的子串
题目解析
给了一个字符串s 和一个字符串数组words 要求在s中找到由words里面所有字符串任意顺序组成的子串
在words中 每一个字符串的长度是相同的
算法分析
这个题目和上一个题目思路类似
异位词的题目是 数组里面存的是字符 字符串每一个位置就是就是字符 所以可以直接对应 这里我们给的是字符串数组 所以我们需要把给的字符串按固定的长度分为不同的字符串来看
这个固定的长度就是字符串数组中每一个字符串的长度 另外left和right每次要移动的长度也应该是这个长度
另外在根据words中单词长度来控制字符串的时候 对于最后剩下的长度可能不够了 这样要特殊处理下
代码实现
class Solution {
public:vector<int> findSubstring(string s, vector<string>& words) {unordered_map<string, int>m1;for (string ss : words)m1[ss]++; //统计words中每一个字符串出现的次数int wordlen = words[0].size(); //words中单个单词的长度int con1 =m1.size(); //统计words中有多少种字符串int n = s.size();vector<int>v;for (int i = 0; i <wordlen; ++i) {unordered_map<string, int>m2;int con2 = 0;int right = i,left = i; while (right < n){string str1;for (int j = 0; j < wordlen; j++) {if (s[right + j] == '\0')break;str1 += s[right + j]; //str为从right位置开始的wordlen个字符组成的字符串}if (str1.size() < wordlen)break;m2[str1]++;if (m2[str1] == m1[str1])con2++;else{while (m2[str1] > m1[str1]) //需要从左删除的情况{string str2;for (int k = 0; k < wordlen; k++)str2 += s[left + k]; //str2为从left位置开始的wordlen个字符组成的字符串if (m2[str2] == m1[str2])con2--; //如果从左删除的字符串是符合要求的则con2--m2[str2]--;left += wordlen;}}if (con2 == con1)v.push_back(left);right += wordlen;}}return v;
}
};
八.最小覆盖子串
算法分析
区别于之前题目的是 窗口内可以有t中没有的字符 或者t中有的字符出现多次 只有t中有的的字符大于等于t中的个数就可以
之前的题目在s中right位置的字符在窗口内的数量大于在给定字符串中的数量时候 需要从左边删数据到==为止 这里小于和大于的情况都不需要考虑了 只有等于的时候此时符合的个数con2++
在con2==con1时候需要处理 此时窗口内的内容就是符合要求的 如果此时窗口长度更小就更新数据 这里用begin来存一下此时left的位置 minlen来存一下此时窗口的长度 此时需要从左出窗口 为了进行下次的判断 这里需要从左一直删数据直到s中存在字符的个数不符合要求
如果一直到了最后beign都没有更新 说明没有符合要求的情况直接返回"" 否则就用begin和minlen来截取s中部分的字符串
代码实现
class Solution {
public:string minWindow(string s, string t) {unordered_map<char, int>m1;for (char x : t){m1[x]++;}int con1 =m1.size();unordered_map<char, int>m2;int n = s.size();int left = 0, right = 0;int con2 = 0;int minlen = INT_MAX;int begin = -1;while (right < n){m2[s[right]]++;if (m2[s[right]] == m1[s[right]])con2++;while (con1 == con2){if (right - left + 1 < minlen){minlen=right-left+1;begin = left;}if (m1[s[left]] == m2[s[left]])con2--;m2[s[left]]--;left++;}right++;}if (begin == -1)return "";elsereturn s.substr(begin, minlen);}
};