滑动窗口算法
什么是滑动窗口算法
我们学习过计算机网络都知道为了避免拥塞发生,在网络传输时有滑动窗口协议控制传输时流量。该协议允许发送方在停止并等待确认前发送多个数据分组。由于发送方不必每发一个分组就停下来等待确认,因此该协议可以加速数据的传输,提高网络吞吐量。这个跟我们今天说的滑动窗口算法是一个原理。
滑动窗口算法的作用
该算法的作用就是将我们多层嵌套的循环语句根据局部最优解来转换为单个的循环语句,从而减少时间复杂性。
下面我们通过LeetCode 上的一道题来看看滑动窗口究竟该如何使用
题目
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
示例 2:
输入: “bbbbb”
输出: 1
解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。
示例 3:
输入: “pwwkew”
输出: 3
解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。请注意,你的答案必须是 子串 的长度,“pwke” 是一个子序列,不是子串。
分析
这道题目我们需要找到字符串中的最长的子串,普通的暴力解法是通过for循环遍历给定字符串的所有子串,判断子串中有没有重复的字符,如果没有则找出最长的。
但是上面的算法我们时间复杂度为O(n^3)。那么有没有更好的算法呢,当然是有的,那就是滑动窗口算法。通过使用HashSet作为滑动窗口,我们可以用O(1)的时间来完成对字符是否在当前的子字符串中的检查。
滑动窗口是数组/字符串问题中常用的抽象概念。窗口通常是在数组/字符串中由开始和结束索引定义的一系列元素的集合,即[left,right)(左闭,右开)。而滑动窗口是可以将两个边界向某一方向“滑动”的窗口。例如,我们将[left,right)向右滑动1个元素,则它将变为[left+1,right+1)(左闭,右开)。我们使用HashSet将字符存储在当前窗口[left,right)(最初left=right)中。然后我们向右侧滑动索引right,如果它不在HashSet中,我们会继续滑动 right。直到s[right]已经存在于HashSet中。此时,我们找到的没有重复字符的最长子字符串将会以索引left开头。如果我们对所有的leift这样做,就可以得到答案。
public int lengthOfLongestSubstring(String s) {
int n = s.length();
Set<Character> set = new HashSet<>();
int ans = 0, left = 0, right = 0;
while (left < n && right < n) {
if (!set.contains(s.charAt(right))){
set.add(s.charAt(right++));
ans = Math.max(ans, right - left);
}
else {
set.remove(s.charAt(left++));
}
}
return ans;
}
算法分析
这里时间复杂度为 O(2n)=O(n),空间复杂度为O(min(m,n))。
优化
这里我们还可以将算法优化,将字符串中每个字符和索引形成映射关系。举个例子,给定字符串“abcabcbb”,这里我们依次按照上述算法执行,我们到第四个字符也就是right为3时,我们left可以跳过前面的0123,直接left和right为4,从4开始再继续找。
public int lengthOfLongestSubstring(String s) {
int length = s.length();
HashMap<Character,Integer> map = new HashMap<Character, Integer>();
int ans =0;
for(int left = 0,right = 0; left < length && right < length;right++){
if (map.containsKey(s.charAt(right))){
left= Math.max(map.get(s.charAt(right)),left);
}
ans = Math.max(ans,right-left+1);
map.put(s.charAt(right),right+1);
}
return ans;
}
总结
从上面的解题我们可以明显的发现,滑动窗口算法的的时间复杂度为线性的(O(n)),我们可以用此算法来查找最大/最小k-子序列,XOR,乘积,总和等一些列问题。
参考资料
无重复字符的最长子串
作者:紫雾凌寒
来源:CSDN