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 题目链接:
2.2 题目描述:
给定一个二进制数组 nums
和一个整数 k
,假设最多可以翻转 k
个 0
,则返回执行操作后 数组中连续 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));
}
}
代码解释如下:
- 定义变量:
left
和right
分别表示滑动窗口的左右边界,初始都指向数组的起始位置(索引为0)。zeroCount
用于统计窗口内0的个数,初始化为0。maxLength
用于记录满足条件下连续1的最大个数(包含最多k
个翻转的0),初始化为0。
- 滑动窗口循环:
- 通过
while (right < nums.length)
来不断向右移动窗口的右边界right
,遍历整个数组。 - 当
nums[right]
为0时,说明窗口内多了一个0,zeroCount
自增1。
- 通过
- 调整窗口(收缩窗口):
- 当窗口内0的个数
zeroCount
超过了给定的可翻转0的个数k
时,需要收缩窗口,也就是左移left
指针,通过while (zeroCount > k)
循环来实现。如果窗口左边界对应的元素nums[left]
为0,那么将zeroCount
减1,表示移除了一个0,然后左移left
指针(left++
)。
- 当窗口内0的个数
- 更新最大长度:
- 在每次移动右边界
right
后,通过maxLength = Math.max(maxLength, right - left + 1);
来更新满足条件下连续1的最大长度,这里right - left + 1
就是当前窗口的长度。
- 在每次移动右边界
- 主函数测试:
- 在
main
方法中,给出了示例中的两组测试数据进行方法调用并输出结果,方便验证代码的正确性。
- 在
整体代码利用滑动窗口的思想,通过合理地移动窗口左右边界以及统计窗口内0的个数,来找到满足最多翻转k
个0情况下连续1的最大个数对应的子数组长度。
三. 将 x 减到 0 的最小操作数 (medium)
3.1 题目链接:
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));
}
}
代码解释如下:
- 计算数组总和并确定目标和:
- 首先通过一个循环遍历数组
nums
,计算数组中所有元素的总和sum
,即sum += num
。 - 然后确定目标和
target
,它等于数组总和sum
减去给定的整数x
,也就是要从数组两端移除元素凑出这个目标和,使得最终x
能减到0。如果target
小于0,说明数组元素总和都小于x
,无法将x
减到0,直接返回-1
;如果target
等于0,说明需要移除整个数组所有元素,返回数组长度nums.length
。
- 首先通过一个循环遍历数组
- 初始化变量及滑动窗口循环:
- 定义
left
表示滑动窗口的左边界,初始为0;curSum
表示当前窗口内元素的和,初始为0;minOp
用于记录最小操作数,初始化为Integer.MAX_VALUE
(代表一个很大的值,方便后续比较取最小值)。 - 通过
for
循环,以right
作为滑动窗口的右边界,从索引0开始遍历数组nums
,每次循环将nums[right]
加入到curSum
中,即curSum += nums[right]
,来不断扩大窗口内元素的和。
- 定义
- 调整窗口(收缩窗口):
- 当
curSum
大于目标和target
时,需要收缩窗口,也就是通过while (curSum > target && left <= right)
循环,从窗口左边移除元素,即将nums[left]
从curSum
中减去(curSum -= nums[left]
),然后左移left
指针(left++
),直到窗口内元素和不大于目标和为止。
- 当
- 更新最小操作数:
- 当
curSum
恰好等于目标和target
时,说明找到了一种可行的移除元素的方案,此时可以计算操作数,操作数等于数组总长度nums.length
减去当前窗口的长度(right - left + 1
),然后通过minOp = Math.min(minOp, nums.length - (right - left + 1));
来更新最小操作数minOp
,取当前的最小操作数和之前记录的minOp
中的较小值。
- 当
- 返回结果:
- 最后判断
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));
}
}
代码解释如下:
-
定义变量与初始化:
left
表示滑动窗口的左边界,初始化为0,用于标记从哪棵树开始的窗口范围符合要求。maxFruits
用于记录能收集到的水果的最大数目,初始化为0,后续不断更新取最大值。fruitCount
是一个HashMap
,用于统计窗口内每种水果的个数,其中键表示水果的种类,值表示该种类水果在当前窗口内的数量。
-
滑动窗口循环:
- 通过
for
循环,以right
作为滑动窗口的右边界,从索引0开始遍历数组fruits
,每次循环将当前位置fruits[right]
对应的水果放入“篮子”(也就是在fruitCount
中更新其数量),使用fruitCount.put(fruits[right], fruitCount.getOrDefault(fruits[right], 0) + 1)
,意思是如果该水果种类已经在map
中存在,就获取其当前数量并加1;如果不存在,则初始化为1。
- 通过
-
调整窗口(收缩窗口):
- 当
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++
)。
- 当
-
更新最大水果数目:
- 在每次移动右边界
right
后,通过maxFruits = Math.max(maxFruits, right - left + 1);
来更新能收集到的水果的最大数目,这里right - left + 1
就是当前窗口内包含的树的数量,也就是能收集到的水果数量,取它和之前记录的maxFruits
中的较大值进行更新。
- 在每次移动右边界
-
主函数测试:
- 在
main
方法中,给出了题目示例中的三组测试数据进行方法调用并输出结果,方便验证代码的正确性。
- 在
整体代码利用滑动窗口的思路,通过合理地移动窗口左右边界以及统计窗口内水果种类和数量,来找到符合规则下能收集到的水果的最大数目。
五. 找到字符串中所有字母异位词(medium)
5.1 题目链接:
5.2 题目描述:
给定两个字符串 s
和 p
,找到 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
中各字符出现的次数以及当前滑动窗口内各字符出现的次数,不断移动窗口并比较窗口内字符情况与目标字符串,判断是否为异位词,若是则记录窗口起始索引。
具体步骤
-
初始化相关变量与数据结构
- 创建一个
ArrayList
类型的result
列表,用于存储满足条件的子串起始索引,初始化为空列表。 - 首先判断字符串
s
的长度是否小于字符串p
的长度,如果是,则直接返回空列表,因为不可能存在异位词子串了。 - 创建
targetMap
哈希表,用于统计字符串p
中各字符出现的次数,通过循环遍历字符串p
的字符数组,使用targetMap.put(c, targetMap.getOrDefault(c, 0) + 1)
来实现字符计数,即如果字符c
已经在targetMap
中,就获取其当前计数并加1;如果不在,则初始化为1。 - 创建
windowMap
哈希表,用于统计滑动窗口内各字符出现的次数,初始为空。 - 定义
left
和right
分别作为滑动窗口的左右边界,初始都为0。 - 定义
matchCount
变量,用于统计当前窗口内与目标字符串p
中字符匹配的个数,初始为0。
- 创建一个
-
滑动窗口循环移动与处理
- 通过
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++
),继续下一轮窗口移动与判断。
- 通过
-
主函数测试
在main
方法中,给出了题目示例中的两组测试数据进行方法调用并输出结果,方便验证代码的正确性。
通过上述代码,利用滑动窗口结合哈希表的方式,能够准确找出字符串s
中所有字符串p
的异位词对应的子串起始索引。