写在开头的话
本文旨在通过实践展示滑动窗口技术的应用,特别是在处理数组或字符串时的高效查找和统计操作。主要包括在给定的数组或字符串中找到满足特定条件的连续子数组或子字符串,旨在提高算法的时间效率和理解窗口滑动的原理与实现。
第一节
知识点:
(1)滑动窗口的原理(2)最大连续子数组和(3)最小覆盖子串(4)字符串的排列(5)最长连续不重复子串
滑动窗口
滑动窗口的原理
滑动窗口是一种高效解决数组或字符串子问题的技术,本质是双指针算法。它通过在输入序列上维护一个动态范围(窗口),逐步向右移动并更新窗口内容,以便在每一步都能获得所需的信息。滑动窗口通常用于查找满足某些条件的最长或最短子数组或子字符串,优化了时间复杂度,避免了嵌套循环。接下来我们直接从习题理解滑动窗口的思想。
最大连续子数组和
问题描述
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
思路讲解
以序列 [−2,1,−3,4,−1,2,1,−5,4] 为例:
- 为求出最大连续的子数组和,我们逻辑上假设有一个窗口在原数组上滑动。
- 当前窗口元素的和小于 0 时,我们直接舍弃开窗口,重新开辟新的窗口。
- 记录所有开辟窗口的最大值。


- −2 比 0 小,重新开辟窗口。
- 1 比 0 大,记录下最大值 1。
- 1 + (−3) 比 0 小,重新开辟窗口。
- 4 比 0 大,记录下最大值 4。
- 4 + (-1) 比 0 大,最大值依旧为 4。
- 4 + (-1)+2 比 0 大,记录下最大值 5。
- 4 + (-1)+2+1比 0 大,记录下最大值 6。
- 4 + (-1)+2+1−5 比 0 大,最大值依旧为 6。
- 4 + (-1)+2+1−5+4 比 0 大,最大值依旧为 6。
结果为 6。
代码实现
C++代码实现
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int maxSubArraySum(vector<int>& nums) {
int max_so_far = nums[0];
int max_ending_here = nums[0];
for (int i = 1; i < nums.size(); i++) {
max_ending_here = max(nums[i], max_ending_here + nums[i]);
max_so_far = max(max_so_far, max_ending_here);
}
return max_so_far;
}
// 示例
int main() {
vector<int> arr = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
cout << "Maximum subarray sum is " << maxSubArraySum(arr) << endl;
return 0;
}
Java代码实现
java
public class MaxSubArraySum {
public static int maxSubArraySum(int[] nums) {
int max_so_far = nums[0];
int max_ending_here = nums[0];
for (int i = 1; i < nums.length; i++) {
max_ending_here = Math.max(nums[i], max_ending_here + nums[i]);
max_so_far = Math.max(max_so_far, max_ending_here);
}
return max_so_far;
}
// 示例
public static void main(String[] args) {
int[] arr = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
System.out.println("Maximum subarray sum is " + maxSubArraySum(arr));
}
}
Python代码实现
python
def max_subarray_sum(arr):
max_so_far = arr[0]
max_ending_here = arr[0]
for i in range(1, len(arr)):
max_ending_here = max(arr[i], max_ending_here + arr[i])
max_so_far = max(max_so_far, max_ending_here)
return max_so_far
# 示例
arr = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
print("Maximum subarray sum is", max_subarray_sum(arr))
运行结果

最小覆盖字串
问题描述
给定两个长度为 n,m 的小写字符串 s,t。你需要求出 s 包含 t 所有字符的最短子串长度。
思路讲解
我们用两个指针来维护该问题,对 s 串维护一个窗口,该窗口包含了 t 的全部字符。对于判断是否包含 t 的全部字符, 我们可以用一个 hash 表进行判断,如果该窗口包含哈希表中全部的字符,且每个个数均不小于哈希表各个字符的个数,则当前窗口是可行窗口。
我们以 ABACB 与 ABC 为例。

