相应的滑动窗口问题在 leetcode上的 第3题 (median),第76题(hard),第438题(median),这几道都是可以用滑动窗口在 O ( n ) O(n) O(n)的时间复杂度内解决,无需使用暴力的解法,虽然有些题目暴力的解法也是可以通过的,但是我们的目标是找到一种最优解法。
这里我们首先要看第一道例题,也是leetcode上面的,寻找最小连续字串,使得连续字串的和>=s,最后返回子串的长度。题目: 209. Minimum Size Subarray Sum
这是一道最典型的窗口类问题,两年前用了822ms,还是在cpp下,那时候用的是暴力的解法
暴力解法如下,那时候写的代码比较洒脱,想到什么就是什么,不会去想着优化代码:
class Solution {
public:
int minSubArrayLen(int s, vector<int>& nums) {
int sum = 0;
for(int i = 0;i< nums.size();i++)
sum+=nums[i];
if(sum<s)return 0;
sum = 0;
int j = 0;
int co = 2147483647;
cout<<co<<endl;
for(int i = 0 ;i < nums.size() ;i++){
j = 0;sum = 0;
for(int k = i ;k<nums.size() ;k++){
j++;
sum+=nums[k];
if(sum >=s){
co = co > j?j:co;
}
}
}
return co;
}
};
下面是我昨天用滑动窗口写的代码:
class Solution {
public int minSubArrayLen(int s, int[] nums) {
// 滑动窗口解题思路
int r = 0, l = 0;
int sum = 0,ret = nums.length+1;
while(l < nums.length){
if(sum < s && r < nums.length)
sum+=nums[r++];
else{
sum-=nums[l++];
}
if(sum >= s)
ret = (r-l) > ret ? ret:(r-l);
}
if(ret == nums.length+1)
ret = 0;
return ret;
}
}
暴力解法的思路,就是两个循环直接得出最小长度。这里主要是解释怎么用滑动窗口的思路解答问题。
我们在用暴力解法的时候,存在大量的重复计算问题,比如说,我们从下标为1的位置计算到下标为6的位置,第二轮循环又从下标为2的位置计算到下标为6,实际上我们第一遍就计算了这个过程。
这里,我们引入滑动窗口,我们设置左右两个指针(实际上不是cpp中那个指针,而是一种指向),初始时两个指针都在最左边。右边的指针向前推动,我们计算左右指针之间元素的和,当和大于所给的值s,这时候右边的指针就不动了,并且记录此时的长度,与最短长度进行比较,更新最短长度。左边指针开始向右滑动,缩短长度,看看是否还是>=s,我们通过左右指针不断地向前滑动,直到左指针到了边界,此时,整个过程结束了,返回记录的最短长度。
再看一下第三题这个例子, Longest Substring Without Repeating Characters,最长不重复子串
Input: “abcabcbb”
Output: 3
Explanation: The answer is “abc”, with the length of 3.
Input: “bbbbb”
Output: 1
Explanation: The answer is “b”, with the length of 1.
这道题在两年前,也是用暴力AC过,也是用cpp的情况下235ms。
暴力CPP解法:
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int count,record = 0,index = 0;
int i = 0,j;
bool flag = false;
while(index < s.size()&&i< s.size())
{
count = 0;
flag = false;
for(i = index; i<s.length(); i++)
{
for(j = index; j<i; j++)
{
if(s[i] == s[j])
{
count = 0;
flag = true;
index = j+1;
break;
}
}
count++;
record = count>record?count:record;
if(flag)break;
}
}
return record;
}
};
下面是用java滑动窗口写的代码:
class Solution {
public int lengthOfLongestSubstring(String s) {
int []mark = new int[128]; // 默认初始化为0
char []a = s.toCharArray();
int r = -1, l= -1;
int ret = 0;
while(l < s.length()-1){
if(r+1 < s.length() && mark[a[r+1]] == 0)
mark[a[++r]] ++;
else{
mark[a[++l]]--;
}
ret = max(r-l, ret);
}
return ret;
}
public int max(int a, int b){
if(a>b) return a;
else
return b;
}
}
这里我引入了一个mark数组来记录元素是否在子数组中,由于a-z,以及A-Z的Asic在128以内,所以可以这么定义标记数组,并且初始化为0,原理是和上面一样的,不同的是我们设置了mark标记函数,我们的右指针向前推动,如果前面的元素没在子数组中出现过,那就长度+1,继续向前,如果前面的数组已经在子数组中出现过,那就左指针向前推动并且重新设置标记数组。
这题分析,必然是滑动窗口,因为是小写字母,我们这里用两个数组来记录每个字母出现的个数,老规矩,先上代码再做解释:
class Solution {
public List<Integer> findAnagrams(String s, String p) {
int []need = new int[26];
int []window = new int[26];
List<Integer> res = new ArrayList();
if(p.length() > s.length() || s.length() == 0) return res;
// 记录每个p每个字母出现的次数
for(int i = 0; i<p.length(); i++)
need[p.charAt(i)-'a'] ++;
int left = 0, right = 0;
while(right < s.length()){
int index = s.charAt(right++)-'a';
window[index]++;
// 当窗口中所包含的当前字母个数超过p中该字母的个数,证明需要缩小窗口了
while(window[index] > need[index]){
window[s.charAt(left) - 'a'] --;
left++;
}
if(right - left == p.length()){
res.add(left);
}
}
return res;
}
//abacabc abc
}
这里最重要的一点是 什么时候该缩小窗口(见注释),如果我们用字母逐一比对的方式,这种情况下时间复杂度肯定是要高的