【优选算法】滑动窗口:长度最小的子数组,无重复字符的最长子串,最大连续1的个数,将x减到0的最小操作数,水果成篮,异位词,串联所有单词的子串,最小覆盖子串

文章目录

1. 长度最小的子数组(LC209)

长度最小的子数组

题目描述

解题思路

  1. 初始化两个指针leftright,充当窗口的左端点和右端点。循环以下两步
  2. 进窗口:窗口需要满足和大于等于sum,如果和小于sum则进窗口
  3. 判断 -> 出窗口 : 如果和大于等于sum则更新结果len,出窗口,left向右走一步

注意: 由于数组元素都是正整数,利用单调性,子数组的元素越多,和越大。因此找到符合要求的窗口以后,没必要让右端继续扩大,只需要判断左端缩小以后能否符合要求。这里利用单调性减少了很多没必要的枚举。

时间复杂度:O(n)右指针遍历一遍,左指针不回退。

代码实现

java 复制代码
public int minSubArrayLen(int target, int[] nums) {
        int left = 0;
        int right = 0;
        int len = Integer.MAX_VALUE;
        int sum = 0;
        while(right<nums.length){
            //进窗口
            sum+=nums[right];

            //判断
            while(sum>=target){
                //更新结果
                len = Math.min(len,right - left +1);
                //出窗口
                sum-=nums[left++];
            }
            right++;
        }
        return len == Integer.MAX_VALUE ? 0 : len;
    }

注意:

  1. 因为每次len要取最小值,为了防止0干扰结果,初始化要置为最大值。
  2. 结果可能为0,为了防止返回最大值,返回时需要判断。

2. 无重复字符的最长子串(LC3)

无重复字符的最长子串

题目描述

解题思路

  • 思路一:暴力枚举+set判断重复。
  • 思路二:在思路一上进行优化。定义leftright充当窗口的左端点和右端点。
    1. 固定左端点,右端点逐步向右移动,窗口每添加一个字符,则加入到hashset中。
    2. right遇到重复字符,left只需要移动到重复字符的下一个位置,right不需要回退。

代码实现

java 复制代码
public int lengthOfLongestSubstring(String s) {
        HashSet<Character> set = new HashSet<>();
        int len = 0;
        int left = 0;
        int right = 0;
        while(right<s.length()){
            //判断
            while(set.contains(s.charAt(right)))
                set.remove(s.charAt(left++));
            //进窗口
            set.add(s.charAt(right));
            len =Math.max(len,right - left +1);
            right++;
        }
        return len;
    }

注意: 不可以先添加到哈希表中再判断是否重复

3. 最大连续1的个数(LC1004)

最大连续1的个数 III

题目描述

解题思路

鉴于题目没有要求翻转0,翻转0的操作比较复杂,这道题的要求可以转换为在给定的数组找到最长子数组,并且保证子数组中0的个数不超过K个。

  1. 定义left=0,right=0
  2. 进窗口:遇到1不必理会;遇到0,开始计数
  3. 判断是否出窗口:当窗口中0个数超过K,left向右移动。如果left遇到0,计数器-1,直到窗口合法。
  4. 更新结果,取最大值

代码实现

java 复制代码
public int longestOnes(int[] nums, int k) {
        int zero = 0;
        int len = 0;
        for(int left = 0, right = 0;right < nums.length;right++){
            //进窗口
            if(nums[right]==0)
                zero++;
            while(zero > k){
                if(nums[left]==0)
                    zero--;
                left++;
            }
            len = Math.max(len,right - left +1);
        }
        return len;
    }

4. 将x减到0的最小操作数(LC1658)

将x减到0的最小操作数

题目描述

解题思路