- 窗口右指针向右移动,到
ABAC时,该窗口包含ABC全部字符。 - 窗口左指针向右移动,到
BAC时,发现该窗口依旧包含ABC全部字符,更新答案。 - 窗口左指针向右移动,到
AC时,发现该窗口不包含ABC全部字符,右指针向右移动一位。 - 最终右指针走到尽头,为
ABC,结束代码。
代码实现
C++代码实现
cpp
#include <iostream>
#include <string>
#include <unordered_map>
using namespace std;
string minWindow(string s, string t) {
if (t.size() > s.size()) return "";
unordered_map<char, int> t_count, window_count;
for (char c : t) t_count[c]++;
int have = 0, need = t_count.size();
int left = 0, min_length = INT_MAX, min_left = 0;
for (int right = 0; right < s.size(); right++) {
char c = s[right];
window_count[c]++;
if (t_count.find(c) != t_count.end() && window_count[c] == t_count[c]) {
have++;
}
while (have == need) {
if (right - left + 1 < min_length) {
min_length = right - left + 1;
min_left = left;
}
window_count[s[left]]--;
if (t_count.find(s[left]) != t_count.end() && window_count[s[left]] < t_count[s[left]]) {
have--;
}
left++;
}
}
return min_length == INT_MAX ? "" : s.substr(min_left, min_length);
}
// 示例
int main() {
string s = "ADOBECODEBANC";
string t = "ABC";
cout << "Minimum window substring is \"" << minWindow(s, t) << "\"" << endl;
return 0;
}
Java代码实现
java
import java.util.HashMap;
public class MinWindowSubstring {
public static String minWindow(String s, String t) {
if (t.length() > s.length()) return "";
HashMap<Character, Integer> t_count = new HashMap<>();
HashMap<Character, Integer> window_count = new HashMap<>();
for (char c : t.toCharArray()) t_count.put(c, t_count.getOrDefault(c, 0) + 1);
int have = 0, need = t_count.size();
int left = 0, min_length = Integer.MAX_VALUE, min_left = 0;
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
window_count.put(c, window_count.getOrDefault(c, 0) + 1);
if (t_count.containsKey(c) && window_count.get(c).intValue() == t_count.get(c).intValue()) {
have++;
}
while (have == need) {
if (right - left + 1 < min_length) {
min_length = right - left + 1;
min_left = left;
}
char leftChar = s.charAt(left);
window_count.put(leftChar, window_count.get(leftChar) - 1);
if (t_count.containsKey(leftChar) && window_count.get(leftChar) < t_count.get(leftChar)) {
have--;
}
left++;
}
}
return min_length == Integer.MAX_VALUE ? "" : s.substring(min_left, min_left + min_length);
}
// 示例
public static void main(String[] args) {
String s = "ADOBECODEBANC";
String t = "ABC";
System.out.println("Minimum window substring is \"" + minWindow(s, t) + "\"");
}
}
Python代码实现
python
def min_window(s, t):
if len(t) > len(s):
return ""
from collections import Counter
t_count = Counter(t)
window_count = Counter()
have, need = 0, len(t_count)
left = 0
min_length = float('inf')
min_left = 0
for right in range(len(s)):
c = s[right]
window_count[c] += 1
if c in t_count and window_count[c] == t_count[c]:
have += 1
while have == need:
if (right - left + 1) < min_length:
min_length = right - left + 1
min_left = left
window_count[s[left]] -= 1
if s[left] in t_count and window_count[s[left]] < t_count[s[left]]:
have -= 1
left += 1
return s[min_left:min_left + min_length] if min_length != float('inf') else ""
# 示例
s = "ADOBECODEBANC"
t = "ABC"
print("Minimum window substring is \"{}\"".format(min_window(s, t)))
运行结果

字符串的排序
问题描述

