[LeetCode]-滑动窗口

前言

记录刷 LeetCode 时遇到的滑动窗口相关题目

209.长度最小的子数组

滑动窗口不适合含负值的数组

/**
 *劣质的 滑动窗口 ,由于把考虑到的特殊情况都用if-else单独拎出来处理,导致过多的if-else分支,一方面代码不够简洁,一方面执行效率也降低了
 * =============下面做了四个改进===============================
 * ==========改进过程可以明显发现影响时间最大的就是count函数========================================================
 * ====由于滑动窗口是O(n)算法且是一个元素一个元素进行操作,所以用变量维护的方法维护每一个滑动窗口(子数组)的和是可取的,=======
 * ====完全不需要每次都调用循环的方法去求和=================================================================
 * ===================================================================================================
**/
public int minSubArrayLen(int target, int[] nums) {
    int length = nums.length;
    /* 改进一:
        首先这两个if-else是完全没必要的
        if(length == 0){
            return 0;
        }
        if(length == 1){
            return nums[0] >= target ? 1 : 0;
        }*/
    int result = length + 1;
    int left = 0;
    int right = 0;
    /*改进二:
     *      这段是用来初始化窗口的,改掉这一段后,时间明显提升,可能是因为每一次都调用了count方法,而count方法本身是个循环,应该用变量维护的方法求
     * 当前子数组的和而不是每次变换窗口都计算子数组的和
     * while (right < length && count(nums, left, right) < target){
     *             right++;
     *}*/
    int count1 = 0;
    while (right < length){
        count1 += nums[right];
        if(count1 >= target){
            break;
        }else {
            right++;
        }
    }
    /* 注意点1
     * 此时right可能已经超出数组长度了·  即特例 :所有数组元素加起来都不等于target,也就是说 result 可能是没有被修改的,
     * 所以最后不应该直接返回result,而是返回 result == length + 1 ? 0 : result*/
    while (right < length){
        if(count1 >= target){
            result = Math.min(right - left + 1, result);
            /*改进三:就算left加一后与right重合了也无所谓,此时子数组和为0,到下一次循环时right会加一,不用担心left比right大的
            if(left + 1 <= right) {
                count1 -= nums[left];
                left++;
            }else {
                right++;
                left++;
                if(right < length){
                    count1 = nums[left];
                }
            }*/
            count1 -= nums[left++];
        }else {
            right++;
            if(right < length){
                count1 += nums[right];
            }
        }
    }
    /* 注意点2 当所有数组元素加起来都不等于target时应返回0*/
    return result == length + 1 ? 0 : result;
}
/*public int count(int[] nums, int left, int right){
    if(left == right){
        return nums[left];
    }
    int count = 0;
    for(int i = left;i <= right && i < nums.length;i++){
        count += nums[i];
    }
    return count;
}*/
//整理后是:
public int minSubArrayLen(int target, int[] nums) {
    int length = nums.length;
    int result = length + 1;
    int left = 0;
    int right = 0;
    int count1 = 0;
    while (right < length){
        count1 += nums[right];
        if(count1 >= target){
            break;
        }else {
            right++;
        }
    }
    while (right < length){
        if(count1 >= target){
            result = Math.min(right - left + 1, result);
            count1 -= nums[left++];
        }else {
            right++;
            if(right < length){
                count1 += nums[right];
            }
        }
    }
    return result == length + 1 ? 0 : result;
}

904.水果成篮