在两端操作比较复杂,采用"正难则反"的策略:先计算整个数组的和sum,在数组中间找最长子数组,使中间子数组的和为sum-x,剩余两端的元素个数就是最小操作数。这样土题目转换为找到最长的和为target的子数组。(target = sum-x

题目提醒所有元素都是正数,利用单调性,窗口变大,和增大。因此要维持窗口和sum1小于等于target;当sum1大于target,就没必要扩大窗口,而应该缩小窗口。

  1. 计算数组和sum,定义left=0,right=0,定义子数组和sum1 = 0
  2. 进窗口:right向右走一步;
  3. 判断是否出窗口:如果sum1大于targetleft向右走一步;相等则记录长度,取最大值。

代码实现

java 复制代码
public int minOperations(int[] nums, int x) {
        int sum = 0;
        int n = nums.length;
        for(int i :nums )
            sum += i;
        int target = sum - x;
        if(target <0)
            return -1;

        int sum1 =0;
        int left = 0;
        int right = 0;
        int len = 0;
        while(right < n){
            sum1 += nums[right];
            while(sum1>target){
                sum1-=nums[left++];
            }
            if(sum1 == target)
                len = Math.max(len,right - left +1);
            right++;
        }
        return len==0?-1:n-len;
    }

5. 水果成篮(LC904)

水果成篮

题目描述

解题思路

翻译:在数组中找到最长子数组,其中最多有两个不同的数。

  1. 固定left,向右移动right,当种类超过2时,right停止移动。接下来进行枚举,left向右移动一步,种类可能会变为2,也有可能不变。无论哪种情况right都不需要回退。利用hashmap记录数字和出现的次数。
  2. 进窗口:right向右走一步
  3. 判断是否出窗口:当hashmap长度大于二时,left要向右移动,出窗口,对应数字的数量要-1。直到hashmap长度回到2
  4. 更新子数组长度,取最大值。

代码实现

java 复制代码
 public int totalFruit(int[] fruits) {
        int len = 0;
        int left = 0;
        int right = 0;
        HashMap<Integer,Integer> map = new HashMap<>();

        while(right < fruits.length){
            //进窗口
            int in = fruits[right];
            map.put( in, map.getOrDefault(in,0)+1);

            //判断
            while(map.size() > 2){
                int out = fruits[left];
                //出窗口
                if(map.get(out) > 1)
                    map.put(out,map.get(out)-1);
                else 
                    map.remove(out);
                left++;
            }
            len = Math.max(len,right - left + 1);

            right++;
        } 
        return len;
    }

6. 找到字符串中所有字母异位词(LC438)

找到字符串中所有字母异位词

题目描述

解题思路

  1. 判断异位词:
    • 思路一:对两个字符串都进行排序,再用双指针依次判断是否相等。时间复杂度太高
    • 思路二:利用hashmap统计每个字符串中字母种类和个数,不关心顺序。
      • 因为字符只有26个,这里hashmap可以简化为数组hash1[],下标0代表a,以此类推,数组中存的值代表出现的次数。
  2. 主问题: leftright充当窗口的左端点和右端点
    1. 进窗口:right向右走一步,hash[in]++
    2. 判断是否出窗口:只需要判断窗口大小是否小于等于p字符串p长度。不等则出窗口,hash[out]--
    3. 更新结果:每个窗口是hash2,与hash1对比,完全相等就可以确认为异位词。记录当前left下标。

代码实现

java 复制代码
public List<Integer> findAnagrams(String s, String p) {
        int[] hash1 = new int[26];
        int[] hash2 = new int[26];
        int left = 0;
        int right = 0;
        List<Integer> ret = new ArrayList<>();

        //计算p的hash数组
        for (int i = 0; i < p.length(); i++) {
            hash1[p.charAt(i) - 97]++;
        }

        while (right < s.length()) {
            //进窗口
            char in = s.charAt(right);
            hash2[in - 97]++;

            //判断 出窗口
            if (right - left + 1 > p.length()) {
                char out = s.charAt(left);
                hash2[out - 97]--;
                left++;
            }

            if (right - left + 1 == p.length()) {
                //更新结果
                boolean equal = true;
                for (int i = 0; i < 26; i++) {
                    if (hash1[i] != hash2[i]) {
                        equal = false;
                        break;
                    }
                }
                if (equal)
                    ret.add(left);
            }
            right++;
        }
        return ret;
    }

7. 串联所有单词的子串(LC30)

串联所有单词的子串

题目描述

解题思路

把主串按单词长度分开,就可以转换为找words的异位词。

与上一题的区别:

  1. 哈希表的映射关系:单词字符串和出现的次数
  2. right每次移动的步长:单词的长度
  3. 以下图为例,无法确定单词恰好对应下标[0 1 2],也可能存在[1 2 3[2 3 4 ]这样的组合。因此滑动窗口不止要遍历一次,要遍历单词长度len次。

代码实现

java 复制代码
public List<Integer> findSubstring(String s, String[] words) {
        int sLen = s.length();
        int wordLen = words[0].length();
        int wordsLen = words.length;
        //串联子串的长度
        int len = wordLen*wordsLen;
        List<Integer> ret = new ArrayList<>();

        HashMap<String,Integer> hash1 = new HashMap<>();
        for(String ss : words){
            hash1.put(ss,hash1.getOrDefault(ss,0)+1);
        }

        //滑动窗口执行次数
        for(int i = 0 ; i < wordLen ; i++){
            HashMap<String,Integer> hash2 = new HashMap<>();
            for(int left = i,right = i ; right + wordLen <= sLen ; right+=wordLen){
                //进窗口
                String in = s.substring(right , right + wordLen);
                hash2.put(in,hash2.getOrDefault(in,0)+1);

                //长度超出串联子串长度,出窗口
                if(right - left + 1 > len){
                    String out = s.substring(left,left+wordLen); 
                    if(hash2.get(out)>1)
                        hash2.put(out,hash2.get(out)-1);
                    else
                        hash2.remove(out);
                    left+=wordLen;
                }

                //长度相等时判断两个哈希表是否相等
                if(hash1.size()==hash2.size()){
                    boolean equal = true;
                    for(String ss : hash2.keySet()){
                        if(hash1.get(ss)==null || !hash1.get(ss).equals(hash2.get(ss))){
                            equal = false;
                            break;
                        }
                    }
                    if(equal)
                        ret.add(left);
                }
            }
        }
        return ret;
    }

8. 最小覆盖子串(LC76)

最小覆盖子串

题目描述

解题思路

利用hashmap记录t中字符和出现的次数,这里hashmap简化为数组,下标表示字母,数组中存的值表示出现次数。

  1. 进窗口:当right遇到有效字符(该字符在窗口出现次数== t中对应的次数)时停止移动。
  2. 判断 出窗口:当left遇到有效字符时,如果窗口符合条件则记录窗口和长度,取最小值更新。left再向后走一步

优化:如果采用遍历判断两个hash表是否相等会非常费时间,引入变量count来标记有效字符的种类。

  1. 进窗口后,如果两个哈希表中对应数量相同,则count++,表示这个字符的数量足够了。
  2. 出窗口之前,如果数量相等,则count--
  3. 在判断两个哈希表是否相等时,先判断countt中种类是否相等。

代码实现

java 复制代码
public String minWindow(String ss, String tt) {
        int left = 0;
        int right = 0;

        int count = 0;//s中有效字符个数
        int kinds = 0;//t中字符种类

        int len = Integer.MAX_VALUE;//比较合法窗口的长度
        int begin = -1;//结果的起始下标

        char[] s = ss.toCharArray();
        char[] t = tt.toCharArray();
        
        int[] hash1 = new int[128];
        int[] hash2 = new int[128];

        for(char ch : t){
            if(hash1[ch]==0)
                kinds++;
            hash1[ch]++;
        }
        while(right<s.length){
            //进窗口
            char in = s[right];
            hash2[in]++;
            if(hash1[in]==hash2[in])
                count++;

            //判断 
            while(count == kinds){
                //更新
                int newLen = right - left + 1;
                if(newLen<len){
                    begin = left;
                    len = newLen;
                }
                char out = s[left];
                if(hash2[out]==hash1[out])
                    count--;
                hash2[out]--;
                left++;
            }
            right++;
        }
        return begin==-1?"":ss.substring(begin,begin+len);
    }
相关推荐
naruto_lnq2 小时前
C++与自动驾驶系统
开发语言·c++·算法
啊阿狸不会拉杆2 小时前
《数字信号处理》第6章:数字滤波器的基本概念及几种特殊滤波器
算法·matlab·信号处理·数字信号处理·dsp
放荡不羁的野指针2 小时前
leetcode150题-双指针
数据结构·算法·leetcode
好学且牛逼的马3 小时前
【Hot100|15-LeetCode 238. 除自身以外数组的乘积】
数据结构·算法·leetcode
Tisfy3 小时前
LeetCode 3651.带传送的最小路径成本:动态规划
算法·leetcode·动态规划·题解·排序
努力学习的小廉3 小时前
我爱学算法之—— 递归回溯综合(一)
算法·深度优先
m0_736919103 小时前
C++中的策略模式实战
开发语言·c++·算法
孞㐑¥3 小时前
算法—位运算
c++·经验分享·笔记·算法
软件算法开发3 小时前
基于卷尾猴优化的LSTM深度学习网络模型(CSA-LSTM)的一维时间序列预测算法matlab仿真
深度学习·算法·matlab·lstm·一维时间序列预测·卷尾猴优化·csa-lstm