Optimal Algorithms:滑动窗口

一. getOrDefault 方法的使用

1. 定义与功能

  • getOrDefault是Java中的Map接口提供的一个方法。它的作用是获取指定键对应的值,如果键不存在,则返回一个默认值。
  • 方法签名为V getOrDefault(Object key, V defaultValue),其中key是要获取值的键,defaultValue是键不存在时返回的默认值,V是值的数据类型。

2. 使用示例

  • 假设有一个HashMap<String, Integer>,用于统计单词出现的次数:
import java.util.HashMap;
import java.util.Map;
public class GetOrDefaultExample {
    public static void main(String[] args) {
        Map<String, Integer> wordCount = new HashMap<>();
        // 统计单词"apple"出现的次数
        wordCount.put("apple", wordCount.getOrDefault("apple", 0)+1);
        System.out.println(wordCount.get("apple")); 
        // 尝试获取不存在的单词"banana"的次数,返回默认值0
        System.out.println(wordCount.getOrDefault("banana", 0)); 
    }
}
  • 在这个示例中,wordCount.getOrDefault("apple", 0)的作用是:如果wordCount这个Map中已经存在键为"apple"的值,就获取这个值;如果不存在,就返回默认值0。这样就可以方便地进行计数操作,不用担心NullPointerException(空指针异常),因为如果键不存在,会返回一个合理的默认值。

二. 最大连续 1 的个数 III(medium)

2.1 题目链接:

最大连续 1 的个数 III

2.2 题目描述:

给定一个二进制数组 nums 和一个整数 k,假设最多可以翻转 k0 ,则返回执行操作后 数组中连续 1 的最大个数 。

示例 1:

输入: nums = [1,1,1,0,0,0,1,1,1,1,0], K = 2
输出: 6
解释: [1,1,1,0,0,1,1,1,1,1,1]
粗体数字从 0 翻转到 1,最长的子数组长度为 6。

示例 2:
输入: nums = [0,0,1,1,0,0,1,1,1,0,1,1,0,0,0,1,1,1,1], K = 3
输出: 10
解释: [0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1,1,1]
粗体数字从 0 翻转到 1,最长的子数组长度为 10。

提示:

  • 1 <= nums.length <= 105
  • nums[i]不是 0 就是 1
  • 0 <= k <= nums.length

2.3 题目解析:

public class MaxConsecutiveOnesIII {
    public static int longestOnes(int[] nums, int k) {
        int left = 0;
        int right = 0;
        int zeroCount = 0;
        int maxLength = 0;
        // 开始滑动窗口遍历数组
        while (right < nums.length) {
            if (nums[right] == 0) {
                zeroCount++;
            }
            // 当窗口内0的个数超过了k,需要收缩窗口(左移left指针)
            while (zeroCount > k) {
                if (nums[left] == 0) {
                    zeroCount--;
                }
                left++;
            }
            // 更新最大连续1的个数(包含最多k个翻转的0)对应的长度
            maxLength = Math.max(maxLength, right - left + 1);
            right++;
        }
        return maxLength;
    }

    public static void main(String[] args) {
        int[] nums = {1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0};
        int k = 2;
        System.out.println(longestOnes(nums, k));

        int[] nums2 = {0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1};
        int k2 = 3;
        System.out.println(longestOnes(nums2, k2));
    }
}

代码解释如下:

  1. 定义变量
    • leftright分别表示滑动窗口的左右边界,初始都指向数组的起始位置(索引为0)。
    • zeroCount用于统计窗口内0的个数,初始化为0。
    • maxLength用于记录满足条件下连续1的最大个数(包含最多k个翻转的0),初始化为0。
  2. 滑动窗口循环
    • 通过while (right < nums.length)来不断向右移动窗口的右边界right,遍历整个数组。
    • nums[right]为0时,说明窗口内多了一个0,zeroCount自增1。
  3. 调整窗口(收缩窗口)
    • 当窗口内0的个数zeroCount超过了给定的可翻转0的个数k时,需要收缩窗口,也就是左移left指针,通过while (zeroCount > k)循环来实现。如果窗口左边界对应的元素nums[left]为0,那么将zeroCount减1,表示移除了一个0,然后左移left指针(left++)。
  4. 更新最大长度
    • 在每次移动右边界right后,通过maxLength = Math.max(maxLength, right - left + 1);来更新满足条件下连续1的最大长度,这里right - left + 1就是当前窗口的长度。
  5. 主函数测试
    • main方法中,给出了示例中的两组测试数据进行方法调用并输出结果,方便验证代码的正确性。

