系列博客目录
文章目录
理论知识
滑动窗口(Sliding Window)是一种常用的算法技巧,特别适用于在数组或字符串中查找符合条件的子序列或子数组。滑动窗口的基本思想是使用一个窗口(即一段连续的区间)来扫描整个数据结构,并通过动态调整窗口的大小和位置来逐步得出结果。
##滑动窗口算法总结
1. 基本概念
- 窗口大小固定:在滑动窗口中,窗口的大小固定,每次窗口滑动一步。
- 窗口大小可变:有时,窗口大小是动态调整的,通过扩展或收缩窗口来满足特定条件。
2. 滑动窗口的类型
- 固定窗口大小:滑动窗口的大小在整个过程中保持不变。
- 动态窗口大小:窗口大小根据某些条件进行扩展或收缩,直到满足题目的要求。
3. 适用场景
- 子数组或子字符串问题:求解给定条件下,子数组或子字符串的最值、最长子序列、最小子序列等问题。
- 在线算法:当需要一次遍历整个数据结构,并且有某些局部性质需要被保持或更新时。
4. 常见的滑动窗口问题类型
1) 最小/最大长度子数组
- 题目:给定一个整数数组,求和大于或小于等于某个目标的最小子数组长度。
- 解法:可以通过调整窗口的左右边界来扩展或收缩窗口,直到满足条件。
2) 最长子串/子数组
- 题目:给定一个字符串或数组,要求找出满足某些条件的最长子串/子数组。
- 解法:通过右指针扩展窗口,左指针收缩,保持窗口内满足条件的最大区间。
3) 包含所有字符的最小窗口
- 题目:给定一个字符串 S 和一个目标字符串 T,找出最小的窗口子串,包含目标字符串 T 的所有字符。
- 解法:使用两个指针来形成窗口,动态调整窗口的大小,并通过哈希表来记录窗口内字符的出现情况。
4) 重复字符或子数组
- 题目:给定一个字符串或数组,求不包含重复字符的最长子串或子数组。
- 解法:使用滑动窗口(动态窗口)和哈希表记录每个字符的最后出现位置,通过右指针扩展窗口,左指针根据重复字符的出现位置调整。
5. 常见的滑动窗口解法模板
动态窗口大小
def slidingWindow(nums):
left = 0
result = float('inf') # 或其他合适的初始值
for right in range(len(nums)):
# 窗口扩展:操作 right,加入新的元素
while condition: # 满足题意条件时
# 更新窗口内的结果
result = min(result, current_window_value)
# 窗口收缩:操作 left,移除不再符合条件的元素
left += 1
return result
固定窗口大小
def slidingWindow(nums):
window_sum = 0
for i in range(len(nums)):
window_sum += nums[i] # 加入当前元素到窗口
if i >= window_size - 1:
# 当前窗口的大小达到要求,进行处理
# 这里可以做计算,更新结果
window_sum -= nums[i - window_size + 1] # 移出窗口最左边的元素
return result
6. 经典题目与解法
-
最小覆盖子串(Minimum Window Substring)
- 给定字符串
S
和目标字符串T
,找出最小的窗口,包含 T 中的所有字符。 - 解法:滑动窗口+哈希表记录字符频率。
- 给定字符串
-
最大连续子数组和(Maximum Subarray Sum)
- 给定一个整数数组,找到一个具有最大和的连续子数组。
- 解法:动态调整窗口大小(滑动窗口)并计算窗口内的和。
-
无重复字符的最长子串(Longest Substring Without Repeating Characters)
- 给定一个字符串,找出最长的不包含重复字符的子字符串。
- 解法:滑动窗口+哈希表。
-
子数组和为 k(Subarray Sum Equals K)
- 给定一个整数数组,找出其和为
k
的子数组的数量。 - 解法:使用滑动窗口和前缀和。
- 给定一个整数数组,找出其和为
7. 时间复杂度
滑动窗口的最大优势是它可以将复杂的暴力算法优化到 O(n) 时间复杂度。通常情况下,窗口内的元素总共会被遍历一次(每个元素最多被访问两次,左指针和右指针各一次)。因此,滑动窗口算法的时间复杂度通常是 O(n),其中 n 是数组或字符串的长度。
例题
3. 无重复字符的最长子串
问题描述:
给定一个字符串 s
,找出其中不含有重复字符的最长子串的长度。
示例:
-
输入:
s = "abcabcbb"
输出:3
解释: 无重复字符的最长子串是"abc"
,所以其长度为3
。 -
输入:
s = "bbbbb"
输出:1
解释: 无重复字符的最长子串是"b"
,所以其长度为1
。 -
输入:
s = "pwwkew"
输出:3
解释: 无重复字符的最长子串是"wke"
,所以其长度为3
。
注意:"pwke"
是子序列而非子串,因此不计入。
提示:
0 <= s.length <= 5 * 10^4
s
由英文字母、数字、符号和空格组成
题解
思路就是最长的无重复字符串前部分是一个相对短的无重复字符串,我们通过一个可以变大的滑动窗口,首先通过滑动窗口包含小的无重复字符串,然后在针对字符串遍历的过程中,不断扩充滑动窗口,直到滑动窗口的长度等于最大的无重复字符串。具体操作就是遍历到一个新的字符,就查看这个字符是否有重复字符在窗口中,然后动态调整窗口大小。如果包含(用hashmap求得),则把左边的指针变为上一个重复字符的位置+1。
class Solution {
public int lengthOfLongestSubstring(String s) {
int Longest = 0;
int left = 0;
int right = 0;
char[] sArray = s.toCharArray();
Map<Character,Integer> map = new HashMap<>();
while(right<s.length()){
if(map.containsKey(sArray[right])&&map.get(sArray[right])>=left){
left = map.get(sArray[right]);//忘了加1
}else{
map.put(sArray[right],right);//不应该在else中,因为不管是不是包含重复,都要更新每个字符的最大位置所在。
}
right++;
Longest= Math.max(Longest,right-left);
}
return Longest;
}
}
修改上面错误代码后。
class Solution {
public int lengthOfLongestSubstring(String s) {
int Longest = 0;
int left = 0;
int right = 0;
char[] sArray = s.toCharArray();
Map<Character,Integer> map = new HashMap<>();
while(right<s.length()){
if(map.containsKey(sArray[right])&&map.get(sArray[right])>=left){
left = map.get(sArray[right])+1;
}
map.put(sArray[right],right);
right++;
Longest= Math.max(Longest,right-left);
}
return Longest;
}
}
209. 长度最小的子数组 中等
问题描述:
给定一个含有 n
个正整数的数组 nums
和一个正整数 target
,找出该数组中满足其总和大于等于 target
的长度最小的子数组,并返回其长度。如果不存在符合条件的子数组,返回 0
。
示例:
-
输入:
target = 7
,nums = [2, 3, 1, 2, 4, 3]
输出:2
解释: 子数组[4, 3]
是该条件下的长度最小的子数组。 -
输入:
target = 4
,nums = [1, 4, 4]
输出:1
-
输入:
target = 11
,nums = [1, 1, 1, 1, 1, 1, 1, 1]
输出:0
提示:
1 <= target <= 10^9
1 <= nums.length <= 10^5
1 <= nums[i] <= 10^4
进阶:
- 如果你已经实现了 O(n) 时间复杂度的解法,请尝试设计一个 O(n log n) 时间复杂度的解法。
题解
思路就是应用滑动窗口,遍历数组,每次添加新的一个数字,当找到了满足大于target的子数组后,我们可以从滑动窗口的前面部分移除适当的数字,来使其仍满足大于target,但是减少了长度。
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int left = 0;
int right = 0;
int sum = 0;
int shortest = Integer.MAX_VALUE;
while(right < nums.length){
sum+= nums[right++];
if(sum>=target){
while(true){
sum-=nums[left++];
if(sum<target){
shortest = Math.min(shortest,right-left+1);
break;
}
}
}
}
return shortest == Integer.MAX_VALUE? 0:shortest;//注意返回值,来处理数组中所有的数值加起来都达不到target的情况。
}
}
滑动窗口和双指针的关系
滑动窗口(Sliding Window)和双指针(Two Pointers)有着密切的关系,它们在许多算法问题中是相互交替使用的技巧。二者的核心思想都是通过两个指针来扫描数据结构,动态调整指针位置来满足特定条件。下面是它们的关系和区别:
1. 双指针(Two Pointers)
- 定义:双指针是指在一个数据结构(通常是数组或字符串)中使用两个指针来进行遍历操作。这两个指针可以是从两端向中间移动(对撞指针),也可以是从同一端一起向前推进(如快慢指针)。
- 典型应用:两数之和(Two Sum),判断回文串,合并有序数组等。
2. 滑动窗口(Sliding Window)
- 定义:滑动窗口是一种特殊的双指针技巧,通常用于查找子数组或子串。它通过动态调整窗口的大小(即指针的位置)来满足特定条件。滑动窗口可以是固定大小,也可以是动态调整的。
- 典型应用:无重复字符的最长子串,长度最小的子数组等。
3. 滑动窗口和双指针的关系
-
本质上都是双指针技巧:滑动窗口是双指针技术的一种具体应用。当我们在滑动窗口问题中使用“左指针”和“右指针”来表示窗口的边界时,实际上就是在应用双指针技巧。
-
滑动窗口是双指针的一种特殊情况:双指针可以用于其他算法(例如:合并有序数组),而滑动窗口则通常用于查找符合某种条件的子数组或子串。滑动窗口中的两个指针常常用来维护一个满足条件的区间或窗口。
4. 两者的相似性与区别
-
相似性:
- 都是通过维护两个指针来扫描整个数组(或字符串),并通过调整指针的位置来优化算法。
- 都可以通过一遍遍历来解决问题(通常是 O(n) 时间复杂度)。
-
区别:
- 滑动窗口:指针移动时,窗口的大小会变化,可以扩展也可以收缩,通常用于动态变化的区间。
- 双指针:不仅限于窗口大小变化,有时指针之间的距离不变(如快慢指针问题),而是通过指针间的相对位置来解决问题。
5. 示例对比
双指针:
- 问题:两数之和(Two Sum II)
- 解法:使用两个指针,分别指向数组的左右两端,计算当前和并根据和与目标值的大小调整指针。
def twoSum(nums, target):
left, right = 0, len(nums) - 1
while left < right:
total = nums[left] + nums[right]
if total == target:
return [left, right]
elif total < target:
left += 1
else:
right -= 1
return []
滑动窗口:
- 问题:无重复字符的最长子串
- 解法:使用两个指针维护一个窗口,左指针指向窗口的开始,右指针扩展窗口,保持窗口内没有重复字符。
def lengthOfLongestSubstring(s):
left = 0
max_len = 0
seen = {}
for right in range(len(s)):
if s[right] in seen and seen[s[right]] >= left:
left = seen[s[right]] + 1
seen[s[right]] = right
max_len = max(max_len, right - left + 1)
return max_len
6. 总结
- 滑动窗口可以看作是一种特殊的双指针技术,主要用于动态维护子数组或子串的区间。
- 双指针技术更为通用,可以用于很多不同类型的问题,而滑动窗口更专注于处理那些涉及到区间长度、最值、满足某些条件的子数组的问题。