一文带你吃透滑动窗口问题

271 阅读10分钟

一、引言

滑动数组算法是一种在计算机科学中广泛应用的算法,主要用于处理与数组相关的问题。它通过滑动窗口的方式,在数组中选取连续的子数组,并对其进行操作。本文将介绍滑动数组算法的原理、应用和实现方法,以及面试中常见的滑动窗口题目。

二、滑动数组算法原理

滑动数组算法的核心思想是通过滑动窗口的方式,对数组中的子数组进行操作。具体来说,算法将数组划分为若干个子数组,每个子数组的长度固定。然后,算法依次对每个子数组进行操作,直到处理完整个数组。

滑动窗口的长度可以根据具体问题来设定,可以是固定的,也可以是动态的。在处理过程中,滑动窗口会在数组中左右滑动,选取连续的子数组进行操作。这种处理方式可以有效地减少计算量和时间复杂度,提高算法的效率。

三、滑动数组算法应用

滑动数组算法在许多领域都有广泛的应用,如计算机视觉、机器学习、数据挖掘等。以下是一些具体的应用场景:

  1. 图像处理:滑动窗口算法可以用于图像分割、特征提取等任务。通过滑动窗口的方式,可以对图像中的每个子区域进行处理,提取出有用的特征信息。
  2. 机器学习:滑动窗口算法可以用于分类、聚类等任务。通过对连续子序列进行处理,可以提取出数据中的特征,为后续的分类或聚类操作提供支持。
  3. 数据挖掘:滑动窗口算法可以用于频繁项集挖掘、关联规则挖掘等任务。通过滑动窗口的方式,可以对数据中的子序列进行频繁项集的挖掘和关联规则的推导。

四、滑动数组算法实现

常见的实现方式:

  1. 初始化滑动窗口:根据问题需求,设定滑动窗口的长度和起始位置。
  2. 构建滑动窗口:根据滑动窗口的长度和起始位置,构建初始的滑动窗口。
  3. 移动滑动窗口:每次将滑动窗口向右移动一个位置,同时更新滑动窗口中的元素。
  4. 对滑动窗口中的元素进行操作:根据问题需求,对滑动窗口中的元素进行相应的操作。
  5. 重复步骤3和4,直到处理完整个数组。

五、面试中的滑动窗口问题

1. 如何识别滑动窗口问题?

主要从以下几个方面进行判断:

  1. 问题的描述:滑动窗口问题通常涉及到对一个数组或列表进行处理,要求找出满足某种条件的子序列或子数组。问题描述中可能会涉及到“连续子序列”、“固定长度窗口”、“最长”、“最短”、“滑动”等关键词。
  2. 问题的约束条件:滑动窗口问题通常具有一些特定的约束条件,如窗口的长度固定、元素的值范围有限等。这些约束条件可以帮助我们判断是否适合使用滑动窗口算法。
  3. 问题的复杂度要求:滑动窗口问题通常需要对数组或列表进行高效的处理,因此需要关注问题的复杂度要求。如果问题的复杂度要求较高,使用滑动窗口算法可以有效地降低时间复杂度,提高算法的效率。
  4. 问题的解法:对于滑动窗口问题,通常需要采用双指针或哈希表等技巧进行解法设计。如果问题可以通过双指针或哈希表等技巧进行解决,那么很可能是滑动窗口问题。

关键词

  • 满足xx条件
  • 最长最短
  • 子序列子数组子串

2. 滑动窗口问题的分类?

  1. 根据滑动窗口的形状分类:

    • 线性滑动窗口:滑动窗口在数组中线性移动,每次移动一个元素。
    • 矩形滑动窗口:滑动窗口在数组中以矩形形状移动,可以同时移动多个元素。
  2. 根据滑动窗口的目标值分类:

    • 求最大值滑动窗口:滑动窗口内元素的最大值即为问题答案。
    • 求最小值滑动窗口:滑动窗口内元素的最小值即为问题答案。
    • 求平均值滑动窗口:滑动窗口内元素的平均值即为问题答案。
  3. 根据滑动窗口的移动方式分类:

    • 双端队列滑动窗口:使用双端队列维护滑动窗口内的元素,可以快速找到窗口内的最大值或最小值。
    • 哈希表滑动窗口:使用哈希表记录每个元素在滑动窗口中的位置,可以快速查找元素是否在滑动窗口内。
  4. 根据滑动窗口的应用场景分类:

    • 寻找最长的子序列:通过滑动窗口找到数组中最长的连续子序列。
    • 寻找最短的子序列:通过滑动窗口找到数组中最短的连续子序列。
    • 频繁项集挖掘:通过滑动窗口找到数组中频繁出现的元素或子序列。

3. 滑动窗口问题的通用解题模板

下面我统一用双指针的方式

寻找最长子序列

思路

  • 初始化左右双指针(LR)在起始点,R向右逐位滑动循环

  • 每次的滑动过程中:

    • 如果窗口内元素满足条件,R向右扩大窗口,并更新最优结果
    • 窗口内元素不满足条件,L向右缩小窗口
  • 终止条件:R到达数组末尾

伪代码

function () {
    初始化left,right,result,bestResult  
    while (右指针没有到结尾) {  
      while (result不满足要求) {  
        窗口缩小,移除left对应元素,left右移  
      }  
      更新最优结果bestResult  
      right++  
    }  
    return bestResult
}

寻找最短子序列