整体代码利用滑动窗口的思想,通过合理地移动窗口左右边界以及统计窗口内0的个数,来找到满足最多翻转k个0情况下连续1的最大个数对应的子数组长度。

三. 将 x 减到 0 的最小操作数 (medium)

3.1 题目链接:

将 x 减到 0 的最小操作数

3.2 题目描述:

给你一个整数数组 nums 和一个整数 x 。每一次操作时,你应当移除数组 nums 最左边或最右边的元素,然后从 x 中减去该元素的值。请注意,需要 修改 数组以供接下来的操作使用。

如果可以将 x 恰好 减到 0 ,返回 最小操作数 ;否则,返回 -1

示例 1:

输入: nums = [1,1,4,2,3], x = 5
输出:2
解释: 最佳解决方案是移除后两个元素,将 x 减到 0 。

示例 2:

输入: nums = [5,6,7,8,9], x = 4
输出: -1

示例 3:

输入: nums = [3,2,20,1,1,3], x = 10
输出: 5
解释: 最佳解决方案是移除后三个元素和前两个元素(总共 5 次操作),将 x 减到 0 。

提示:

  • 1 <= nums.length <= 105
  • 1 <= nums[i] <= 104
  • 1 <= x <= 109

3.3 题目解析:

public class MinOperations {
    public static int minOperations(int[] nums, int x) {
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        // 目标和(即剩余需要凑出的和)
        int target = sum - x;
        if (target < 0) {
            return -1;
        }
        if (target == 0) {
            return nums.length;
        }
        int left = 0;
        int curSum = 0;
        int minOp = Integer.MAX_VALUE;
        for (int right = 0; right < nums.length; right++) {
            curSum += nums[right];
            while (curSum > target && left <= right) {
                curSum -= nums[left];
                left++;
            }
            if (curSum == target) {
                minOp = Math.min(minOp, nums.length - (right - left + 1));
            }
        }
        return minOp == Integer.MAX_VALUE? -1 : minOp;
    }

    public static void main(String[] args) {
        int[] nums = {1, 1, 4, 2, 3};
        int x = 5;
        System.out.println(minOperations(nums, x));

        int[] nums2 = {5, 6, 7, 8, 9};
        int x2 = 4;
        System.out.println(minOperations(nums2, x2));

        int[] nums3 = {3, 2, 20, 1, 1, 3};
        int x3 = 10;
        System.out.println(minOperations(nums3, x3));
    }
}

代码解释如下:

  1. 计算数组总和并确定目标和
    • 首先通过一个循环遍历数组nums,计算数组中所有元素的总和sum,即sum += num
    • 然后确定目标和target,它等于数组总和sum减去给定的整数x,也就是要从数组两端移除元素凑出这个目标和,使得最终x能减到0。如果target小于0,说明数组元素总和都小于x,无法将x减到0,直接返回-1;如果target等于0,说明需要移除整个数组所有元素,返回数组长度nums.length
  2. 初始化变量及滑动窗口循环
    • 定义left表示滑动窗口的左边界,初始为0;curSum表示当前窗口内元素的和,初始为0;minOp用于记录最小操作数,初始化为Integer.MAX_VALUE(代表一个很大的值,方便后续比较取最小值)。
    • 通过for循环,以right作为滑动窗口的右边界,从索引0开始遍历数组nums,每次循环将nums[right]加入到curSum中,即curSum += nums[right],来不断扩大窗口内元素的和。
  3. 调整窗口(收缩窗口)
    • curSum大于目标和target时,需要收缩窗口,也就是通过while (curSum > target && left <= right)循环,从窗口左边移除元素,即将nums[left]curSum中减去(curSum -= nums[left]),然后左移left指针(left++),直到窗口内元素和不大于目标和为止。
  4. 更新最小操作数
    • curSum恰好等于目标和target时,说明找到了一种可行的移除元素的方案,此时可以计算操作数,操作数等于数组总长度nums.length减去当前窗口的长度(right - left + 1),然后通过minOp = Math.min(minOp, nums.length - (right - left + 1));来更新最小操作数minOp,取当前的最小操作数和之前记录的minOp中的较小值。
  5. 返回结果
    • 最后判断minOp是否还是初始的Integer.MAX_VALUE,如果是,说明没有找到能将x减到0的方案,返回-1;否则返回找到的最小操作数minOp

