leetcode150题-滑动窗口

滑动窗口

长度最小的子数组

给定一个含有 n 个正整数的数组和一个正整数 target 。

找出该数组中满足其总和大于等于 target 的长度最小的 子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。

示例 1:

输入:target = 7, nums = [2,3,1,2,4,3]

输出:2

解释:子数组 [4,3] 是该条件下的长度最小的子数组。

示例 2:

输入:target = 4, nums = [1,4,4]

输出:1

示例 3:

输入:target = 11, nums = [1,1,1,1,1,1,1,1]

输出:0

我的解答:o(n^2)复杂度,双遍历

官方解法:

关键在于利用全为正这个性质

Java 复制代码
class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        // nums和target全为正
        // 核心在于维护一个和大于traget的滑动窗口
        // 则对于一个子数组集合[left,right],如果大于traget了
        // 然后就right + 1,而缩小left到不能再小为止
        // 这样left和right都是o(n)
        int n = nums.length;
        int ans = n + 1;
        int left = 0;
        int sum = 0;
        for(int right = 0; right < n; right++){
            sum += nums[right];
            while(sum - nums[left] >= target){
                sum -= nums[left];
                left++;
            }
            if( (sum >= target) && (ans > right - left + 1)){
                ans = right - left + 1;
            }
        }
        return ans == n+1 ? 0 : ans;

    }
}

无重复字符的最长子串

给定一个字符串 s ,请你找出其中不含有重复字符的 最长 子串 的长度。

示例 1:

输入: s = "abcabcbb"

输出: 3

解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。注意 "bca" 和 "cab" 也是正确答案。

示例 2:

输入: s = "bbbbb"

输出: 1

解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

我的解答:

Java 复制代码
class Solution {
    public int lengthOfLongestSubstring(String s) {
        // 思路类似,也是维护一个当前最长无重复子串
        // 判断是否重复可以维护一个哈希表,这样就是o(1)的判断复杂度
        // 如果有重复,则移动left到重复的下一个
        // 记录最长长度
        int n = s.length();
        if(n < 2){
            return n;
        }
        int ans = 0;
        Map<Character,Integer> map = new HashMap<Character,Integer>();
        int left = 0;
        // 初始化
        map.put(s.charAt(left),left);
        for(int right = 1; right < n; right++){
            char c = s.charAt(right);
            Integer index = map.get(c);
            if(index == null){
                // 加入当前不会重复
                map.put(c,right);
            }else{
                // 加入会重复
                while(left <= index){
                    map.remove(s.charAt(left));
                    left++;
                }
                map.put(c,right);
            }
            if(right - left + 1 > ans){
                ans = right - left + 1;
            }
        }
        return ans;
    }
}

还可以优化,去掉while循环

java 复制代码
class Solution {
    public int lengthOfLongestSubstring(String s) {
        int n = s.length();
        int ans = 0;
        // key: 字符, value: 该字符下一次可以开始的位置 (index + 1)
        Map<Character, Integer> map = new HashMap<>(); 
        
        for (int right = 0, left = 0; right < n; right++) {
            char c = s.charAt(right);
            if (map.containsKey(c)) {
                // 核心:如果字符在map里,left跳到重复字符上次出现位置的下一个
                // 注意:必须用 Math.max,防止 left 往回跳
                // (跳到当前窗口左侧的老数据上)
                left = Math.max(left, map.get(c));
            }
            ans = Math.max(ans, right - left + 1);
            map.put(c, right + 1); // 存入 right + 1,方便下次直接赋值给 left
        }
        return ans;
    }
}

串联所有单词的子串

给定一个字符串 s 和一个字符串数组 words。 words 中所有字符串 长度相同。

s 中的 串联子串 是指一个包含 words 中所有字符串以任意顺序排列连接起来的子串。

例如,如果 words = ["ab","cd","ef"], 那么 "abcdef", "abefcd","cdabef", "cdefab","efabcd", 和 "efcdab" 都是串联子串。 "acdbef" 不是串联子串,因为他不是任何 words 排列的连接。

返回所有串联子串在 s 中的开始索引。你可以以 任意顺序 返回答案。

我的想法

Java 复制代码
// 使用kmp算法得到words里面每个单词在s里面的索引,这样就是一个int数组了
// 然后使用滑动窗口(窗口大小为words数组的长度)滑一次,就得到结果了

存在问题:

Java 复制代码
// 一个单词可能在 s 中多次出现,甚至与其他单词的位置重叠。
// 例如 s = "aaaaa", words = ["aaa", "aa"]。
// 这样的匹配就解决不了了

字符串的排列