public int totalFruit(int[] fruits) {
    int length = fruits.length;
    /* 不必要的特判:
    if(length == 1){
        return 1;
    }
    if(length == 2){
        return 2;
    }*/
    int left = 0;
    int right = -1;
    //找到与首元素不同的元素,对应情况:前面好几个都一样
    for(int i = 1;i < length;i++){
        if(fruits[i] != fruits[left]){
            right = i;
            break;
        }
    }
    //特例 : 所有数都是同一个
    if(right == -1){
        return length;
    }
    int record1;
    int record2;
    int maxCount = right - left + 1;
    while (right < length - 1){
        //更新记录的两个数 
        record1 = fruits[left];
        record2 = fruits[right];
        while (right < length - 1 && (fruits[right + 1] == record1 || fruits[right + 1] == record2)){
            right++;
        }
        maxCount = Math.max(maxCount, (right - left + 1));
        int temp = right;
        while (fruits[temp] == fruits[right]){
            temp--;
        }
        left = temp + 1;
        //此时要么right的下一个是不同的数,要么right >= length - 1然后跳出循环,所以right++是完全买毛病的
        right++;
    }
    return maxCount;
}

76.最小覆盖子串(hard)

class Solution {
    private Map<Character,Integer> tMap = new HashMap<>();
    private Map<Character,Integer> resMap = new HashMap<>();
    public String minWindow(String s, String t) {
        if(s.length() == 0){
            return "";
        }
        int tLength = t.length();
        int sLength = s.length();
        for (int i = 0; i < tLength; i++) {
            char c = t.charAt(i);
            tMap.put(c,tMap.getOrDefault(c,0) + 1);
        }
        int l = 0;
        while (l < sLength && !tMap.containsKey(s.charAt(l))){
            l++;
        }
        int r = sLength - 1;
        while (r > l && !tMap.containsKey(s.charAt(r))){
            r--;
        }
        if(l > r){
            return "";
        }
        //以上代码是在把s两端不在t中出现的字符全部剔除,再在剩下的字符串newS中查找符合条件的字符串
        String newS = s.substring(l,r + 1);
        //len记录运算过程中当前记录到的最小窗口的长度
        int len = 1000000000;
        int resl = 0;
        int resr = -1;
        //p1是窗口左边界,收缩窗口;p2是窗口右边界,扩大窗口
        int p1 = 0;
        //p2从负一开始是为了应对newS长度为一的情况,如样例s:"a",t:"a"
        int p2 = -1;
        //p2<newS.length() - 1很容易理解,窗口的右边界达到字符串末端时是滑动的终止条件
        //不过可能会出现p2到达字符串末端时,p1还可以再向右继续缩小窗口,因此还要加上一个p1<p2的条件
        while (p1 < p2 || p2 < newS.length() - 1){
            //只要当前窗口不符合条件p2都要不断右移
            while (p2 < newS.length() - 1 && !check()){
                p2++;
                char c = newS.charAt(p2);
                resMap.put(c,resMap.getOrDefault(c,0) + 1);
            }
            //当前窗口符合条件,就要更新len以及resl,resr,保证此时的len是最小的,resl和resr是相对应的字符串下标
            if(check() && p2 - p1 +  1 < len){
                len = p2 - p1 +  1;
                resl = p1;
                resr = p2;
            }
            //此时得到的窗口是符合条件的,所以可以开始缩小窗口往右遍历寻找更优解
            if(p1<p2){
            char c = newS.charAt(p1++);
            //p1一开始指向的肯定是t中有的字符,收缩的思路是找到下一个t中有的字符
            resMap.put(c,resMap.get(c) - 1);
            while (!tMap.containsKey(newS.charAt(p1))){
                p1++;
            }}
        }
        return (resr == -1) ? "" : newS.substring(resl,resr + 1);
    }
    public boolean check(){
        for (Map.Entry<Character, Integer> entry : tMap.entrySet()) {
            Character key = entry.getKey();
            Integer val = entry.getValue();
            if (resMap.getOrDefault(key, 0) < val) {
                return false;
            }
        }
        return true;
    }
}

674. 最长连续递增序列(在线处理/滑动窗口)

要求子序列必须是连续的。变量 l 维护当前遍历到的数所在的连续序列的长度,那么,当遍历到 nums[i] 时,如果 nums[i] > nums[i - 1],l 就加一,否则 nums[i] 就应该作为新的递增序列的开头重新计算, l 置为 1

