

🔥个人主页:北极的代码(欢迎来访)
🎬作者简介:java后端学习者
✨命运的结局尽可永在,不屈的挑战却不可须臾或缺!
前言:今天忙着写学校的作业,只能简单的写两个算法题了,其实算法题也不简单,对于我这种小白来说,今天依旧是数组篇,给大家推荐我最近在读**《数据结构与算法》**这本书,我个人感觉讲的挺好的,适合新手入门。
一 最小长度子数组
题目背景:LeetCode 209
给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。
示例:
- 输入:s = 7, nums = [2,3,1,2,4,3]
- 输出:2
- 解释:子数组 [4,3] 是该条件下的长度最小的子数组。
提示:
- 1 <= target <= 10^9
- 1 <= nums.length <= 10^5
- 1 <= nums[i] <= 10^5
题目分析 :这个题目,我们一拿到手想到的肯定是最暴力的双层for循环,通过内外嵌套的循环来枚举所有可能的子数组,找到第一个满足条件的,然后返回最短的,进行求和比对
但是缺点也很明显,时间复杂度高 - O(n²),需要 大量重复计算,空间复杂度虽然低,但时间代价太大。由上一节我们学习的双指针法在这也能派上用场,有关数组的查询问题,双指针是不错的方法,在这里也叫做滑动窗口法,比较形象,本质都是一样,下面我们具体解释。
解法一:暴力拆解
java
int result = Integer.MAX_VALUE; // 记录最短长度
for (int i = 0; i < nums.size(); i++) { // 外层:确定子数组起点
int sum = 0; // 重置和
for (int j = i; j < nums.size(); j++) { // 内层:确定子数组终点
sum += nums[j]; // 累加
if (sum >= s) { // 找到第一个满足条件的
int subLength = j - i + 1; // 计算长度
result = Math.min(result, subLength); // 更新最短长度
break; // 这个起点找到了,继续下一个起点
}
}
}
具体实现的流程:
逐步演示
以
s = 7, nums = [2,3,1,2,4,3]为例:第1轮:起点 i=0
j=0: sum=2, 2<7 → 继续 j=1: sum=2+3=5, 5<7 → 继续 j=2: sum=5+1=6, 6<7 → 继续 j=3: sum=6+2=8, 8≥7 → 满足! 子数组[2,3,1,2],长度=4 result = min(∞,4) = 4 break (结束内层循环)找到起点0的第一个满足条件的子数组:
[2,3,1,2]第2轮:起点 i=1
j=1: sum=3, 3<7 → 继续 j=2: sum=3+1=4, 4<7 → 继续 j=3: sum=4+2=6, 6<7 → 继续 j=4: sum=6+4=10, 10≥7 → 满足! 子数组[3,1,2,4],长度=4 result = min(4,4) = 4 break第3轮:起点 i=2
j=2: sum=1, 1<7 → 继续 j=3: sum=1+2=3, 3<7 → 继续 j=4: sum=3+4=7, 7≥7 → 满足! 子数组[1,2,4],长度=3 result = min(4,3) = 3 ← 更新为更短的 break第4轮:起点 i=3
j=3: sum=2, 2<7 → 继续 j=4: sum=2+4=6, 6<7 → 继续 j=5: sum=6+3=9, 9≥7 → 满足! 子数组[2,4,3],长度=3 result = min(3,3) = 3 break第5轮:起点 i=4
j=4: sum=4, 4<7 → 继续 j=5: sum=4+3=7, 7≥7 → 满足! 子数组[4,3],长度=2 result = min(3,2) = 2 ← 更新为更短的 break第6轮:起点 i=5
j=5: sum=3, 3<7 → 无法满足 内层循环结束,没有break最终结果
result = 2
解法二:滑动窗口法
java
class Solution {
// 滑动窗口
public int minSubArrayLen(int s, int[] nums) {
int left = 0;
int sum = 0;
int result = Integer.MAX_VALUE;
for (int right = 0; right < nums.length; right++) {
sum += nums[right];
while (sum >= s) {
result = Math.min(result, right - left + 1);
sum -= nums[left++];
}
}
return result == Integer.MAX_VALUE ? 0 : result;
}
}
具体解析:
什么是滑动窗口法呢,实际就是双指针,我们定义了两个可以自行移动的指针(移动的逻辑由我们控制),在这里,我们要先明确,在暴力解法中,是一个for循环滑动窗口的起始位置,一个for循环为滑动窗口的终止位置,用两个for循环 完成了一个不断搜索区间的过程。那么滑动窗口如何用一个for循环来完成这个操作呢。
我们在这个方法中只用到了一个for循环,问题来了,我们用for循环控制的是起始索引还是终止索引呢,如果是起始索引,那么从逻辑上来说跟第一种的暴力解法一致,因此显而易见,我们用for循环来控制终止索引

