双指针——滑动窗口

双指针算法是一种常用的算法技巧,广泛应用于数组、链表、字符串等数据结构的处理中。其中,滑动窗口是双指针的一种特殊形式,主要用于解决子数组或子字符串相关的问题。下面我们详细讨论双指针和滑动窗口的区别,以及滑动窗口的特点和应用场景。


1. 双指针的基本形式

双指针的核心思想是使用两个指针(通常是下标或迭代器)在数据结构中协同工作,通过移动指针来解决问题。双指针的常见形式包括:

  • 左右指针:两个指针从两端向中间移动,常用于有序数组或字符串的问题(如两数之和、反转字符串等)。
  • 快慢指针:两个指针以不同的速度移动,常用于链表问题(如判断链表是否有环、找到链表的中间节点等)。

双指针的特点是:

  • 两个指针的移动方向可以是同向或反向。
  • 通常用于优化时间复杂度,将暴力解法的 O(n²) 优化为 O(n)。

2. 滑动窗口的特殊形式

滑动窗口是双指针的一种特殊形式,通常用于解决子数组或子字符串的问题。滑动窗口的核心思想是维护一个窗口(由两个指针表示),通过移动窗口的左右边界来满足特定条件。

滑动窗口的特点是:

  • 窗口大小可变或固定
    • 可变窗口:窗口的大小根据条件动态调整。
    • 固定窗口:窗口的大小固定不变。
  • 同向移动:左右指针通常同向移动,且右指针主动移动,左指针被动移动。
  • 时间复杂度低:通常为 O(n),因为每个元素最多被访问两次(一次由右指针,一次由左指针)。

3. 滑动窗口的典型应用场景

滑动窗口常用于以下类型的问题:

  1. 满足条件的最短子数组
    • 例如:找到和大于等于目标值的最短子数组。
  2. 满足条件的最长子数组
    • 例如:找到不包含重复字符的最长子字符串。
  3. 固定窗口大小的问题
    • 例如:计算大小为 k 的子数组的最大平均值。

4. 滑动窗口的模板

以下是滑动窗口的通用模板(以可变窗口为例):

假设目标是找到一个最小长度的子数组,使得子数组的和大于等于目标值 target。如果不存在这样的子数组,则返回 0#include 
#include  // 用于 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;
}

代码说明:

  1. 变量初始化:
    left 是左指针,初始值为 0。
    result 用于存储最终结果,初始值为 INT_MAX,表示一个非常大的值。
    window 用于存储当前窗口内的元素和,初始值为 0。
  2. 右指针移动:
    使用 for 循环,右指针从 0 到 nums.size() - 1 逐个移动。
    每次右指针移动时,将当前元素加入窗口状态 window。
  3. 窗口收缩:
    当窗口内的和 window 大于等于目标值 target 时,尝试收缩左边界。
    在收缩过程中,更新结果 result,取当前窗口长度 right - left + 1 和已知的最小长度的较小值。
    每次收缩左边界时,从窗口状态中减去左指针所指的元素,并将左指针右移。
  4. 返回结果:
    如果最终结果仍然是 INT_MAX,说明没有找到满足条件的子数组,返回 0。
    否则返回找到的最小长度。
  5. 示例:
    假设 nums = {2, 3, 1, 2, 4, 3},target = 7,则函数返回值为 2,因为子数组 [4, 3] 的和为 7,且长度为 2,这是满足条件的最短子数组。

5. 滑动窗口与双指针的区别

特性 双指针 滑动窗口
指针移动方向 可以同向或反向 通常同向移动
窗口大小 不固定 可变或固定
应用场景 数组、链表、字符串等 子数组、子字符串问题
典型问题 两数之和、反转链表 最短/最长子数组、无重复字符子串
时间复杂度 通常 O(n) 通常 O(n)

6. 滑动窗口的示例

示例 1:无重复字符的最长子字符串

给定一个字符串,找到不包含重复字符的最长子字符串的长度。

#include 
#include 
#include 
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 
#include 
#include 
#include 
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))。
  • 通过掌握滑动窗口的模板和典型问题,可以高效解决许多与子数组或子字符串相关的算法问题。

你可能感兴趣的:(算法题,c++,双指针,滑动窗口)