对于字符串 abcd 来说,acdb 是它的排列,abcc 不是它的排列。
思路讲解
这题让判断 s2 的一个子串是否是 s1 的排列,实际上就是判断 s2 的一个子串是否是 s1 的字母异位词,其实都是一个意思。假设有两个字符串 s 和 t,若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。判断的时候我们需要每次从 s2 中截取和 s1 长度一样的子串,所以这就是一道滑动窗口问题,窗口的长度就是 s1 的长度,是固定的,当窗口长度达到 s1 长度的时候,窗口每往右滑动一步就判断窗口内的字母和 s1 是否互为字母异位词,如下图所示

代码实现
C++代码实现
cpp
class Solution {
public:
bool checkInclusion(string s1, string s2) {
int n = s1.length(), m = s2.length();
if (n > m) {
return false;
}
vector<int> cnt1(26), cnt2(26);
for (int i = 0; i < n; ++i) {
++cnt1[s1[i] - 'a'];
++cnt2[s2[i] - 'a'];
}
if (cnt1 == cnt2) {
return true;
}
for (int i = n; i < m; ++i) {
++cnt2[s2[i] - 'a'];
--cnt2[s2[i - n] - 'a'];
if (cnt1 == cnt2) {
return true;
}
}
return false;
}
};
Java代码实现
java
class Solution {
public boolean checkInclusion(String s1, String s2) {
int n = s1.length(), m = s2.length();
if (n > m) {
return false;
}
int[] cnt1 = new int[26];
int[] cnt2 = new int[26];
for (int i = 0; i < n; ++i) {
++cnt1[s1.charAt(i) - 'a'];
++cnt2[s2.charAt(i) - 'a'];
}
if (Arrays.equals(cnt1, cnt2)) {
return true;
}
for (int i = n; i < m; ++i) {
++cnt2[s2.charAt(i) - 'a'];
--cnt2[s2.charAt(i - n) - 'a'];
if (Arrays.equals(cnt1, cnt2)) {
return true;
}
}
return false;
}
}
Python代码实现
python
class Solution(object):
def checkInclusion(self, s1, s2):
"""
:type s1: str
:type s2: str
:rtype: bool
"""
# 统计 s1 中每个字符出现的次数
counter1 = collections.Counter(s1)
N = len(s2)
# 定义滑动窗口的范围是 [left, right],闭区间,长度与s1相等
left = 0
right = len(s1) - 1
# 统计窗口s2[left, right - 1]内的元素出现的次数
counter2 = collections.Counter(s2[0:right])
while right < N:
# 把 right 位置的元素放到 counter2 中
counter2[s2[right]] += 1
# 如果滑动窗口内各个元素出现的次数跟 s1 的元素出现次数完全一致,返回 True
if counter1 == counter2:
return True
# 窗口向右移动前,把当前 left 位置的元素出现次数 - 1
counter2[s2[left]] -= 1
# 如果当前 left 位置的元素出现次数为 0, 需要从字典中删除,否则这个出现次数为 0 的元素会影响两 counter 之间的比较
if counter2[s2[left]] == 0:
del counter2[s2[left]]
# 窗口向右移动
left += 1
right += 1
return False
最长连续不重复子串
问题描述
给定一个长度为 n 的小写字符串 S。请找出不包含重复的字符最长连续区间,输出区间长度。
思路讲解
我们同样用两个指针来维护该问题,以 abbcd 为例,用 hash 表判断该字符是否在窗口中出现了两次及以上。
- 初始时左右指针均指向第一个字符,窗口为 [l,r),右指针开始扫描。
- 左指针此时指向第 1 位,右指针此时指向第 2 位,窗口为
a。 - 左指针此时指向第 1 位,右指针此时指向第 3 位,窗口为
ab。 - 左指针此时指向第 1 位,右指针此时指向第 4 位,窗口为
abb,不合法,开始移动左指针。 - 左指针此时指向第 2 位,右指针此时指向第 4 位,窗口为
bb,不合法,移动左指针。 - 左指针此时指向第 3 位,右指针此时指向第 4 位,窗口为
b,合法,移动右指针。 - 左指针此时指向第 3 位,右指针此时指向第 5 位,窗口为
bc。 - 左指针此时指向第 3 位,右指针此时指向第 6 位,窗口为
bcd。结束循环。
最终结果为 3。