如图所示,我们通过for循环来控制 j,也就是终止位置
而代码部分的whlie循环,以这个终止位置为前提条件,进行判断和数组求和比对,如果不满足题目所要的子数组和大于目标值,就不执行for循环内部的while循环,这样就移动数组的终止位置,然后再进行while循环
当满足时,也就是子数组的和大于等于目标值,我们调用Math函数来对所要返回的result进行取小(题目要求的),后续的逻辑就是,既然我们已经满足了题目的第一个条件(大于等于目标值),那么我们就要处理第二个条件(长度最小),因此我们小缩小起始位置,如上面的动图,这也是while循环所做的判断,left++,这便是滑动窗口的精髓所在!滑动窗口的精髓就在于窗口的右边界不断右移,左边界通过 while 循环动态调整,从而在 O(n) 时间内找到全局最优解。
关于条件判断,我们为什么用while语句而不用if,道理很简单,但是很容易顺手写错,因为我们不知道要缩进几次才是最小的,while可以根据条件执行多次,只要是符合的,而if只根据条件执行一次。
更专业的说,
-
while:持续收缩,找到以当前 right 为结尾的最优解
-
if:只收缩一次,可能错过更优解
相关题目拓展:
题目背景:LeetCode 904
你正在探访一家农场,农场从左到右种植了一排果树。这些树用一个整数数组
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 :[1,2,1] - 有重复
示例2 :[0,1,2,2] - 有连续重复
示例3 :[1,2,3,2,2] - 混合分布
示例4 :[3,3,3,1,2,1,1,2,3,3,4] - 复杂随机分布
java
class Solution {
public int totalFruit(int[] fruits) {
Map<Integer, Integer> basket = new HashMap<>();
int left = 0;
int maxCount = 0;
for(int right=0;right<fruits.length;right++){
basket.put(fruits[right],basket.getOrDefault(fruits[right],0)+1);
while (basket.size() > 2) {
basket.put(fruits[left], basket.get(fruits[left]) - 1);
if (basket.get(fruits[left]) == 0) {
basket.remove(fruits[left]);
}
left++;
}
// 更新最大长度
maxCount = Math.max(maxCount, right - left + 1);
}
return maxCount;
}
}
题目讲解:
这道题的原理跟我们上面的那道题一样,只不过从数组变成了集合,目标是找到最长的连续子数组,其中最多包含两种不同的水果(元素),使用两个指针 left 和 right 维护一个窗口:
right 向右移动,扩大窗口(采摘新水果)
当窗口内水果种类超过2种时,移动 left 缩小窗口(放弃最早的水果)每次窗口调整后,记录当前窗口长度,取最大值
把当前水果加入窗口,并更新计数。
basket.put(fruits[right], basket.getOrDefault(fruits[right], 0) + 1);
getOrDefault 的作用:
如果这种水果已经在篮子里,获取当前数量
如果不在篮子里,返回默认值0
然后+1,表示又摘了一个
收缩过程:
-
获取左边界指向的水果种类:
fruits[left] -
将这种水果的数量减1(因为要移除一棵树)
-
如果数量变成0,从map中删除这个键(表示这种水果完全移出窗口)
-
left++,左边界右移一位 -
重复检查,直到种类数 ≤ 2
重要 :maxCount 的更新是在 while 循环之后,不是在之前
总结
| 问题 | 答案 |
|---|---|
| 收缩后的长度小于之前记录的长度怎么办? | 没关系,我们保留的是历史最大值 |
| 会不会丢失更长的合法窗口? | 不会,因为更长且合法的窗口在后续 right 移动中会被记录 |
| 为什么不先记录再收缩? | 因为可能记录到不满足条件的窗口 |
核心思想 :滑动窗口保证每次记录的都是当前 right 下,满足条件的最长窗口 ,而全局最大值通过 Math.max() 不断更新,不会丢失历史的最佳结果。
结语:如果对你有帮助,请点赞,关注,收藏,你的鼓励就是我最大的支持!