代码模板:
int left = 0; // 滑动窗口左指针
int right = 0; // 滑动窗口右指针
for (; right < nums.size(); right++)
{
// 执行某项操作
...
// 若不满足窗口条件
while (condition not satisfied)
{
// 左指针右移
...
left++;
}
...
}
...
3. 无重复字符的最长子串 Medium 滑动窗口、散列表 2021/11/10
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
示例:
输入: s = “pwwkew”
输出: 3
重要思想方法:滑动窗口,遍历字符串,左端位置默认为0,右端也默认为0,从右端向右逐渐滑动。
class Solution {
public:
//右端-左端+1即为字符串长度,散列表中的元素即为拆分的字符串
//如果没有遇到散列表中的字符,则加入散列表并继续右移
//如果遇到散列表中的字符,将原有字符删除并左端向右滑动,直到该元素不出现在散列表中为止
//时间复杂度仅为O(n),比暴力解法的O(n^3)快得多!
int lengthOfLongestSubstring(string s) {
if(s.size()==0) return 0;
int max_length = 0;
int left = 0; //左端元素位置
int right = 0; //右端元素位置
unordered_set<char> lookup; //散列表,用于存放元素
for(; right < s.size(); right++)
{
while(lookup.find(s[right])!=lookup.end()) //如果在散列表中发现右端元素
{
lookup.erase(s[left]); //删除左端元素
left++; //左端向右一格
}
max_length = max(right-left+1, max_length); //比较和最大长度之间的关系
lookup.insert(s[right]); //将右端元素插入散列表
}
return max_length;
}
};
713. 乘积小于 K 的子数组 Medium 滑动窗口 2022/5/7
给你一个整数数组 nums 和一个整数 k ,请你返回子数组内所有元素的乘积严格小于 k 的连续子数组的数目。
示例:
输入:nums = [10,5,2,6], k = 100
输出:8
解释:8 个乘积小于 100 的子数组分别为:[10]、[5]、[2],、[6]、[10,5]、[5,2]、[2,6]、[5,2,6]。
需要注意的是 [10,5,2] 并不是乘积小于 100 的子数组。
滑动窗口,对于每次右指针所在的位置,right-left+1恰好等于当前以右指针为终点的子数组个数。如果乘积超过k,则左指针向右移动直到小于k。
class Solution {
public:
int numSubarrayProductLessThanK(vector<int>& nums, int k) {
if (k <= 1) return 0;
long mul = 1;
int count = 0;
int left = 0;
int right = 0;
int n = nums.size();
for (; right < n; right++)
{
mul = mul * nums[right];
// 窗口过大,左指针收缩
while (mul >= k)
{
mul = mul /= nums[left];
left++;
}
count += right - left + 1;
}
return count;
}
};
209. 长度最小的子数组 Medium 滑动窗口 2023/1/29
给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
示例:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
// 窗口的左右区间
int left = 0;
int right = 0;
int res = INT_MAX;
// 初始和设为第一个数
int sum = nums[0];
// 套用滑动窗口模板
while (right < nums.size()) {
// 窗口太小 右指针右移
if (sum < target) {
right++;
// 注意边界条件
if (right >= nums.size()) break;
sum += nums[right]; // 移完加上右指针的值
continue;
}
// 窗口太大 左指针左移
res = min(res, right - left + 1);
sum -= nums[left];
left++;
}
return res == INT_MAX ? 0 : res;
}
};
下面为改进版滑动窗口,针对窗口大,左指针右移进行while循环直至窗口过小,且在大循环开始加上右指针的值,非常巧妙。
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
// 窗口的左右区间
int left = 0;
int right = 0;
int res = INT_MAX;
// 初始和设为第一个数
int sum = 0;
// 套用滑动窗口模板
while (right < nums.size()) {
sum += nums[right];
// 窗口太大 左指针右移
if (sum >= target) {
while (sum >= target) {
sum -= nums[left];
left++;
}
res = min(res, right - left + 2);
}
// 无论左指针是否右移 右指针都要右移
right++;
}
return res == INT_MAX ? 0 : res;
}
};
本题也可抓住其连续项和的特点,使用前缀和+二分法也可实现,但时间复杂度为O(nlogn),相较滑动窗口的O(n)稍差一些。
求出前缀和后,即找到两个最接近的元素差要求大于target即可,遍历一次每次使用二分法找到对应右区间并更新长度即可。
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int n = nums.size();
int res = INT_MAX;
vector<int> preSum(n + 1, 0);
for (int i = 0; i < n; i++)
preSum[i + 1] = preSum[i] + nums[i];
// 遍历每个数为左区间,使用二分法找到其右区间
for (int i = 0; i < preSum.size(); i++) {
int targetNum = target + preSum[i];
// 二分法
int left = i;
int right = preSum.size() - 1;
while (left <= right) {
int mid = (left + right) / 2;
if (preSum[mid] < targetNum)
left = mid + 1;
else right = mid - 1;
}
// 找不到右区间
if (left >= preSum.size()) break;
res = min(res, left - i);
}
return res == INT_MAX ? 0 : res;
}
};
附二分查找模板,非常重要!!!
int searchInsert(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
while (left <= right) {
int mid = (right + left) / 2;
if (nums[mid] == target) {
return mid;
}
else if (nums[mid] < target) {
left = mid + 1;
}
else {
right = mid - 1;
}
}
return left;
}
3. 无重复字符的最长子串 Medium 滑动窗口、散列表 2021/11/10
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
示例:
输入: s = “pwwkew”
输出: 3
重要思想方法:滑动窗口,遍历字符串,左端位置默认为0,右端也默认为0,从右端向右逐渐滑动。
class Solution {
public:
//右端-左端+1即为字符串长度,散列表中的元素即为拆分的字符串
//如果没有遇到散列表中的字符,则加入散列表并继续右移
//如果遇到散列表中的字符,将原有字符删除并左端向右滑动,直到该元素不出现在散列表中为止
//时间复杂度仅为O(n),比暴力解法的O(n^3)快得多!
int lengthOfLongestSubstring(string s) {
if(s.size()==0) return 0;
int max_length = 0;
int left = 0; //左端元素位置
int right = 0; //右端元素位置
unordered_set<char> lookup; //散列表,用于存放元素
for(; right < s.size(); right++)
{
while(lookup.find(s[right])!=lookup.end()) //如果在散列表中发现右端元素
{
lookup.erase(s[left]); //删除左端元素
left++; //左端向右一格
}
max_length = max(right-left+1, max_length); //比较和最大长度之间的关系
lookup.insert(s[right]); //将右端元素插入散列表
}
return max_length;
}
};
424. 替换后的最长重复字符 Medium 滑动窗口 2023/2/1
给你一个字符串 s 和一个整数 k 。你可以选择字符串中的任一字符,并将其更改为任何其他大写英文字符。该操作最多可执行 k 次。
在执行上述操作后,返回包含相同字母的最长子字符串的长度。
示例:输入:s = “AABABBA”, k = 1
输出:4
将中间的一个’A’替换为’B’,字符串变为 “AABBBBA”。
子串 “BBBB” 有最长重复字母, 答案为 4。
本题使用了freq数组,在使用滑动窗口的同时实时记录26个字母的频数,出现次数最多的字母数+更改次数如果小于窗口长,则说明左指针需要向右移动,直到满足条件。
class Solution {
public:
int characterReplacement(string s, int k) {
// 滑动窗口
int left = 0;
int right = 0;
int res = 0;
vector<int> freq(26, 0);
while (right < s.size()) {
freq[s[right] - 'A']++;
// 求出最多字符的频数
int max_freq = *max_element(freq.begin(), freq.end());
// 当前窗口不行,左指针要右移
while (right - left + 1 > k + max_freq) {
freq[s[left] - 'A']--;
left++;
}
res = max(res, right - left + 1);
right++;
}
return res;
}
};
438. 找到字符串中所有字母异位词 Medium 滑动窗口 2023/2/1
给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。
示例:
输入: s = “cbaebabacd”, p = “abc”
输出: [0,6]
解释:
起始索引等于 0 的子串是 “cba”, 它是 “abc” 的异位词。
起始索引等于 6 的子串是 “bac”, 它是 “abc” 的异位词。
本题与上题类似,也是记录26个字母出现的频次,但本题更为简单,窗口定长,通过判断频次数组是否相等来判断是否是异位词。
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
if (s.size() < p.size()) return vector<int>();
// 滑动窗口
// 统计滑动窗口中的字符数目
vector<int> s_count(26);
vector<int> p_count(26);
vector<int> res;
// 初始化
for (int i = 0; i < p.size(); i++){
s_count[s[i] - 'a']++;
p_count[p[i] - 'a']++;
}
// 看看初始化后是否相同
if (s_count == p_count) res.push_back(0);
// 逐步删除i, 加入i + p.size()
for (int i = 0; i < s.size() - p.size(); i++){
s_count[s[i] - 'a']--;
s_count[s[i + p.size()] - 'a']++;
if (s_count == p_count) res.push_back(i + 1);
}
return res;
}
};