main方法中,给出了题目示例中的三组测试数据进行方法调用并输出结果,方便验证代码的正确性。整体代码利用滑动窗口的思想,通过不断调整窗口来寻找满足将x减到0的最小操作数对应的方案。

四. 水果成篮(medium)

4.1 题目链接:

水果成篮

4.2 题目描述:

你正在探访一家农场,农场从左到右种植了一排果树。这些树用一个整数数组 fruits 表示,其中 fruits[i] 是第 i 棵树上的水果 种类 。

你想要尽可能多地收集水果。然而,农场的主人设定了一些严格的规矩,你必须按照要求采摘水果:

  • 你只有 两个 篮子,并且每个篮子只能装 单一类型 的水果。每个篮子能够装的水果总量没有限制。
  • 你可以选择任意一棵树开始采摘,你必须从 每棵 树(包括开始采摘的树)上 恰好摘一个水果 。采摘的水果应当符合篮子中的水果类型。每采摘一次,你将会向右移动到下一棵树,并继续采摘。
  • 一旦你走到某棵树前,但水果不符合篮子的水果类型,那么就必须停止采摘。
    给你一个整数数组 fruits ,返回你可以收集的水果的 最大 数目。

示例 1:

输入: fruits = [1,2,1]
输出: 3
解释: 可以采摘全部 3 棵树。

示例 2:

输入: fruits = [0,1,2,2]
输出: 3
解释: 可以采摘 [1,2,2] 这三棵树。
如果从第一棵树开始采摘,则只能采摘 [0,1] 这两棵树。

示例 3:

输入: fruits = [1,2,3,2,2]
输出: 4
解释: 可以采摘 [2,3,2,2] 这四棵树。
如果从第一棵树开始采摘,则只能采摘 [1,2] 这两棵树。

示例 4:

输入: fruits = [3,3,3,1,2,1,1,2,3,3,4]
输出: 5
解释: 可以采摘 [1,2,1,1,2] 这五棵树。

提示:

  • 1 <= fruits.length <= 105
  • 0 <= fruits[i] < fruits.length

4.3 题目解析:

import java.util.HashMap;
import java.util.Map;

public class FruitIntoBaskets {
    public static int totalFruit(int[] fruits) {
        int left = 0;
        int maxFruits = 0;
        Map<Integer, Integer> fruitCount = new HashMap<>();

        for (int right = 0; right < fruits.length; right++) {
            // 将当前水果放入篮子(统计个数)
            fruitCount.put(fruits[right], fruitCount.getOrDefault(fruits[right], 0) + 1);
            // 当篮子里水果种类超过2种时,收缩窗口(左移left指针)
            while (fruitCount.size() > 2) {
                fruitCount.put(fruits[left], fruitCount.get(fruits[left]) - 1);
                if (fruitCount.get(fruits[left]) == 0) {
                    fruitCount.remove(fruits[left]);
                }
                left++;
            }
            // 更新能收集到的最大水果数目
            maxFruits = Math.max(maxFruits, right - left + 1);
        }

        return maxFruits;
    }

    public static void main(String[] args) {
        int[] fruits = {1, 2, 1};
        System.out.println(totalFruit(fruits));

        int[] fruits2 = {0, 1, 2, 2};
        System.out.println(totalFruit(fruits2));

        int[] fruits3 = {3, 3, 3, 1, 2, 1, 1, 2, 3, 3, 4};
        System.out.println(totalFruit(fruits3));
    }
}

