目录
系列文章目录
刷题笔记(一)–数组类型:二分法
刷题笔记(二)–数组类型:双指针法
题录
209.长度最小的子数组
链接:209.长度最小的子数组
对应题目截图如下:
这个题目有两种解法,第一种就是暴力解法,我直接遍历整个数组,然后得出每一个下标对应的大于target的窗口长度是多少,然后取最小的那一个。
class Solution {
public static int minSubArrayLen(int target, int[] nums) {
int left = 0;//定义左指针下标
int right = 0;//定义右指针下标
int lengthRs = target;//定义最小的长度
boolean flagMe = false;
for (left = 0; left < nums.length; left++) {
int res = nums[left];//记录当前的和
int lengthNow = 1;//定义当前的长度
right = left + 1;//右指针不能和左指针同一个下标
//如果说当前和小于target或者说右指针越界了就不能循环了
while(res < target && right < nums.length){
res += nums[right];
lengthNow++;
right++;
}
boolean flag = false;
if(res >= target){
//进行一个标志位判断,看一下最后结果是不是大于target
flag = true;
flagMe = true;
}
//进行判断,看一下当前的长度和当前记录的最小长度那个小
if(flag && lengthRs > lengthNow){
lengthRs = lengthNow;
}
}
if(flagMe){
return lengthRs;
}
return 0;
}
}
但是可以发现,这种解法的效率不是很高
所以这里我们就延伸出了,解这个题的另外一种方式–滑动窗口法,注意看下图指针移动的过程。
刚开始都是起始位置:
然后j下标到2时,窗口和就大于7
这个时候我们就移动i指针往前一位
然后此时窗口和小于7,那就继续移动j指针
这个时候和又大于7,就移动i指针。
所以当大于target,就移动i指针,如果小于target就移动right指针。一直到j遍历完整个数组。所以对应解法如下:
public int minSubArrayLen(int target, int[] nums) {
int left = 0;
int sum = 0;//定义当前的窗口内的值的和
int lengthRs = Integer.MAX_VALUE;//定义窗口长度,这里取最大值
for (int right = 0; right < nums.length; right++) {//定义右指针为right,然后开始遍历
//每次都把结果加到sum上
sum += nums[right];
while(sum >= target){//如果窗口内的和大于target,就不能再继续移动right指针了,就要准备移动left指针
lengthRs = Integer.min(lengthRs,right - left + 1);//这里更新下标长度
sum -= nums[left];//当前窗口值大于target,所以减去左边指针的值
left++;//更新left指针
}
}
//如果是最后长度是Integer.MAX_VALUE,那就证明窗口所有的和加起来都小于target
return lengthRs == Integer.MAX_VALUE ? 0 : lengthRs;
}
暴力解法的时间复杂度是O(n^2),因为最差情况就是一个等差数列的和。
而滑动窗口时间复杂度是O(n),因为每一个元素都最多被遍历两次,也就是O(2n),去掉常数就是O(n)。
904. 水果成篮
链接:904.水果成篮
对应题目截图如下:
这道题是什么意思呢?
一句话总结一下:求只出现两个元素的最大子区间。
这一题和上一题相比,增加了对区间元素种类的控制。这题属于滑动窗口当中的计数问题,用arr数组来记录两个篮子中出现的水果数目,count来记录水果的种类,用count来控制窗口的滑动。如果说count <= 2,那么right就可以一直往右走;如果说count > 2,那就是left指针往右走。一边移动一变更新arr数组中对应的水果数目。
class Solution {
public int totalFruit(int[] fruits) {
int left = 0;
int n = fruits.length;
if(n < 2){
return n;
}
int max = 2;//这里设置最长区间的下标,2就是最小值了
int[] arr = new int[n];//创建一个数组用来记录每个数组出现的次数
int count = 0;//用count来控制数组里面元素不为0的个数,也就是只能用两个篮子来装水果
for (int right = 0; right < n; right++) {
if (arr[fruits[right]] == 0){
count++;//如果这个水果之前没有出现过,那么证明就是第一次入篮,count就+1
}
arr[fruits[right]] += 1;//更新每个水果对应出现次数
while(left < n && count > 2){
arr[fruits[left]]--;
//这里注意了,因为我们这里还要用left来判断count,所以不能后面不能直接跟left++
if(arr[fruits[left]] == 0) count--;
left++;
}
max = Math.max(max,right - left + 1);//取连续下标区间最长的
}
return max;
}
}
76. 最小覆盖子串
链接:76. 最小覆盖子串
截屏如下:
这个题,怎么说呢,和上面的题一样的思路,但是不一样的就是对count值的控制,说一下整体的思路吧:
一个一个来
这就是整体思路,最后判断一下特殊情况就好(看return行)
public class 最小覆盖子串 {
public String minWindow(String s, String t) {
int left = 0;//定义最后的左指针下标
int right = 0;//定义最后的右指针下标
int leftNow = 0;//当前的当前的左指针下标,用来求最小子串
int count = 0;//用来控制窗口的滑动
int minSize = Integer.MAX_VALUE;//记录最小子串的长度
Map<Character,Integer> map = new HashMap<>();//用一个map表来映射对应的字符出现次数
for(Character c:t.toCharArray()){
//统计字符出现的次数
map.put(c,map.getOrDefault(c,0) + 1);
}
for (int rightNow = 0; rightNow < s.length(); rightNow++) {
Character c = s.charAt(rightNow);
if(map.containsKey(c)){
//记录当前字符对应的value值
int cnt = map.get(c);
map.put(c,cnt - 1);
//如果cnt - 1 == 0,那就证明当前这个字符已经完全的在子串当中
if(cnt == 1) count++;
}
//能进入while循环,就证明当前字符已经完全在子串中了
while(count >= map.size()){
//这个时候就要判断一下要不要更新子串
if(minSize > (rightNow - leftNow + 1)){
right = rightNow;
left = leftNow;
minSize = right - left + 1;
}
Character rightC = s.charAt(leftNow);
if(map.containsKey(rightC)){
int cnt = map.get(rightC);
map.put(rightC,cnt + 1);
//如果说cnt == 0,那就意味着什么呢?意味着原先字符是完全在子串中的,经过下面的leftNow++后就不在了
if(cnt == 0){
count--;
}
}
leftNow++;
}
}
return minSize == Integer.MAX_VALUE ? "" : s.substring(left,right+1);
}
}
总结
有没有发现,其实所谓的滑动窗口本质上还是一种双指针,解题思路其实大致还是可以使用我们的双指针模板,但是只是有相似之处,可以借鉴,这个和双指针还是有点区别的。
(自己用自己图不用去水印了吧~)
这里是我们之前的模板,但是这里我们这个模板就需要更换了
这里大致是需要这个模板的,但是记住了,不能太死。因为你最后实际i和j的值肯定是不在这个位置的。上面这个模板指针的位置是你最后正确答案的位置,什么意思呢?这样解释,逻辑位置和实际位置,我们的逻辑位置,就是我们要知道这个位置是正确的位置也是我们最后要返回的位置,实际地址,是我们实际上代码中i和j的位置。是不是还是有点抽象?(PS:这里好好理解一下,我对于这个概念的处理贯穿后续的内容)
我用上面76题的对应指针来进行讲解。
这两个就是逻辑指针的位置:
int left = 0;//定义最后的左指针下标
int right = 0;//定义最后的右指针下标
这两个就是实际指针的位置:
int leftNow = 0
int rightNow = 0
我们需要用实际指针位置来判断逻辑指针的位置,需要用实际指针来更新逻辑指针的位置。而且其实逻辑指针是可以不存在的!!!这里说一下,是可以不存在的!!
什么意思呢?
那说完了整体的思想,来说一下准确的实现。我们写滑动窗口的题主要是要记住一下几点:
一点一点来解答(用209题做具体模板,以下说法参考209题)
所有滑动窗口的题具体的精髓就在于如果移动这个窗口
这个玩意太暧昧了,但是就是一个我们当前逻辑指针记录的答案,和实际指针记录的答案的一个比较,然后如果说要扩充范围,就移动窗口结束位置。如果要缩小范围,就移动窗口起始位置。
下面给出代码模板
int left; //定义实际指针的位置
/*这里做对应题目的处理,1.我们要不要使用逻辑指针,如果使用我们就定义。但是你使用不
使用,你都要根据实际题目的要求来确定,如果方便就是使用。比如int变量求长度(比如209)
,比如String变量记录最短字符串(比如76)
2.确定我们中间要用哪一些处理来进行我们窗口滑动的判断
*/
for(int right = 0;;){//遍历右指针
//动态更新实际窗口数据,只针对右指针
while(实际答案已经得到,满足了窗口滑动条件,准备和我们的逻辑答案进行判断){
//接着就是判断语句,是否要更新(位置不固定,根据题目自己选择)
//更新当前实际窗口的值,逻辑上移动left指针,也就是left++。(为什么是逻辑上?因为这个时候可能判断语句在中当前这条语句的下面)
//实际上在移动left,指针,也就是left++
}
}
return 正确答案;
所以其实也不难总结,我们实际上难点在下面