代码实现
C++代码实现
cpp
#include <iostream>
#include <unordered_set>
#include <algorithm>
using namespace std;
int lengthOfLongestSubstring(string s) {
unordered_set<char> charSet;
int start = 0, maxLength = 0;
for (int end = 0; end < s.length(); ++end) {
while (charSet.find(s[end]) != charSet.end()) {
charSet.erase(s[start]);
++start;
}
charSet.insert(s[end]);
maxLength = max(maxLength, end - start + 1);
}
return maxLength;
}
int main() {
string s = "abcabcbb";
cout << lengthOfLongestSubstring(s) << endl; // 输出: 3,最长不重复子串是 "abc"
return 0;
}
Java代码实现
java
import java.util.HashSet;
public class LongestSubstring {
public static int lengthOfLongestSubstring(String s) {
HashSet<Character> charSet = new HashSet<>();
int start = 0, maxLength = 0;
for (int end = 0; end < s.length(); end++) {
while (charSet.contains(s.charAt(end))) {
charSet.remove(s.charAt(start));
start++;
}
charSet.add(s.charAt(end));
maxLength = Math.max(maxLength, end - start + 1);
}
return maxLength;
}
public static void main(String[] args) {
String s = "abcabcbb";
System.out.println(lengthOfLongestSubstring(s)); // 输出: 3,最长不重复子串是 "abc"
}
}
Python代码实现
python
def length_of_longest_substring(s: str) -> int:
char_set = set()
start = 0
max_length = 0
for end in range(len(s)):
while s[end] in char_set:
char_set.remove(s[start])
start += 1
char_set.add(s[end])
max_length = max(max_length, end - start + 1)
return max_length
# 测试
s = "abcabcbb"
print(length_of_longest_substring(s)) # 输出: 3,最长不重复子串是 "abc"
运行结果