代码解释如下:

  1. 定义变量与初始化

    • left表示滑动窗口的左边界,初始化为0,用于标记从哪棵树开始的窗口范围符合要求。
    • maxFruits用于记录能收集到的水果的最大数目,初始化为0,后续不断更新取最大值。
    • fruitCount是一个HashMap,用于统计窗口内每种水果的个数,其中键表示水果的种类,值表示该种类水果在当前窗口内的数量。
  2. 滑动窗口循环

    • 通过for循环,以right作为滑动窗口的右边界,从索引0开始遍历数组fruits,每次循环将当前位置fruits[right]对应的水果放入“篮子”(也就是在fruitCount中更新其数量),使用fruitCount.put(fruits[right], fruitCount.getOrDefault(fruits[right], 0) + 1),意思是如果该水果种类已经在map中存在,就获取其当前数量并加1;如果不存在,则初始化为1。
  3. 调整窗口(收缩窗口)

    • fruitCount中记录的水果种类超过2种时(也就是不符合只能用两个篮子且每个篮子装单一类型水果的规则了),需要收缩窗口,即左移left指针。通过while (fruitCount.size() > 2)循环来实现。在循环内,先将窗口左边界对应的水果数量减1,即fruitCount.put(fruits[left], fruitCount.get(fruits[left]) - 1),然后判断其数量是否减到了0,如果减到0了,说明这种水果在窗口内已经没有了,就从fruitCount中移除该水果种类(if (fruitCount.get(fruits[left]) == 0) { fruitCount.remove(fruits[left]); }),最后左移left指针(left++)。
  4. 更新最大水果数目

    • 在每次移动右边界right后,通过maxFruits = Math.max(maxFruits, right - left + 1);来更新能收集到的水果的最大数目,这里right - left + 1就是当前窗口内包含的树的数量,也就是能收集到的水果数量,取它和之前记录的maxFruits中的较大值进行更新。
  5. 主函数测试

    • main方法中,给出了题目示例中的三组测试数据进行方法调用并输出结果,方便验证代码的正确性。

整体代码利用滑动窗口的思路,通过合理地移动窗口左右边界以及统计窗口内水果种类和数量,来找到符合规则下能收集到的水果的最大数目。

五. 找到字符串中所有字母异位词(medium)

5.1 题目链接:

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

5.2 题目描述:

给定两个字符串 sp,找到 s 中所有 p
异位词的子串,返回这些子串的起始索引。不考虑答案输出的顺序。

示例 1:

输入: s = “cbaebabacd”, p = “abc”
输出: [0,6]
解释:
起始索引等于 0 的子串是 “cba”, 它是 “abc” 的异位词。
起始索引等于 6 的子串是 “bac”, 它是 “abc” 的异位词。

示例 2:

输入: s = “abab”, p = “ab”
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 “ab”, 它是 “ab” 的异位词。
起始索引等于 1 的子串是 “ba”, 它是 “ab” 的异位词。
起始索引等于 2 的子串是 “ab”, 它是 “ab” 的异位词。

提示:

  • 1 <= s.length, p.length <= 3 * 104
  • s 和 p 仅包含小写字母

5.3 题目解析:

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class FindAllAnagrams {
    public static List<Integer> findAnagrams(String s, String p) {
        List<Integer> result = new ArrayList<>();
        if (s.length() < p.length()) {
            return result;
        }

        // 用于统计字符串p中各字符出现的次数
        Map<Character, Integer> targetMap = new HashMap<>();
        for (char c : p.toCharArray()) {
            targetMap.put(c, targetMap.getOrDefault(c, 0) + 1);
        }

        // 用于统计滑动窗口内各字符出现的次数
        Map<Character, Integer> windowMap = new HashMap<>();
        int left = 0;
        int right = 0;
        int matchCount = 0;

        while (right < s.length()) {
            char rightChar = s.charAt(right);
            // 将窗口右移一位,把新进入窗口的字符加入统计
            windowMap.put(rightChar, windowMap.getOrDefault(rightChar, 0) + 1);
            // 如果窗口内该字符出现次数不超过目标字符串中该字符出现次数,则匹配字符数加1
            if (windowMap.get(rightChar) <= targetMap.getOrDefault(rightChar, 0)) {
                matchCount++;
            }
            // 当窗口大小等于目标字符串长度时,判断是否为异位词
            if (right - left + 1 == p.length()) {
                if (matchCount == p.length()) {
                    result.add(left);
                }
                char leftChar = s.charAt(left);
                // 将窗口左移一位,把移出窗口的字符从统计中移除
                windowMap.put(leftChar, windowMap.get(leftChar) - 1);
                // 如果窗口内该字符出现次数小于目标字符串中该字符出现次数,匹配字符数减1
                if (windowMap.get(leftChar) < targetMap.getOrDefault(leftChar, 0)) {
                    matchCount--;
                }
                left++;
            }
            right++;
        }

        return result;
    }

    public static void main(String[] args) {
        String s = "cbaebabacd";
        String p = "abc";
        List<Integer> res = findAnagrams(s, p);
        System.out.println(res);

        String s2 = "abab";
        String p2 = "ab";
        List<Integer> res2 = findAnagrams(s2, p2);
        System.out.println(res2);
    }
}

