一、引言
滑动数组算法是一种在计算机科学中广泛应用的算法,主要用于处理与数组相关的问题。它通过滑动窗口的方式,在数组中选取连续的子数组,并对其进行操作。本文将介绍滑动数组算法的原理、应用和实现方法,以及面试中常见的滑动窗口题目。
二、滑动数组算法原理
滑动数组算法的核心思想是通过滑动窗口的方式,对数组中的子数组进行操作。具体来说,算法将数组划分为若干个子数组,每个子数组的长度固定。然后,算法依次对每个子数组进行操作,直到处理完整个数组。
滑动窗口的长度可以根据具体问题来设定,可以是固定的,也可以是动态的。在处理过程中,滑动窗口会在数组中左右滑动,选取连续的子数组进行操作。这种处理方式可以有效地减少计算量和时间复杂度,提高算法的效率。
三、滑动数组算法应用
滑动数组算法在许多领域都有广泛的应用,如计算机视觉、机器学习、数据挖掘等。以下是一些具体的应用场景:
- 图像处理:滑动窗口算法可以用于图像分割、特征提取等任务。通过滑动窗口的方式,可以对图像中的每个子区域进行处理,提取出有用的特征信息。
- 机器学习:滑动窗口算法可以用于分类、聚类等任务。通过对连续子序列进行处理,可以提取出数据中的特征,为后续的分类或聚类操作提供支持。
- 数据挖掘:滑动窗口算法可以用于频繁项集挖掘、关联规则挖掘等任务。通过滑动窗口的方式,可以对数据中的子序列进行频繁项集的挖掘和关联规则的推导。
四、滑动数组算法实现
常见的实现方式:
- 初始化滑动窗口:根据问题需求,设定滑动窗口的长度和起始位置。
- 构建滑动窗口:根据滑动窗口的长度和起始位置,构建初始的滑动窗口。
- 移动滑动窗口:每次将滑动窗口向右移动一个位置,同时更新滑动窗口中的元素。
- 对滑动窗口中的元素进行操作:根据问题需求,对滑动窗口中的元素进行相应的操作。
- 重复步骤3和4,直到处理完整个数组。
五、面试中的滑动窗口问题
1. 如何识别滑动窗口问题?
主要从以下几个方面进行判断:
- 问题的描述:滑动窗口问题通常涉及到对一个数组或列表进行处理,要求找出满足某种条件的子序列或子数组。问题描述中可能会涉及到“连续子序列”、“固定长度窗口”、“最长”、“最短”、“滑动”等关键词。
- 问题的约束条件:滑动窗口问题通常具有一些特定的约束条件,如窗口的长度固定、元素的值范围有限等。这些约束条件可以帮助我们判断是否适合使用滑动窗口算法。
- 问题的复杂度要求:滑动窗口问题通常需要对数组或列表进行高效的处理,因此需要关注问题的复杂度要求。如果问题的复杂度要求较高,使用滑动窗口算法可以有效地降低时间复杂度,提高算法的效率。
- 问题的解法:对于滑动窗口问题,通常需要采用双指针或哈希表等技巧进行解法设计。如果问题可以通过双指针或哈希表等技巧进行解决,那么很可能是滑动窗口问题。
关键词:
满足xx条件最长、最短子序列、子数组、子串
2. 滑动窗口问题的分类?
-
根据滑动窗口的形状分类:
- 线性滑动窗口:滑动窗口在数组中线性移动,每次移动一个元素。
- 矩形滑动窗口:滑动窗口在数组中以矩形形状移动,可以同时移动多个元素。
-
根据滑动窗口的目标值分类:
- 求最大值滑动窗口:滑动窗口内元素的最大值即为问题答案。
- 求最小值滑动窗口:滑动窗口内元素的最小值即为问题答案。
- 求平均值滑动窗口:滑动窗口内元素的平均值即为问题答案。
-
根据滑动窗口的移动方式分类:
- 双端队列滑动窗口:使用双端队列维护滑动窗口内的元素,可以快速找到窗口内的最大值或最小值。
- 哈希表滑动窗口:使用哈希表记录每个元素在滑动窗口中的位置,可以快速查找元素是否在滑动窗口内。
-
根据滑动窗口的应用场景分类:
- 寻找最长的子序列:通过滑动窗口找到数组中最长的连续子序列。
- 寻找最短的子序列:通过滑动窗口找到数组中最短的连续子序列。
- 频繁项集挖掘:通过滑动窗口找到数组中频繁出现的元素或子序列。
3. 滑动窗口问题的通用解题模板
下面我统一用双指针的方式
寻找最长子序列
思路:
-
初始化左右双指针(
L、R)在起始点,R向右逐位滑动循环 -
每次的滑动过程中:
- 如果窗口内元素满足条件,
R向右扩大窗口,并更新最优结果 - 窗口内元素不满足条件,
L向右缩小窗口
- 如果窗口内元素满足条件,
-
终止条件:
R到达数组末尾
伪代码:
function () {
初始化left,right,result,bestResult
while (右指针没有到结尾) {
while (result不满足要求) {
窗口缩小,移除left对应元素,left右移
}
更新最优结果bestResult
right++
}
return bestResult
}
寻找最短子序列
思路:
-
初始化左右双指针(
L、R)在起始点,R向右逐位滑动循环 -
每次的滑动过程中:
- 如果窗口内元素满足条件,
L向右缩小窗口,并更新最优结果 - 窗口内元素不满足条件,
R向右扩大窗口
- 如果窗口内元素满足条件,
-
终止条件:
R到达数组末尾
伪代码:
function () {
初始化left,right,result,bestResult
while (右指针没有到结尾) {
while (result满足要求) {
更新最优结果bestResult
窗口缩小,移除left对应元素,left右移
}
right++
}
return bestResult
}
4. 滑动窗口面试真题
无重复字符的最长子串
难度:Medium
题目描述:
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列, 不是子串。
提示:
0 <= s.length <= 5 * 104s由英文字母、数字、符号和空格组成
题解:
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 // 返回最终结果
}
时间复杂度:
right指针每次循环会移动一步,因此它遍历整个字符串的次数是len。left指针在遇到重复字符时,会逐步向右移动以删除重复的字符。在最坏的情况下,left指针可能会向右移动到末尾,因此它遍历整个字符串的次数也是len。set.has(s.charAt(right))和set.delete(s.charAt(left))的时间复杂度都是 O(1)。set.add(s.charAt(right))的时间复杂度也是 O(1)。Math.max(ans, set.size)的时间复杂度是 O(1)。
因此,总的时间复杂度是 O(n),其中 n 是字符串的长度。
空间复杂度:
-
O(∣Σ∣),其中 Σ 表示字符集(即字符串中可以出现的字符),∣Σ∣ 表示字符集的大小。在本题中没有明确说明字符集,因此可以默认为所有 ASCII 码在 [0,128) 内的字符,即 ∣Σ∣=128。我们需要用到哈希集合来存储出现过的字符,而字符最多有 ∣Σ∣ 个,因此空间复杂度为 O(∣Σ∣)。
-
其他变量(如
left,right,ans,len)的空间复杂度都是 O(1)。
长度最小的子数组
难度:Medium
题目描述:
给定一个含有 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 <= 1091 <= nums.length <= 1051 <= 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 // 返回最终结果
}
时间复杂度:
- 右指针
right在主循环中每次都会移动一步,所以它遍历整个数组的次数是len。 - 左指针
left在满足条件的情况下会逐步向右移动,因此它遍历整个数组的次数也是len。 sum += nums[right]和sum -= nums[left]的时间复杂度都是 O(1)。ans = right - left + 1和ans = Math.min(ans, right - left + 1)的时间复杂度都是 O(1)。
因此,总的时间复杂度是 O(len)。
空间复杂度:
- 变量
left,right,sum, 和ans的空间复杂度都是 O(1)。 - 除了这些变量外,代码中没有使用额外的数据结构,因此空间复杂度是 O(1)。
因此,总的空间复杂度是 O(1)。