转载声明:文章来源https://blog.csdn.net/love666666shen/article/details/122707179
概述
算法面试过程中,经常会遇到求解满足某种条件的子串问题,对于这种类型的题,一般可以使用双指针或滑动窗口解答,滑动窗口问题可以认为是一种特殊的双指针。
什么是滑动窗口
在学习计算机网络时,在TCP协议中,为了进行拥塞控制,提出使用滑动窗口进行优化。
滑动窗口,顾名思义是使用一个大小可变的窗口,通过控制窗口左右两端移动的方向和移动步调,来达到找出要查找子序列的目的。左右两端点一般是向前滑动,可以是右端固定时,左端向前滑动;或者左端固定时,右端向前滑动。
滑动窗口法,可以用来解决一些查找满足一定条件的连续区间的性质的问题。由于区间连续,因此当区间发生变化时,可以通过旧有的计算结果对搜索空间进行剪枝,这样便减少了重复计算,降低了时间复杂度。
使用场景
滑动窗口法常用于求解满足某种条件的某段连续区间的最短或最长子序列(一般为子数组、子字符串等),如:
1)最小摘要
2)和大于给定目标值的最短子序列
3)无重复字符的最长子串
4)有k个不同字符的子串
滑动窗口的重要性质是:窗口的左边界和右边界永远只能向右移动,而不能向左移动。这是为了保证滑动窗口的时间复杂度是 O(n)O(n)。如果左右边界向左移动的话,这叫做“回溯”,算法的时间复杂度就可能不止 O(n)O(n)
算法思想
滑动窗口问题可以想象成队列,一端在push元素,另一端在pop元素,如下所示:
假设有数组[a b c d e f g h]
一个大小为3的滑动窗口在其上滑动,则有:
[a b c]
[b c d]
[c d e]
[d e f]
[e f g]
[f g h]
算法解题步骤如下:
1、声明左右两个指针left和right,初始时都指向起始位置 left = right = 0。
2、满足不了条件是, right 指针不停地后移以扩大窗口 [left, right]接近目标,直到窗口中的序列符合要求。
3、找到一个符合要求的子序列时,停止移动 right的值,转而不断移动左端 left 指针以缩小窗口 [left, right],直到窗口中的序列不再符合要求。同时,每次增加 left前,都要更新一轮结果。
4、重复第 2 和第 3 步,直到 right 到达序列的尽头。
第 2 步相当于在寻找一个可行解,然后第 3 步在优化这个可行解,最终找到最优解。左右指针轮流前进,窗口大小增增减减,窗口不断向右滑动。
算法模板
如何使用滑动窗口问题,需要思考如下两个问题:
第一个问题,窗口何时扩大,何时缩小?
第二个问题,如何移动窗口左右两端,以找到全部的解?
下面以在int类型的数组arr中,查找和为target的子序列为例,总结滑动窗口的做题模板如下:
func template(arr []int, target int) {
var (
// 子序列
path []int
// 当前子序列的和
sum int
)
# 初始化滑动窗口两端(根据具体情况,对左右边界赋初始值)
left, right := 0, 0
// 循环遍历
for left < len(arr) {
// 没找到符合条件的子序列,根据情况后移左右端指针位置,以扩大或缩小窗口范围查找出满足条件的子序列
if sum < target {
sum += arr[right]
right++
} else if sum > target {
// 当前和大于目标值,通过缩小窗口减少总和,左边界后移
sum -= arr[left]
left++
} else {
// 找到和为目标值的子序列,放入新开辟的path切片中
path := make([]int, 0)
// 将[left, right)范围内的子序列放入path切片中
for i := left; i < right; i++ {
path = append(path, arr[i])
}
// 找到一个可行解,更新结果值
res = append(res, path)
// 找到一个符合条件的子序列后,查找下一个子序列时,从和中去除最左侧元素,
// 从左侧元素的下一个位置开始再查找符合条件的下一个子序列,因此先加left,然后left再后移
sum -= arr[left]
left++
}
}
}
模板只是一个解题思路,具体的题目可能需要具体分析,但是大体框架是不变的。
题目示例
剑指 Offer 57 - II. 和为s的连续正数序列
题目难度:简单
题目链接:https://leetcode-cn.com/problems/he-wei-sde-lian-xu-zheng-shu-xu-lie-lcof/
题目描述
输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。
序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。
示例 1:
输入:target = 9
输出:[[2,3,4],[4,5]]
示例 2:
输入:target = 15
输出:[[1,2,3,4,5],[4,5,6],[7,8]]
限制:
1 <= target <= 10^5
算法实现
使用left、right两个指针分别指向当前子序列起始位置,并累积当前窗口范围内的和。
如何窗口的和小于 target 的时候,窗口的和需要增加,可以通过扩大窗口的范围增加,因此累加当前右边界对应元素后,窗口的右边界向右移动。
当窗口的和大于 target 的时候,窗口的和需要减少,通过缩小窗口以较少和的值,因此在减去当前左侧边界对应位置的元素后,窗口的左边界向右移动
当窗口的和恰好等于 target 的时候,需要将当前窗口为 [i, j)范围内的子序列添加到结果集中。
找到一个 left 开头的子序列后,接下来查找下一个满足条件的子序列。可以从left之后的序列查找,因此从和sum中减去left左边界对应元素的值更新当前子序列的和之后,从left+1 开头的序列查找,所以窗口的左边界要向右移动。
// 使用滑动窗口解决和为target的连续子序列问题
func findContinuousSequence(target int) [][]int {
var (
// res为满足条件的子序列集合
res [][]int
// sum为当前子序列的和
sum int
)
// left、right为滑动窗口的左右边界
left, right := 1, 1
// 由于子序列至少包含2个元素,因此左边界小于等于目标值的一半
for left <= target/2 {
// 当前和小于目标值,通过扩大窗口范围增加,右边界后移
if sum < target {
sum += right
right++
} else if sum > target {
// 当前和大于目标值,通过缩小窗口减少总和,左边界后移
sum -= left
left++
} else {
// 找到和为目标值的子序列,放入新开辟的path切片中
path := make([]int, 0)
// 将[left, right)范围内的子序列放入path切片中
for i := left; i < right; i++ {
path = append(path, i)
}
// 找到一个可行解,更新结果值
res = append(res, path)
// 找到一个符合条件的子序列后,查找下一个子序列时,从和中去除最左侧元素,
// 从左侧元素的下一个位置开始再查找符合条件的下一个子序列,因此先加left,然后left再后移
sum -= left
left++
}
}
return res
}
帖子还没人回复快来抢沙发