代码解释如下:

整体思路

使用滑动窗口的思想,通过两个哈希表分别统计目标字符串p中各字符出现的次数以及当前滑动窗口内各字符出现的次数,不断移动窗口并比较窗口内字符情况与目标字符串,判断是否为异位词,若是则记录窗口起始索引。

具体步骤

  1. 初始化相关变量与数据结构

    • 创建一个ArrayList类型的result列表,用于存储满足条件的子串起始索引,初始化为空列表。
    • 首先判断字符串s的长度是否小于字符串p的长度,如果是,则直接返回空列表,因为不可能存在异位词子串了。
    • 创建targetMap哈希表,用于统计字符串p中各字符出现的次数,通过循环遍历字符串p的字符数组,使用targetMap.put(c, targetMap.getOrDefault(c, 0) + 1)来实现字符计数,即如果字符c已经在targetMap中,就获取其当前计数并加1;如果不在,则初始化为1。
    • 创建windowMap哈希表,用于统计滑动窗口内各字符出现的次数,初始为空。
    • 定义leftright分别作为滑动窗口的左右边界,初始都为0。
    • 定义matchCount变量,用于统计当前窗口内与目标字符串p中字符匹配的个数,初始为0。
  2. 滑动窗口循环移动与处理

    • 通过while (right < s.length())循环,不断向右移动窗口的右边界right,遍历整个字符串s
    • 在每次移动右边界时,取出当前位置right对应的字符rightChar,将其加入到windowMap中进行计数,即windowMap.put(rightChar, windowMap.getOrDefault(rightChar, 0) + 1)
    • 接着判断窗口内该字符rightChar出现的次数是否不超过目标字符串p中该字符出现的次数,如果是,则说明该字符匹配上了,将matchCount加1,即if (windowMap.get(rightChar) <= targetMap.getOrDefault(rightChar, 0)) { matchCount++; }
    • 当窗口大小(right - left + 1)等于目标字符串p的长度时,说明窗口内的子串长度已经和目标字符串一样长了,此时进行异位词判断。如果matchCount等于p的长度,意味着窗口内的字符组成和目标字符串p的字符组成完全一样(是异位词关系),就将窗口左边界left添加到结果列表result中,即if (matchCount == p.length()) { result.add(left); }
    • 然后要收缩窗口(左移窗口左边界left),先取出窗口左边界对应的字符leftChar,将其在windowMap中的计数减1,即windowMap.put(leftChar, windowMap.get(leftChar) - 1)。再判断如果窗口内该字符leftChar出现的次数小于目标字符串p中该字符出现的次数,说明少了一个匹配的字符,将matchCount减1,即if (windowMap.get(leftChar) < targetMap.getOrDefault(leftChar, 0)) { matchCount--; },最后左移left指针(left++)。
    • 每次循环结束后,右移right指针(right++),继续下一轮窗口移动与判断。
  3. 主函数测试
    main方法中,给出了题目示例中的两组测试数据进行方法调用并输出结果,方便验证代码的正确性。

通过上述代码,利用滑动窗口结合哈希表的方式,能够准确找出字符串s中所有字符串p的异位词对应的子串起始索引。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值