给你两个字符串 s1 和 s2 ,写一个函数来判断 s2 是否包含 s1 的 排列。如果是,返回 true ;否则,返回 false 。

换句话说,s1 的排列之一是 s2 的 子串 。

示例 1:

输入:s1 = "ab" s2 = "eidbaooo"

输出:true

解释:s2 包含 s1 的排列之一 ("ba").

示例 2:

输入:s1= "ab" s2 = "eidboaoo"

输出:false

我的做法:

Java 复制代码
class Solution {
    public boolean checkInclusion(String s1, String s2) {
        // 排列,就是s1的字母及其个数,在s2中都有
        int n1 = s1.length();
        int n2 = s2.length();
        if(n1 > n2){
            return false;
        }
        // s1中的字母个数
        // 题目限定仅仅包含小写字母
        Map<Character,Integer> s1CharCount = new HashMap<>();
        for(char c : s1.toCharArray()){
            Integer count = s1CharCount.get(c);
            if(count == null){
                s1CharCount.put(c,1);
            }else{
                s1CharCount.put(c,count+1);
            }
        }

        // 滑动窗口中各个单词的出现次数
        Map<Character,Integer> windowCharCount = new HashMap<>();
        int left = 0;
        for(int right = 0; right < n2; right++){
            // 新滑进来一个
            char c = s2.charAt(right);
            Integer count = windowCharCount.get(c);
            if(count == null){
                windowCharCount.put(c,1);
            }else{
                windowCharCount.put(c,count+1);
            }
            // 没够继续滑
            if(right - left + 1 < n1){
                continue;
            }
            // 够了判断是否满足排列
            if(s1CharCount.equals(windowCharCount)){
                return true;
            }
            // 不是排列,最后面的滑出去
            c = s2.charAt(left);
            count = windowCharCount.get(c);
            if(count == 1){
                windowCharCount.remove(c);
            }else{
                windowCharCount.put(c,count-1);
            }
            left++;
        }
        return false;
    }
}

通过了,但是执行效率不高。核心在于每次都需要调用map的equals方法,而且由于每次只有小写字母,可以直接用数组

Java 复制代码
class Solution {
    public boolean checkInclusion(String s1, String s2) {
        // 排列,就是s1的字母及其个数,在s2中都有
        int n1 = s1.length();
        int n2 = s2.length();
        if(n1 > n2){
            return false;
        }
        // s1Count来记录每一个字符缺失的数目
        int[] s1Count = new int[26];
        // 使用diff来记录s1和s2中不匹配的字符的个数
        int diff = 0;
        for(char c : s1.toCharArray()){
            if(s1Count[c - 'a'] == 0){
                // 第一次出现
                diff++;
            }
            s1Count[c - 'a']++;
        }
        char[] s2Chars = s2.toCharArray();
        for(int i = 0; i < n2; i++){
            int c = s2Chars[i] - 'a';
            s1Count[c]--;
            if(s1Count[c] == 0){
                // 消除了一个不匹配
                // 第c号字符的数目已经相同
                diff--;
            }
            if(i < n1 - 1){
                // 窗口大小不够
                continue;
            }
            
            if(diff == 0){
                // 匹配了
                return true;
            }

            int out = s2Chars[i - n1 + 1] - 'a';
            if(s1Count[out] == 0){
                // 原来是匹配的,现在移走了
                diff++;
            }
            s1Count[out]++;
        }

        return false;
    }
}

第一步是确定子串的开头在哪里,依次遍历

但是可以知道开头最长就到每个单词的长度那里

因为再往后实际上已经在之前切分过了。

第二步就是通过滑动窗口判断当前窗口内是否满足,如果不满足则向右滑动,然后再次判断。直至终点。

原来自己写的时候c == null直接continue了,任务不符合的单词就不应该被加进来。

但是对于对于输入"barfoothefoobarman",["foo","bar"] 会只输出了[0]而不是[0,9]

因为当前窗口有"the",不代表以后窗口不能满足

我看答案之后自己的解答:

Java 复制代码
class Solution {
    public List<Integer> findSubstring(String s, String[] words) {
        if(words.length == 0){
            return new ArrayList();
        }
        int wordLen = words[0].length();
        int windowLen = wordLen * words.length;

        Map<String,Integer> targetCount = new HashMap<>();
        // 遍历查看words数组中各个单词的出现次数
        for(String word : words){
            targetCount.merge(word,1,Integer::sum);
        }

        List<Integer> ans = new ArrayList<>();
        // 枚举滑动窗口的起点
        for(int start = 0; start < wordLen; start++){
            // 当前窗口计数,直接使用浅拷贝即可
            Map<String,Integer> nowCount = new HashMap<>(targetCount);
            // 窗口与目标单词个数不符合的计数
            int diff = words.length;
            // right为开,即窗口为[start,right)
            for(int right = start + wordLen; right <= s.length(); right += wordLen){
                // 当前新加入窗口的单词word
                String word = s.substring(right - wordLen,right);
                Integer c = nowCount.get(word);
                if(c != null){
                    // 如果c为null,则出现了不存在的单词,不管
                    if(c > 1){
                        // 新进来了一个缺的单词,更新nowCount
                        nowCount.put(word,c-1);
                    }else if(c == 1){
                        // 缺且刚好能满足,不同的个数减一
                        nowCount.put(word,0);
                        diff--;
                    } else if(c == 0){
                        // 不缺了,但是又进来一个
                        nowCount.put(word,c-1);
                        diff++;
                    }
                }

                // 窗口左端点
                int left = right - windowLen;
                if(left < 0){
                    // 窗口还没到个数,继续滑,不需要出
                    continue;
                }

                // 判断当前是否满足条件了
                if(diff == 0){
                    ans.add(left);
                }

                // 出最左边的
                String outWord = s.substring(left,left+wordLen);
                c = nowCount.get(outWord);
                if(c != null){
                    if(c == -1){
                        diff--;
                    }else if(c == 0){
                        diff++;
                    }
                    nowCount.put(outWord,c+1);
                }
            }
        }
        return ans;

    }
}

但是是存在问题的,对于"wordgoodgoodgoodbestword",["word","good","best","good"],不能得到8,因为

再第三个good的时候diff变成了3,然后word再出,diff就变成了4,而后无法满足了

Java 复制代码
class Solution {
    public List<Integer> findSubstring(String s, String[] words) {
        if(words.length == 0){
            return new ArrayList();
        }
        int wordLen = words[0].length();
        int windowLen = wordLen * words.length;

        Map<String,Integer> targetCount = new HashMap<>();
        // 遍历查看words数组中各个单词的出现次数
        for(String word : words){
            targetCount.merge(word,1,Integer::sum);
        }

        List<Integer> ans = new ArrayList<>();
        // 枚举滑动窗口的起点
        for(int start = 0; start < wordLen; start++){
            // 应该去记录多出来的单词,而不应该忽略
            // 最重要的是,原来我记录的是不等于0的数,就丢失了原来的数应该是多少
            // 现在记录的是目标是多少和我现在有多少
            Map<String,Integer> nowCount = new HashMap<>();
            // 窗口与目标单词个数不符合的计数
            int diff = targetCount.size();
            // right为开,即窗口为[start,right)
            for(int right = start + wordLen; right <= s.length(); right += wordLen){
                // 当前新加入窗口的单词word
                String inWord = s.substring(right - wordLen,right);
                int oldInCount = nowCount.getOrDefault(inWord,0);
                int targetInCount = targetCount.getOrDefault(inWord,0);

                // 对于不存在的词也进行计数
                nowCount.put(inWord, oldInCount + 1);
                if(oldInCount + 1 == targetInCount){
                    diff--;
                }else if(oldInCount == targetInCount){
                    diff++;
                }

                // 窗口左端点
                int left = right - windowLen;
                if(left < 0){
                    // 窗口还没到个数,继续滑,不需要出
                    continue;
                }

                // 判断当前是否满足条件了
                if(diff == 0){
                    ans.add(left);
                }

                // 出最左边的
                String outWord = s.substring(left,left+wordLen);
                int oldOutCount = nowCount.get(outWord);
                int targetOutCount = targetCount.getOrDefault(outWord,0);
                nowCount.put(outWord,oldOutCount - 1);
                if(oldOutCount - 1 == targetOutCount){
                    diff--;
                }else if(oldOutCount == targetOutCount){
                    diff++;
                }
            }
        }
        return ans;

    }
}

最小覆盖子串

给定两个字符串 s 和 t,长度分别是 m 和 n,返回 s 中的 最短窗口 子串,使得该子串包含 t 中的每一个字符(包括重复字符)。如果没有这样的子串,返回空字符串 ""。

测试用例保证答案唯一。

示例 1:

输入:s = "ADOBECODEBANC", t = "ABC"

输出:"BANC"

解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。

示例 2:

输入:s = "a", t = "a"

输出:"a"

解释:整个字符串 s 是最小覆盖子串。

我看答案之后的解答:

Java 复制代码
class Solution {
    public String minWindow(String s, String t) {
        // 可变滑动窗口从左到右滑动,如果满足了window涵盖t,则不断缩小
        // 记录最小的位置
        int m = s.length();
        int n = t.length();

        int ansLeft = -1;
        int ansRight = m - 1;

        // s和t由英文字符构成,直接用数组
        int[] countS = new int[128];
        int[] countT = new int[128];

        // 初始化t中的字符个数
        for(char c : t.toCharArray()){
            countT[c]++;
        }
        // 遍历窗口
        int left = 0;
        char[] sCharArray = s.toCharArray();
        for(int right = 0; right < m; right++){
            //right 进入
            char inChar = sCharArray[right];
            countS[inChar]++;
            while(contain(countS,countT)){
                // 涵盖
                if(right - left < ansRight - ansLeft){
                    // 找到更小的满足的窗口
                    ansLeft = left;
                    ansRight = right;
                }
                // 不断移出左侧的字符,寻找更小的满足的窗口
                countS[sCharArray[left]]--;
                left++;

            }
        }
        return ansLeft < 0 ? "" : s.substring(ansLeft,ansRight+1);
    }
    // 判断s数组中的个数是不是都大于t
    private boolean contain(int[] countS, int[] countT){
        for(int i = 'A'; i <= 'Z'; i++){
            if(countS[i] < countT[i]){
                return false;
            }
        }
        for(int i = 'a'; i <= 'z'; i++){
            if(countS[i] < countT[i]){
                return false;
            }
        }
        return true;
    }
}

还可以优化,使用diff来消除每次都需要调用contain函数。

Java 复制代码
class Solution {
    public String minWindow(String s, String t) {
        // 可变滑动窗口从左到右滑动,如果满足了window涵盖t,则不断缩小
        // 记录最小的位置

        // 还可以进行优化,每次都要进行判断两个数组是否涵盖
        // 则仍旧可以使用一个diff变量来记录当前不同的个数
        int m = s.length();
        int n = t.length();

        int ansLeft = -1;
        int ansRight = m - 1;

        // s和t由英文字符构成,直接用数组
        int[] countS = new int[128];
        int[] countT = new int[128];

        // 初始化t中的字符个数
        for(char c : t.toCharArray()){
            countT[c]++;
        }

        // 初始化diff
        int diff = 0;
        for(int i : countT){
            if(i != 0){
                diff++;
            }
        }


        // 遍历窗口
        int left = 0;
        char[] sCharArray = s.toCharArray();
        for(int right = 0; right < m; right++){
            //right 进入
            char inChar = sCharArray[right];
            countS[inChar]++;
            if(countT[inChar] != 0 && countS[inChar] == countT[inChar]){
                // 当前的right进入消除了某一个字符的不同
                diff--;
            }
            while(diff == 0){
                // 涵盖
                if(right - left < ansRight - ansLeft){
                    // 找到更小的满足的窗口
                    ansLeft = left;
                    ansRight = right;
                }
                // 不断移出左侧的字符,寻找更小的满足的窗口
                char outChar = sCharArray[left];
                if(countT[outChar] != 0 && countS[outChar] == countT[outChar]){
                    // 当前的left移出导致了某一个字符的不同
                    diff++;
                }
                countS[outChar]--;
                left++;

            }
        }
        return ansLeft < 0 ? "" : s.substring(ansLeft,ansRight+1);
    }
    // 判断s数组中的个数是不是都大于t
    private boolean contain(int[] countS, int[] countT){
        for(int i = 'A'; i <= 'Z'; i++){
            if(countS[i] < countT[i]){
                return false;
            }
        }
        for(int i = 'a'; i <= 'z'; i++){
            if(countS[i] < countT[i]){
                return false;
            }
        }
        return true;
    }
}
相关推荐
BHXDML2 小时前
数据结构:(一)从内存底层逻辑理解线性表
数据结构
小龙报2 小时前
【C语言进阶数据结构与算法】单链表综合练习:1.删除链表中等于给定值 val 的所有节点 2.反转链表 3.链表中间节点
c语言·开发语言·数据结构·c++·算法·链表·visual studio
TracyCoder1233 小时前
LeetCode Hot100(13/100)——238. 除了自身以外数组的乘积
算法·leetcode
CoderCodingNo3 小时前
【GESP】C++五级练习题 luogu-P3353 在你窗外闪耀的星星
开发语言·c++·算法
Anastasiozzzz3 小时前
LeetCode Hot100 215. 数组中的第K个最大元素
数据结构·算法·leetcode
让我上个超影吧3 小时前
【力扣76】最小覆盖子串
算法·leetcode·职场和发展
近津薪荼3 小时前
优选算法——双指针5(单调性)
c++·学习·算法
2401_857683543 小时前
C++代码静态检测
开发语言·c++·算法
时艰.3 小时前
JVM 垃圾收集器(G1&ZGC)
java·jvm·算法