双指针——滑动窗口
·
双指针算法是一种常用的算法技巧,广泛应用于数组、链表、字符串等数据结构的处理中。其中,滑动窗口是双指针的一种特殊形式,主要用于解决子数组或子字符串相关的问题。下面我们详细讨论双指针和滑动窗口的区别,以及滑动窗口的特点和应用场景。
1. 双指针的基本形式
双指针的核心思想是使用两个指针(通常是下标或迭代器)在数据结构中协同工作,通过移动指针来解决问题。双指针的常见形式包括:
- 左右指针:两个指针从两端向中间移动,常用于有序数组或字符串的问题(如两数之和、反转字符串等)。
- 快慢指针:两个指针以不同的速度移动,常用于链表问题(如判断链表是否有环、找到链表的中间节点等)。
双指针的特点是:
- 两个指针的移动方向可以是同向或反向。
- 通常用于优化时间复杂度,将暴力解法的 O(n²) 优化为 O(n)。
2. 滑动窗口的特殊形式
滑动窗口是双指针的一种特殊形式,通常用于解决子数组或子字符串的问题。滑动窗口的核心思想是维护一个窗口(由两个指针表示),通过移动窗口的左右边界来满足特定条件。
滑动窗口的特点是:
- 窗口大小可变或固定:
- 可变窗口:窗口的大小根据条件动态调整。
- 固定窗口:窗口的大小固定不变。
- 同向移动:左右指针通常同向移动,且右指针主动移动,左指针被动移动。
- 时间复杂度低:通常为 O(n),因为每个元素最多被访问两次(一次由右指针,一次由左指针)。
3. 滑动窗口的典型应用场景
滑动窗口常用于以下类型的问题:
- 满足条件的最短子数组:
- 例如:找到和大于等于目标值的最短子数组。
- 满足条件的最长子数组:
- 例如:找到不包含重复字符的最长子字符串。
- 固定窗口大小的问题:
- 例如:计算大小为 k 的子数组的最大平均值。
4. 滑动窗口的模板
以下是滑动窗口的通用模板(以可变窗口为例):
假设目标是找到一个最小长度的子数组,使得子数组的和大于等于目标值 target。如果不存在这样的子数组,则返回 0。
#include <vector>
#include <climits> // 用于 INT_MAX
int slidingWindow(std::vector<int>& nums, int target) {
int left = 0; // 左指针
int result = INT_MAX; // 存储结果,初始值为最大整数
int window = 0; // 窗口状态(窗口内元素的和)
for (int right = 0; right < nums.size(); ++right) { // 右指针主动移动
// 更新窗口状态
window += nums[right];
// 当窗口满足条件时,尝试收缩左边界
while (window >= target) { // 满足条件:窗口和 >= target
// 更新结果
result = std::min(result, right - left + 1); // 更新最小长度
// 收缩左边界
window -= nums[left];
left += 1;
}
}
// 如果结果仍然是 INT_MAX,说明没有找到满足条件的子数组
return (result == INT_MAX) ? 0 : result;
}
代码说明:
- 变量初始化:
left 是左指针,初始值为 0。
result 用于存储最终结果,初始值为 INT_MAX,表示一个非常大的值。
window 用于存储当前窗口内的元素和,初始值为 0。 - 右指针移动:
使用 for 循环,右指针从 0 到 nums.size() - 1 逐个移动。
每次右指针移动时,将当前元素加入窗口状态 window。 - 窗口收缩:
当窗口内的和 window 大于等于目标值 target 时,尝试收缩左边界。
在收缩过程中,更新结果 result,取当前窗口长度 right - left + 1 和已知的最小长度的较小值。
每次收缩左边界时,从窗口状态中减去左指针所指的元素,并将左指针右移。 - 返回结果:
如果最终结果仍然是 INT_MAX,说明没有找到满足条件的子数组,返回 0。
否则返回找到的最小长度。 - 示例:
假设 nums = {2, 3, 1, 2, 4, 3},target = 7,则函数返回值为 2,因为子数组 [4, 3] 的和为 7,且长度为 2,这是满足条件的最短子数组。
5. 滑动窗口与双指针的区别
| 特性 | 双指针 | 滑动窗口 |
|---|---|---|
| 指针移动方向 | 可以同向或反向 | 通常同向移动 |
| 窗口大小 | 不固定 | 可变或固定 |
| 应用场景 | 数组、链表、字符串等 | 子数组、子字符串问题 |
| 典型问题 | 两数之和、反转链表 | 最短/最长子数组、无重复字符子串 |
| 时间复杂度 | 通常 O(n) | 通常 O(n) |
6. 滑动窗口的示例
示例 1:无重复字符的最长子字符串
给定一个字符串,找到不包含重复字符的最长子字符串的长度。
#include <iostream>
#include <unordered_set>
#include <algorithm>
using namespace std;
int lengthOfLongestSubstring(string s) {
int left = 0;
int max_len = 0;
unordered_set<char> char_set; // 记录窗口内的字符
for (int right = 0; right < s.size(); right++) {
while (char_set.find(s[right]) != char_set.end()) { // 如果字符重复,收缩左边界
char_set.erase(s[left]);
left++;
}
char_set.insert(s[right]); // 扩展右边界
max_len = max(max_len, right - left + 1);
}
return max_len;
}
int main() {
string s = "abcabcbb";
cout << "Length of longest substring: " << lengthOfLongestSubstring(s) << endl;
return 0;
}
示例 2:和大于等于目标值的最短子数组
给定一个正整数数组和一个目标值,找到和大于等于目标值的最短子数组的长度。
#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;
int minSubarrayLen(int target, vector<int>& nums) {
int left = 0;
int min_len = INT_MAX; // 初始化为最大值
int window_sum = 0;
for (int right = 0; right < nums.size(); right++) {
window_sum += nums[right]; // 扩展右边界
while (window_sum >= target) { // 满足条件时,收缩左边界
min_len = min(min_len, right - left + 1);
window_sum -= nums[left];
left++;
}
}
return (min_len == INT_MAX) ? 0 : min_len; // 如果未找到符合条件的子数组,返回 0
}
int main() {
vector<int> nums = {2, 3, 1, 2, 4, 3};
int target = 7;
cout << "Minimum subarray length: " << minSubarrayLen(target, nums) << endl;
return 0;
}
7. 总结
- 滑动窗口是双指针的一种特殊形式,主要用于解决子数组或子字符串的问题。
- 滑动窗口的特点是同向移动、窗口大小可变或固定,并且通常具有较低的时间复杂度(O(n))。
- 通过掌握滑动窗口的模板和典型问题,可以高效解决许多与子数组或子字符串相关的算法问题。
更多推荐
所有评论(0)