简单总结
滑动窗口算法本质上是通过两个指针进行维护,通常为左右指针从起点往终点开始扫描,可以将 O() 的问题优化成 O(n)。
第二节
知识点:
(1)连续的子数组和(2)滑动窗口应用的技巧和优化
滑动窗口应用场景
滑动窗口是一种算法技巧,用于解决一类特定的问题,通常涉及在一个数组或字符串上移动一个固定大小的窗口,并在每个位置计算窗口内特定条件的结果。这个窗口可以是固定大小的,也可以是可变大小的,具体取决于问题的要求。
-
滑动窗口通常用于解决需要在连续子数组或子字符串上执行操作的问题,例如找到最长的连续子数组满足某种条件、找到满足特定条件的最短子数组、计算窗口内的最大值或最小值等。
-
基本思想是维护两个指针,一个指向窗口的起始位置,另一个指向窗口的结束位置。然后,根据问题的要求,移动这两个指针来调整窗口的大小,并在每个位置更新结果。
-
滑动窗口算法通常具有较高的效率,因为它们只需要对每个元素进行一次处理,时间复杂度通常为 O(n),其中 n 是输入数据的大小。
总的来说,滑动窗口是一种非常实用的算法技巧,在解决特定类型的问题时,它能够以较低的时间复杂度提供高效的解决方案。
连续的子数组和
问题描述
思路讲解
滑动窗口在连续的子数组和的应用是一个典型的问题,通常被称为"子数组问题"或"子数组和问题"。其基本思想是通过移动窗口来计算连续子数组的和,以解决与子数组和相关的各种问题,如找到满足特定条件的子数组、计算最大子数组和、最小子数组和等
解法一------暴力解法思路
暴力解法的核心思想是遍历数组中每个可能的起始位置,并计算从该起始位置开始的连续 k 个元素的和,最终找出这些和中的最大值。
具体步骤
- 初始化一个变量
max_sum,用于存储当前找到的最大和,初始值设为负无穷大(或数组中的最小可能值)。 - 遍历数组,从索引
0到n-k(这里n是数组的长度),每次以当前索引为起点,计算连续k个元素的和。 - 更新
max_sum为当前计算的和与max_sum中的较大者。 - 遍历结束后,
max_sum即为所求的最大和。
时间复杂度是 O()
代码实现
C++代码实现
cpp
#include <iostream>
#include <vector>
#include <climits>
using namespace std;
int max_sum_of_k_elements(vector<int>& nums, int k) {
int n = nums.size();
if (n < k) return INT_MIN; // 如果数组长度小于k,无法找到连续的k个元素
int max_sum = INT_MIN;
for (int i = 0; i <= n - k; ++i) {
int current_sum = 0;
for (int j = 0; j < k; ++j) {
current_sum += nums[i + j];
}
if (current_sum > max_sum) {
max_sum = current_sum;
}
}
return max_sum;
}
int main() {
vector<int> nums = {1, 4, 2, 10, 23, 3, 1, 0, 20};
int k = 4;
cout << max_sum_of_k_elements(nums, k) << endl; // 输出 39
return 0;
}
Java代码实现
java
public class MaxSumOfKElements {
public static int maxSumOfKElements(int[] nums, int k) {
int n = nums.length;
if (n < k) return Integer.MIN_VALUE; // 如果数组长度小于k,无法找到连续的k个元素
int max_sum = Integer.MIN_VALUE;
for (int i = 0; i <= n - k; ++i) {
int current_sum = 0;
for (int j = 0; j < k; ++j) {
current_sum += nums[i + j];
}
if (current_sum > max_sum) {
max_sum = current_sum;
}
}
return max_sum;
}
public static void main(String[] args) {
int[] nums = {1, 4, 2, 10, 23, 3, 1, 0, 20};
int k = 4;
System.out.println(maxSumOfKElements(nums, k)); // 输出 39
}
}
Python代码实现
python
def max_sum_of_k_elements(nums, k):
n = len(nums)
if n < k:
return None # 如果数组长度小于k,无法找到连续的k个元素
max_sum = float('-inf')
for i in range(n - k + 1):
current_sum = sum(nums[i:i + k])
if current_sum > max_sum:
max_sum = current_sum
return max_sum
# 示例调用
nums = [1, 4, 2, 10, 23, 3, 1, 0, 20]
k = 4
print(max_sum_of_k_elements(nums, k)) # 输出 39
运行结果

解法二------使用滑动窗口优化算法
如图,在一个整数数组中,需要求数组中连续的 3 个元素的最大和,可以通过滑动窗口的方式求出每一个连续区间的值。
图示