public int findLengthOfLCIS(int[] nums) {
    int len = nums.length;
    int l = 1,maxL = 1; //maxL维护最大长度。l初始化为1,对应 nums[0] 
    for(int i = 1;i < len;i++){
        if(nums[i] > nums[i - 1]){
            l++;
            maxL = Math.max(l,maxL);
        }
        else l = 1;
    }
    return maxL;
}

53.最大子数组和(在线处理/滑动窗口)

public int maxSubArray(int[] nums) {
    int sum = 0,max = Integer.MIN_VALUE;
    for (int num : nums) {
        if (sum < 0) {
            sum = 0;
        }
        sum += num;
        max = Math.max(max, sum);
    }
    return max;
}

从头开始遍历数组,用sum记录此时遍历到的子序列的元素和,如果sum小于 0,也就是说遍历到的子序列和小于 0,由于题目要求是子序列。那么这段元素和为 0 的子序列对后面的子序列来说就是累赘,因为不管后面序列的和为多少,如果要把这段和为 0 的子序列加上,整个序列的和都是变小的,所以如果当前sum小于 0,就要抛弃当前遍历到的子序列

面试题 17.24. 最大子矩阵

解题思路来自该题解
大概就是,对于每一行 i,将第 i 行到第 j 行 (j = i,i + 1,…,row - 1) 之间的所有行 “压榨” 为一个数,然后进行一维数组的 [最大子数组和] 求解

public int[] getMaxMatrix(int[][] matrix) {
    int row = matrix.length;
    int column = matrix[0].length;
    int maxSum = Integer.MIN_VALUE,r1 = 0,c1 = 0,r2 = 0,c2 = 0,sum,r1Tmp = 0,c1Tmp = 0;
    int[] rowSum = new int[column]; //当前被“压榨”的所有行中每一列的和
    for(int i = 0;i < row;i++){  //对于每一行 i
        Arrays.fill(rowSum,0);
        for(int j = i;j < row;j++){  //枚举 j
            sum = 0;
            for(int k = 0;k < column;k++){ 
                rowSum[k] += matrix[j][k];  //求i 到 j 行间所有行中每一列的和
                if(sum > 0) sum += rowSum[k]; 
                else{    //前面的和小于0,那就弃掉,让当前列的和作为新的 sum,同时更新左上角的行列坐标
                    sum = rowSum[k];
                    r1Tmp = i;
                    c1Tmp = k;
                }
                if(sum > maxSum){  //如果得到了比维护的子矩阵最大和还大的和,就更新维护答案的四个坐标以及maxSum
                    maxSum = sum;
                    r1 = r1Tmp;
                    c1 = c1Tmp;
                    r2 = j;
                    c2 = k;
                }
            }
        }
    }
    return new int[]{r1,c1,r2,c2};
}

363. 矩形区域不超过 K 的最大数值和

这道题下面的解法其实不太像滑动窗口,但思路上跟上面两道题非常相似,所以我就放到一起了。三道题建议一起比对
首先,根据题意是要枚举原矩阵中的部分矩形区域,也就是子矩阵,那么类似于上面的 [最大子矩阵] 题目,枚举一个子矩阵时将其每一行 “压缩” 为一个数,得到一个一维数组,这个一维数组每一个数就表示一行的元素总和,再计算这些行能组成的矩阵中,元素总和最大的那个总和即可,当然这个最大总和要求不大于 k

