滑动窗口类算法问题

这一节主要是总结滑动窗口类问题的求解方案

相应的滑动窗口问题在 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,继续向前,如果前面的数组已经在子数组中出现过,那就左指针向前推动并且重新设置标记数组。


滑动窗口类算法问题_第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
}

这里最重要的一点是 什么时候该缩小窗口(见注释),如果我们用字母逐一比对的方式,这种情况下时间复杂度肯定是要高的

滑动窗口类算法问题_第2张图片

你可能感兴趣的:(大厂面试算法指南)