示例
假设给定一个整数数组 nums 和一个整数 k,找到数组中连续的 k 个元素的最大和。
思路讲解
在这个示例中,我们定义了一个函数 max_subarray_sum,它接受一个整数数组 nums 和一个整数 k 作为输入。函数首先检查 k 是否大于数组长度,如果是,则返回 None。然后,我们初始化 max_sum 为负无穷大,并计算前 k 个元素的和作为 window_sum。
接下来,我们使用一个循环来移动窗口,并在每个位置更新 max_sum,同时更新 window_sum 以维护窗口的和。最后,返回 max_sum 作为连续 k 个元素的最大和。
在这个例子中,输出结果为连续的 3 个元素的最大和为 7,对应子数组 [3, 4, -1]。
时间复杂度为 O(n)。
代码实现
C++代码实现
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int maxSubarraySum(vector<int>& nums, int k) {
if (k > nums.size())
return INT_MIN;
int max_sum = INT_MIN;
int window_sum = 0;
for (int i = 0; i < k; ++i)
window_sum += nums[i];
for (int i = 0; i <= nums.size() - k; ++i) {
max_sum = max(max_sum, window_sum);
if (i + k < nums.size())
window_sum = window_sum - nums[i] + nums[i + k];
}
return max_sum;
}
int main() {
vector<int> nums = {1, -2, 3, 4, -1, 2, 1, -5, 4};
int k = 3;
int result = maxSubarraySum(nums, k);
cout << "连续的 " << k << " 个元素的最大和为:" << result << endl;
return 0;
}
Java代码实现
java
import java.util.Arrays;
public class Main {
public static int maxSubarraySum(int[] nums, int k) {
if (k > nums.length)
return Integer.MIN_VALUE;
int max_sum = Integer.MIN_VALUE;
int window_sum = 0;
for (int i = 0; i < k; ++i)
window_sum += nums[i];
for (int i = 0; i <= nums.length - k; ++i) {
max_sum = Math.max(max_sum, window_sum);
if (i + k < nums.length)
window_sum = window_sum - nums[i] + nums[i + k];
}
return max_sum;
}
public static void main(String[] args) {
int[] nums = {1, -2, 3, 4, -1, 2, 1, -5, 4};
int k = 3;
int result = maxSubarraySum(nums, k);
System.out.println("连续的 " + k + " 个元素的最大和为:" + result);
}
}
Python代码实现
python
def max_subarray_sum(nums, k):
if k > len(nums):
return None
max_sum = float('-inf')
window_sum = sum(nums[:k])
for i in range(len(nums) - k + 1):
max_sum = max(max_sum, window_sum)
if i + k < len(nums):
window_sum = window_sum - nums[i] + nums[i + k]
return max_sum
nums = [1, -2, 3, 4, -1, 2, 1, -5, 4]
k = 3
result = max_subarray_sum(nums, k)
print("连续的 {} 个元素的最大和为:{}".format(k, result))
运行结果

滑动窗口的应用和优化
滑动窗口应用
滑动窗口是一个非常有用的技巧,可以应用于多种问题:
-
最大连续子数组和:像你之前看到的那样,滑动窗口可以用来计算一个数组中连续子数组的最大和。通过维护一个窗口,在每个位置计算窗口内元素的和,然后更新最大和。
-
字符串匹配:滑动窗口可以用于字符串匹配问题。例如,在一个长字符串中查找一个短字符串是否存在。通过维护一个窗口,逐步移动并比较窗口内的字符串与目标字符串。
-
最小覆盖子串:给定一个字符串和一个目标字符串,找到字符串中包含目标字符串所有字符的最短子串。通过维护一个窗口,逐步扩大窗口直到包含所有目标字符,然后收缩窗口直到不能再收缩为止。
-
最长无重复字符子串:给定一个字符串,找到其中最长的无重复字符子串。通过维护一个窗口,逐步扩大窗口直到出现重复字符,然后收缩窗口直到不再出现重复字符,同时更新最长子串的长度。
-
可靠传输: TCP 使用滑动窗口来实现可靠传输。发送方发送数据并等待接收方确认,如果收到确认,则将滑动窗口向前滑动,继续发送下一批数据;如果未收到确认,则重传滑动窗口内的数据,直到接收到确认为止。

优化技巧
- 使用哈希表:在滑动窗口中,使用哈希表来记录窗口中的元素及其出现次数,以便快速判断是否满足条件。
- 双指针技巧:在某些情况下,可以使用双指针来优化滑动窗口的操作。例如,在寻找最长无重复字符子串时,可以使用两个指针来标记窗口的起始和结束位置。
- 预处理:在某些问题中,可以在遍历数组或字符串之前进行一些预处理,以减少滑动窗口的操作次数,从而提高效率。
- 优化窗口移动:在移动窗口时,可以根据具体情况选择适当的移动策略,以减少不必要的操作。
简单总结
在本节中,我们学习了滑动窗口算法。滑动窗口是一种在解决数组或字符串相关问题时非常有用的技巧。除了计算子数组和之外,滑动窗口还可以用于解决其他类型的问题,如字符串匹配、最小覆盖子串、最长无重复字符子串等。