class Solution {
    public int maxSumSubmatrix(int[][] matrix, int k) {
        int rows = matrix.length, cols = matrix[0].length, max = Integer.MIN_VALUE;
        for (int l = 0; l < cols; l++) { // 枚举左边界
            int[] rowSum = new int[rows]; 
            for (int r = l; r < cols; r++) { // 枚举右边界
                for (int i = 0; i < rows; i++) {
                    rowSum[i] += matrix[i][r]; //累加计算左右边界之间每一行的元素总和
                }
                max = Math.max(max, findMax(rowSum, k)); //找到左右边界之间这些行可以组成的矩阵中,元素总和最大的那个总和
                if(max == k) return k; //找到等于k的答案直接返回,节约后续的运算,也算个剪枝
            }
        }
        return max;
    }
    private int findMax(int[] arr, int k) {
        int max = Integer.MIN_VALUE,sum;
        for (int i = 0;i < arr.length;i++) { //第i行可以和第i+1,...,第arr.length-1行之间连续的行组成矩阵
            sum = 0;
            for (int j = i; j < arr.length; j++) {
                sum += arr[j];
                if (sum > max && sum <= k) max = sum;
                if(max == k) return k;
            }
        }
        return max;

    }
}

可以看出来,其实 findMax() 函数就是在求 arr 数组的最大子数组和,只不过要求这个最大子数组和不超过 k,那么类似于 [最大子数组和],可以做出如下优化:

class Solution {
    public int maxSumSubmatrix(int[][] matrix, int k) {
    	//...
        int[] rowSum = new int[rows];  //将rowSum的重新new操作改为全填充为0
        for (int l = 0; l < cols; l++) { // 枚举左边界
            Arrays.fill(rowSum,0);
            //...
        }
        return max;
    }
    private int findMax(int[] arr, int k) {
        int sum = 0,max = Integer.MIN_VALUE;
        for(int i : arr){
            if(sum < 0) sum = 0;
            sum += i;
            max = Math.max(max,sum);
        }
        if(max <= k) return max; //最大子数组和不超过k就满足,可以直接返回,否则就还是要按照原来那样算
        max = Integer.MIN_VALUE;
        sum = 0;
        for (int i = 0;i < arr.length;i++) {
            //...
        }
        return max;
    }
}

3. 无重复字符的最长子串

使用滑动窗口算法。初始左右边界都在下标 0 位置,然后开始向右延伸右边界,同时记录遍历到的每个字符及其最近一次出现的下标。延伸右边界直到遇到重复字符,就将左边界收缩到重复字符上一次出现的位置的下一个位置,然后继续延伸右边界,以此类推。移动窗口的同时维护窗口的长度

public int lengthOfLongestSubstring(String s) {
    int len = s.length();
    if (len == 0) return 0;
    char[] c = s.toCharArray();
    //map记录每个字符最近一次出现的下标
    HashMap<Character, Integer> map = new HashMap<Character, Integer>();
    int max = 0;
    int left = 0;
    for(int i = 0; i < len; i++){
        if(map.containsKey(c[i])){
            //出现一个重复字符,就将窗口左边界移到重复字符上一次出现位置的下一个位置
            //由于任何字符上一次出现的位置一定不会超过右边界(i),所以更新后的左边界一定不大于右边界
            left = Math.max(left,map.get(c[i]) + 1);
        }
        map.put(c[i],i);
        //维护最大长度
        max = Math.max(max,i - left + 1);
    }
    return max;
}

187. 重复的DNA序列

每次截取字符串中的连续 10 个字符得到一个子串,使用哈希表记录所有子串的出现次数,出现次数大于 1 的就添加到 ans 中

public List<String> findRepeatedDnaSequences(String s) {
    List<String> ans = new ArrayList<String>(); //保存答案
    Map<String, Integer> countMap = new HashMap<String, Integer>(); //记录子串出现次数
    int len = s.length();
    for (int i = 0; i <= len - 10;i++) {
        String sub = s.substring(i, i + 10); //截取子串
        int num = countMap.getOrDefault(sub,0);
        if(num == 1) ans.add(sub); //判断是否需要添加到ans中
        countMap.put(sub,num + 1);
    }
    return ans;
}

官方题解 的第二种方法也挺有意思,这里简单 mark 一下

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值