思路

  • 初始化左右双指针(LR)在起始点,R向右逐位滑动循环

  • 每次的滑动过程中:

    • 如果窗口内元素满足条件,L向右缩小窗口,并更新最优结果
    • 窗口内元素不满足条件,R向右扩大窗口
  • 终止条件:R到达数组末尾

伪代码

function () {
    初始化left,right,result,bestResult  
    while (右指针没有到结尾) {  
      while (result满足要求) {  
        更新最优结果bestResult  
        窗口缩小,移除left对应元素,left右移  
      }  
      right++  
    }  
    return bestResult
}

4. 滑动窗口面试真题

无重复字符的最长子串

难度:Medium

真题链接leetcode.cn/problems/lo…

题目描述: 给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

示例 1:

输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:

输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
     请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列, 不是子串。

提示:

  • 0 <= s.length <= 5 * 104
  • s 由英文字母、数字、符号和空格组成

题解

var lengthOfLongestSubstring = function (s) {
  let left = 0, right = 0 // 初始化左右指针
  let set = new Set() // set集合帮助验证子串中是否有重复
  let ans = 0 // 最终结果
  const len = s.length // 缓存字符串长度
  // 右指针慢慢往右走
  while (right < len) {
    // 如果子串中出现了重复的字符
    // 不断删除集合中的字符,直至子串不重复
    // 每次删除都向右移动left指针
    while (set.has(s.charAt(right))) {
      set.delete(s.charAt(left))
      left++
    }
    // 子串中没有重复的字符
    // 在set集合中记录right所指的字符
    // 更新结果
    // 右指针右移一位
    set.add(s.charAt(right))
    ans = Math.max(ans, set.size)
    right++
  }
  return ans // 返回最终结果
}

时间复杂度

  1. right 指针每次循环会移动一步,因此它遍历整个字符串的次数是 len
  2. left 指针在遇到重复字符时,会逐步向右移动以删除重复的字符。在最坏的情况下,left 指针可能会向右移动到末尾,因此它遍历整个字符串的次数也是 len
  3. set.has(s.charAt(right)) 和 set.delete(s.charAt(left)) 的时间复杂度都是 O(1)。
  4. set.add(s.charAt(right)) 的时间复杂度也是 O(1)。
  5. Math.max(ans, set.size) 的时间复杂度是 O(1)。

因此,总的时间复杂度是 O(n),其中 n 是字符串的长度。

空间复杂度

  1. O(∣Σ∣),其中 Σ 表示字符集(即字符串中可以出现的字符),∣Σ∣ 表示字符集的大小。在本题中没有明确说明字符集,因此可以默认为所有 ASCII 码在 [0,128) 内的字符,即 ∣Σ∣=128。我们需要用到哈希集合来存储出现过的字符,而字符最多有 ∣Σ∣ 个,因此空间复杂度为 O(∣Σ∣)。

  2. 其他变量(如 leftrightanslen)的空间复杂度都是 O(1)。

长度最小的子数组

难度:Medium

真题链接leetcode.cn/problems/mi…

题目描述: 给定一个含有 n ****个正整数的数组和一个正整数 target

找出该数组中满足其总和大于等于 ****target ****的长度最小的 连续子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度 如果不存在符合条件的子数组,返回 0 。

示例 1:

输入: target = 7, nums = [2,3,1,2,4,3]
输出: 2
解释: 子数组 [4,3] 是该条件下的长度最小的子数组。

示例 2:

输入: target = 4, nums = [1,4,4]
输出: 1

示例 3:

输入: target = 11, nums = [1,1,1,1,1,1,1,1]
输出: 0

提示:

  • 1 <= target <= 109
  • 1 <= nums.length <= 105
  • 1 <= nums[i] <= 105

题解

var minSubArrayLen = function (target, nums) {
  // 初始化左右指针、存储求和值的变量、最终结果
  let left = 0, right = 0, sum = 0, ans = 0
  const len = nums.length // // 缓存字符串长度
  // 右指针慢慢往右走
  while (right < len) {
    sum += nums[right] // 求和
    // 如果当前子数组的和大于等于target
    // 计算当前子数组的length是否比之前子数组的length要小
    // 或者是ans的值是0时(代表第一次满足条件)
    while (sum >= target) {
      // 更新ans的值
      if (right - left + 1 < ans || ans === 0) {
        ans = right - left + 1
      }
      // 寻找下一个满足条件的子数组
      // 所以要sum减去数组最开头的元素
      // 左指针移动一位
      sum -= nums[left]
      left++
    }
    // 没有符合条件的子数组了,右指针往右移动一位扩大范围
    right++
  }
  return ans // 返回最终结果
}

时间复杂度

  1. 右指针 right 在主循环中每次都会移动一步,所以它遍历整个数组的次数是 len
  2. 左指针 left 在满足条件的情况下会逐步向右移动,因此它遍历整个数组的次数也是 len
  3. sum += nums[right] 和 sum -= nums[left] 的时间复杂度都是 O(1)。
  4. ans = right - left + 1 和 ans = Math.min(ans, right - left + 1) 的时间复杂度都是 O(1)。

因此,总的时间复杂度是 O(len)。

空间复杂度

  1. 变量 leftrightsum, 和 ans 的空间复杂度都是 O(1)。
  2. 除了这些变量外,代码中没有使用额外的数据结构,因此空间复杂度是 O(1)。

因此,总的空间复杂度是 O(1)。