滑动窗口模板及例题
滑动窗口为了解决时间复杂度而提出的一种优化方法,一般可以在O(n)的时间复杂度下解决O(n2)的问题,因此这里进行提供模板,以及Leetcode例题方便读者进行理解。
例题理解
下面我们首先给出一道Leetcode中的例题来进行理解,并给出解题模板。
Leetcode 最长连续子串
题目要求
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
输入: s = “abcabcbb”
输出: 2
解释: 因为最长子重复串是 “bb”
输入: s = “bbbbb”
输出: 5
解释: 因为无重复字符的最长子串是 “bbbbb”
通过上面的例子我们已经可以题意要求了,就是给定字符串,求解最长的无重复字串,这里需要注意子串和子序列的区别。本题通过简单的模拟方法当然可以解决,但是我们希望通过这道题引出滑动窗口的方法。
滑动窗口,重点在几个变量,分别是 left、right、map 后面将分别解释这三个变量。
left:左边界变量。
right:右边界变量。
map:从左边界到右边界的中间变量的存储。
通过 map 变量其实可以看到,滑动窗口更是一种Hash Table的应用。本道题我们在每一移动右边界的过程中,都会接着进行判断,左边界的位置是否合适,这个判断方法也就是通过map来进行判断,本题map变量存储的就是从左边界到右边界出现过的 字符 的出现次数,并根据此来判断从左边界到右边界是否为最长的连续子串。下面查看代码。
public int getMaxLen(String s){
int left = 0;
int right = 0;
int result = Integer.MIN_VALUE;
HashMap<Character,Integer>map = new HashMap<>();
while(right<s.length()){
// 下面是移动右边界
map.put(s.charAt(right),map.getOrDefault(s.charAt(right),0)+1);
right++;
// 下面是在右边界移动完成之后,移动左边界
while(map.keySet().size()!=1){
if(map.get(s.charAt(left))==1){
map.remove(s.charAt(left));
}else{
map.put(s.charAt(left),map.get(s.charAt(left))-1);
}
left++;
}
// 下面是更新最长的子串
result = Math.max(result,right-left);
}
return result;
}
通过上面的代码可以看到,我们其实是在每一次移动右边界之后,会相应的更新左边界,更新的过程正是通过map进行的,其实还可以通过ArrayList等变量进行更新,这就需要具体判断,在更新完成之后,就可以更新最后结果。上面是个人手撸代码,大家可以根据题意进行改变代码解题。
相关例题
下面提供一些例题以及相关题解,帮助大家理解。
Leetcode 1208.尽可能使字符串相等
给你两个长度相同的字符串,s 和 t。
将 s 中的第 i 个字符变到 t 中的第 i 个字符需要 |s[i] - t[i]| 的开销(开销可能为 0),也就是两个字符的 ASCII 码值的差的绝对值。
用于变更字符串的最大预算是 maxCost。在转化字符串时,总开销应当小于等于该预算,这也意味着字符串的转化可能是不完全的。
如果你可以将 s 的子字符串转化为它在 t 中对应的子字符串,则返回可以转化的最大长度。
如果 s 中没有子字符串可以转化成 t 中对应的子字符串,则返回 0。
输入:s = “abcd”, t = “bcdf”, cost = 3
输出:3
解释:s 中的 “abc” 可以变为 “bcd”。开销为 3,所以最大长度为 3。
输入:s = “abcd”, t = “cdef”, cost = 3
输出:1
解释:s 中的任一字符要想变成 t 中对应的字符,其开销都是 2。因此,最大长度为 1。
读完题之后,其实就是在我们前面的模板之上多了一点步骤,就是手动求出花费数组,然后求解花费数组的最长子数组,并且最长子数组求和小遇maxCost。下面看下我们的题解。
public int equalSubstring(String s, String t, int maxCost) {
if(s.length()!=t.length()){
return 0;
}
int []diff = new int[s.length()];
for(int i=0;i<s.length();i++){
diff[i] = Math.abs(s.charAt(i)-t.charAt(i));
}
int left = 0;
int right = 0;
int maxLen = 0;
int tempCost = 0;
while(right<diff.length){
tempCost+=diff[right];
right++;
while(tempCost>maxCost){
tempCost-=diff[left];
left++;
}
maxLen = Math.max(maxLen,right-left);
}
return maxLen;
}
Leetcode 3.无重复字符的最长字串
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
输入: s = “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
输入: s = “bbbbb”
输出: 1
解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。
首先利用前面的模板,给大家看下如何灵活运用。
public static int lengthOfLongestSubstring(String s) {
int left = 0;
int right = 0;
int max = 0;
HashMap<Character,Integer>map = new HashMap<>();
ArrayList<Character>arr = new ArrayList<>();
while(right<s.length()){
map.put(s.charAt(right),map.getOrDefault(s.charAt(right),0)+1);
arr.add(s.charAt(right));
right++;
if(map.keySet().size()!=arr.size()){
Character remove = arr.remove(0);
if(map.get(remove)==1){
map.remove(remove);
}else{
map.put(remove,map.get(remove)-1);
}
left++;
}
max = Math.max(max,right-left);
}
return max;
}
从上面代码中可以看到,我们除了HashMap,同样运用了ArrayList来进行左边界的移动,这里进行判重的原则使 map.keySet().size()!=arr.size(),这样子就可以控制左边界的移动,滑动窗口是一种思想,我们只能够根据题意去适当的套用模板,而不能够完全照搬。
下面是一种更加简便的思想,将代码贴出。
public static int lengthOfLongestSubstring(String s) {
int left = 0;
int right = 0;
int max = 0;
// 这里map用来保存字符上一次出现的index
HashMap<Character,Integer>map = new HashMap<>();
while(right<s.length()){
if(map.containsKey(s.charAt(right))){
// 这里的左边界移动要好好理解。
left = Math.max(left,map.get(s.charAt(right)));
}
right++;
max = Math.max(max,right-left);
}
return max;
}
Leetcode 1934 最短超串
假设你有两个数组,一个长一个短,短的元素均不相同。找到长数组中包含短数组所有的元素的最短子数组,其出现顺序无关紧要。
返回最短子数组的左端点和右端点,如有多个满足条件的子数组,返回左端点最小的一个。若不存在,返回空数组。
输入:
big = [7,5,9,0,2,1,3,5,7,9,1,1,5,8,8,9,7]
small = [1,5,9]
输出: [7,10]
输入:
big = [1,2,3]
small = [4]
输出: []
这里主要注意左边界是如何移动的。下面看下代码。
public static int[] shortestSeq(int[] big, int[] small) {
int minLen = Integer.MAX_VALUE;
int left = 0;
int right = 0;
HashSet<Integer>set = new HashSet<>();
for(int temp:small){
set.add(temp);
}
boolean flag = false;
int []result = new int[2];
HashMap<Integer,Integer>map = new HashMap<>();
while(right<big.length){
map.put(big[right],map.getOrDefault(big[right],0)+1);
right++;
while((left<right)&&((!set.contains(big[left]))||(map.get(big[left])>1))){
map.put(big[left],map.get(big[left])-1);
left++;
}
if(map.keySet().containsAll(set)){
flag = true;
if(right-left<min){
min = right-left;
a[0] = left;
a[1] = right-1;
}
}
}
if(flag){
return result;
}
reutrn new int[0];
}
处女作,希望能够给大家提供一些帮助,个人认为这里主要还是需要理解左边界的移动过程。