💬 欢迎讨论:如果你在学习过程中有任何问题或想法,欢迎在评论区留言,我们一起交流学习。你的支持是我继续创作的动力! 👍 点赞、收藏与分享:觉得这篇文章对你有帮助吗?别忘了点赞、收藏并分享给更多的小伙伴哦!你们的支持是我不断进步的动力! 🚀 分享给更多人:如果你觉得这篇文章对你有帮助,欢迎分享给更多对C++算法感兴趣的朋友,让我们一起进步!
接上篇:【优选算法篇】一文读懂滑动窗口:动态调整范围的算法利器(上篇)-CSDN博客
引言:通过上篇文章带大家简单了解“滑动窗口算法”,小试牛刀。接下来将让大家感受一下滑动窗口在解题中的更多妙处。
滑动窗口算法在面试中的重要性不仅在于它是一种高效解决问题的方法,更因为它能够系统化评估候选人在算法设计、优化、实现和调试等多个维度的能力。掌握滑动窗口的核心思想和进阶技巧,对于候选人在算法面试中脱颖而出至关重要。
滑动窗口的“窗口”指的是一个数组或字符串的连续子集。在算法中,我们通过两个指针(通常为左指针和右指针)来表示这个窗口。窗口的大小可以是固定的,也可以是动态变化的,通常依据具体问题的需求来调整。
滑动窗口还常用于处理实时数据流中的问题,例如:
当滑动窗口的大小不是固定时,窗口的大小会动态变化。通过扩展右边界,直到满足特定条件,再通过收缩左边界以保持窗口内的数据符合要求。
在一些问题中,我们需要在给定大小的滑动窗口中计算最大值或最小值。这类问题在数据流处理、实时监控等场景中非常常见。
滑动窗口可以用于解决在一定范围内对字符或元素计数的问题。例如,给定一个字符串和一个字符集,找出每个字符在滑动窗口中的出现频率。
题目链接:1658. 将 x 减到 0 的最小操作数 - 力扣(LeetCode)
题目描述:

nums 的总和 sum。我们希望通过从两端移除元素使得剩余的和为 x。
所以我们需要找到一个子数组,其和为 sum - x。这个子数组的长度越长,表示我们从两端删除的元素越少,从而操作次数最少。
target = sum - x。我们需要通过滑动窗口的方式,找到和为 target 的最长子数组。因为一旦找到这个子数组,剩下的部分就是最小操作数所需删除的元素。
left 和 right,通过移动 right 来扩展窗口,计算当前窗口的和。target,则通过移动 left 来缩小窗口,直到窗口的和小于或等于 target。target,则更新最大子数组长度。n - maxLength,其中 n 是数组的大小,maxLength 是找到的和为 target 的最长子数组的长度。class Solution
{
public:
int minOperations(vector<int>& nums, int x)
{
int n = nums.size(), sum = 0, ret = -1;
// 计算数组总和
for (auto m : nums)
{
sum += m;
}
// 计算目标和 (sum - x)
int target = sum - x;
// 如果目标和小于 0,则无法通过任何方式将 x 减到 0
if (target < 0) return -1;
int tmp = 0;
// 使用滑动窗口查找最大长度的子数组,其和为 target
for (int left = 0, right = 0; right < n; right++)
{
tmp += nums[right]; // 将当前右端元素加入窗口
// 如果窗口和大于 target,则通过移动左边界缩小窗口
while (left < n && tmp > target)
{
tmp -= nums[left++];
}
// 如果窗口和正好等于 target,更新最大子数组长度
if (tmp == target)
{
ret = max(ret, right - left + 1);
}
}
// 如果未找到合适的子数组,返回 -1
return ret == -1 ? -1 : n - ret;
}
};nums 数组,计算其总和 sum。target 设为 sum - x,如果目标值小于 0,则无法通过任何操作使 x 减小为 0,直接返回 -1。left 和 right,当 right 向右移动时,我们不断增加窗口的和 tmp。target 时,通过移动 left 指针来缩小窗口,直到窗口的和小于或等于 target。target,则记录当前子数组的长度,并更新 ret 变量。n - ret,即从两端删除的元素个数。如果没有找到合适的子数组,返回 -1。假设输入数组 nums = [1, 1, 4, 2, 3] 和 x = 5:
sum = 1 + 1 + 4 + 2 + 3 = 11。target = 11 - 5 = 6。[4, 2] 的和为 6,长度为 2。5 - 2 = 3,因为从两端移除元素后的剩余部分是 [4, 2]。最终结果为 3。
O(n)。n 次,总的时间复杂度为 O(n)。O(n)。O(1)。int minOperations(vector<int>& nums, int x)
{
int n = nums.size();
int minOps = INT_MAX; // 初始化最小操作数为无穷大
// 计算前缀和
vector<int> prefixSum(n + 1, 0);
for (int i = 0; i < n; ++i)
{
prefixSum[i + 1] = prefixSum[i] + nums[i];
}
// 暴力枚举两端的移除组合
for (int left = 0; left <= n; ++left)
{
for (int right = 0; right <= n - left; ++right)
{
int leftSum = prefixSum[left];
int rightSum = prefixSum[n] - prefixSum[n - right];
if (leftSum + rightSum == x)
{
minOps = min(minOps, left + right);
}
}
}
// 如果没有找到满足条件的组合,返回 -1
return minOps == INT_MAX ? -1 : minOps;
}代码说明
prefixSum 数组,计算出从左端开始的累积和,这样可以快速得到左端的和。时间复杂度
空间复杂度
前缀和数组需要 O(n)的额外空间。
通过滑动窗口,我们可以高效地找到和为 sum - x 的最大子数组,并计算出最小操作次数。这种方法的时间复杂度是线性的 O(n),非常适合处理大规模数据。
题目描述:

补充:

滑动窗口是维护一个区间 [left, right] 的方法,通过动态调整窗口的左右边界来满足题目要求。在本题中:
利用哈希表 unordered_map<int, int> hash:
class Solution
{
public:
int totalFruit(vector<int>& fruits)
{
// 用于记录窗口中每种水果的出现次数
unordered_map<int, int> hash;
int ret = 0; // 用于存储当前能够采摘的最大连续水果数
// 定义滑动窗口的左右边界
for (int left = 0, right = 0; right < fruits.size(); right++)
{
// 将右边界的水果加入窗口,增加其计数
hash[fruits[right]]++;
// 如果窗口内水果种类超过两种,则开始缩小窗口
while (hash.size() > 2)
{
// 减少左边界水果的计数
hash[fruits[left]]--;
// 如果某种水果的计数变为 0,移除该水果种类
if (hash[fruits[left]] == 0)
hash.erase(fruits[left]);
// 移动左边界
left++;
}
// 更新最大连续子数组长度
ret = max(ret, right - left + 1);
}
// 返回最大长度
return ret;
}
};1. 初始化
hash 记录窗口中每种水果的数量。left 和 right 表示滑动窗口的左右边界。ret 用来记录当前窗口的最大长度。2. 遍历数组
right 遍历水果数组 fruits,每次将 fruits[right] 加入窗口,并在 hash 中更新对应水果的数量。
3. 调整窗口
hash.size()(窗口中的水果种类数)大于 2,说明窗口不满足条件,开始通过移动 left 缩小窗口:
fruits[left] 的计数。
left 向右移动一位。
4. 更新结果
right - left + 1,并更新 ret。
5. 返回结果
ret。以输入 fruits = [1, 2, 1, 2, 3] 为例,展示滑动窗口的动态变化:
初始状态
left = 0, right = 0, hash = {}, ret = 0。Step 1: right = 0
1: hash = {1: 1}。ret = max(0, 0 - 0 + 1) = 1。窗口:[1] 种类:{1} 当前最大长度:1
Step 2: right = 1
2: hash = {1: 1, 2: 1}。ret = max(1, 1 - 0 + 1) = 2。窗口:[1, 2] 种类:{1, 2} 当前最大长度:2
Step 3: right = 2
1: hash = {1: 2, 2: 1}。ret = max(2, 2 - 0 + 1) = 3。窗口:[1, 2, 1] 种类:{1, 2} 当前最大长度:3
Step 4: right = 3
2: hash = {1: 2, 2: 2}。ret = max(3, 3 - 0 + 1) = 4。窗口:[1, 2, 1, 2] 种类:{1, 2} 当前最大长度:4
Step 5: right = 4
3:
hash = {1: 2, 2: 2, 3: 1}。1,hash = {1: 1, 2: 2, 3: 1}。1,hash = {2: 2, 3: 1}。left = 2。ret = max(4, 4 - 2 + 1) = 4。窗口:[2, 1, 3] 种类:{2, 3} 当前最大长度:4
最终结果
遍历结束后,最大长度为 ret = 4。
动态过程图(用字符表示)

3.3 时间与空间复杂度
使用了一个哈希表,最多存储 2 个水果种类,空间复杂度为 O(1)。
class Solution
{
public:
int totalFruit(vector<int>& fruits)
{
int n = fruits.size();
int maxLength = 0;
for (int i = 0; i < n; ++i)
{
unordered_set<int> basket; // 用于存储当前区间的水果种类
int currentLength = 0;
for (int j = i; j < n; ++j)
{
basket.insert(fruits[j]); // 将当前水果加入集合
if (basket.size() > 2) // 超过两种水果,停止继续扩展
break;
currentLength++;
maxLength = max(maxLength, currentLength); // 更新最大长度
}
}
return maxLength;
}
};代码逻辑解析:
i 为起点,遍历数组。j 为终点,扩展当前子区间。unordered_set<int> 存储当前区间内的水果种类。basket.size() <= 2),计算区间长度并更新 maxLength。unordered_set 的大小决定的。滑动窗口的动态调整避免了暴力求解的冗余计算,通过哈希表灵活维护窗口中的水果种类和数量,保证了算法的高效性。时间复杂度为 O(n),空间复杂度为 O(1)。
通过上述「将x减小到0的最小操作数」及「水果成篮」的例子,可以总结出滑动窗口算法的核心思想、应用场景和优化技巧。滑动窗口通过动态调整左右指针,在遍历数组时灵活地扩展和收缩窗口,避免了暴力解法中不必要的重复计算,使得许多问题的时间复杂度从 O(n^2) 或更高,优化到 O(n)。它在处理涉及连续子数组或子串的查找、统计、优化问题时,具有非常高的效率和空间优势,是解决此类问题的强大工具。
路虽远,行则将至;事虽难,做则必成
亲爱的读者们,下一篇文章再会!!!