904. 水果成篮
用一个 Map 记录当前窗口的情况: key - 水果种类 value - 这个水果种类在当前滑动窗口里出现的次数
维持一个 left 指针到 right 指针的滑动窗口
每次 right 右移一位,将这个新加入窗口的 fruits[right] 种类放到 map 里,并将该种类计数 +1
如果当前窗口水果种类数,即 map 的 size 大于规定的 2
那么需要左指针右移,维持窗口内种类的数量不大于 2
思考下 [1,0,1,4,]1,4,1,2,3 这种情况
在 [1,0,1,4,] 这个窗口,凭人的判断应该将左指针移到 1 这里,窗口变成 [1,4]
但是怎么用代码来实现这个逻辑呢?
我刚开始只用了 Set ,没有用 Map。想窗口最左边的元素是 1,那 left 移动到第一个不等于 1 的地方就行了,显然这种情况不适用
后来又想从窗口最右边往左找第一个不等于 1 的地方,对于这种情况也是不适用的
后面看了题解,正确的做法应该是
left 每右移一格,map 里这个种类窗口计数 -1
并且每次 -1 后判断,当前 left 的种类窗口计数是否减到了0
减到0,说明窗口没有这个种类了,remove 掉
对于上面的例子,窗口 [1,0,1,4]
leftIndex = 0 Map {1:2, 0:1, 4:1}
leftIndex = 1 Map {1:1, 0:1, 4:1}
leftIndex = 2 Map {1:1, 0:0, 4:1} --> Map {1:1, 4:1}
窗口里只剩两个种类了,可以退出循环了,最后窗口为 [1,4,]
class Solution { public int totalFruit(int[] fruits) { int left=0; int right=0; int maxRes = Integer.MIN_VALUE; Map<Integer, Integer> curWinKind2CntMap = new HashMap(); while (right < fruits.length) { // 放入窗口右边的水果,并将计数 + 1 // 注意 getOrDefault curWinKind2CntMap.put(fruits[right], curWinKind2CntMap.getOrDefault(fruits[right], 0) + 1); // 当窗口内种类数 > 2 if (curWinKind2CntMap.size() > 2) { // left 左移,fruits[left] 种类数 -1 curWinKind2CntMap.put(fruits[left], curWinKind2CntMap.get(fruits[left]) - 1); // 如果减到0,说明没有了,移除掉 if (curWinKind2CntMap.get(fruits[left]) == 0) { curWinKind2CntMap.remove(fruits[left]); } left++; } // 窗口大小 int winLen = right - left + 1; // 与最大值比较 if (winLen > maxRes) { maxRes = winLen; } right++; } return maxRes; } }
栈 Stack 是 push 与 pop
队列 Queue 是 offer 与 poll
题解:
我们期望:
维持一个单调递减队列,队列内的元素都是当前窗口内的元素。队列首的元素是最大值,也是当前窗口的最大值
有两个问题:
(1)怎么来维持单调递减呢?也就是说窗口右移的时候,怎么把右边新加入窗口的元素加入到队列中去?
offer 操作:加入队末,前面有比它它小的,就移除,直到队列为空 或 遇到一个比它大的,才推进去
(2)怎么来保证队列中的元素都是窗口内的呢?也就是窗口右移的时候,怎么把左边新移出窗口的元素从队列中去除?
poll 操作:如果是普通的队列,队首的元素直接移除就好了。但是前面的 offer 操作比较特殊,可能窗口最左的元素在前面 offer 的时候因为小于队末新加元素已经被移除了,所以要将队首元素和窗口最左元素比较,如果相等,再将其移除。
这样 peek() 操作,直接获取队首元素,就是当前窗口的最大值。
注意:
- 以上操作都只是操作队列首和队列尾,所以用 LinkedList 比较合适,多用 getFirst() removeFirst() getLast() removeLast() 等操作,不要用下标操作
- 刚开始左右指针 left right 下标都是 0,要左指针不动,右指针右移 k 步完成窗口初始化
- 后面就是 while(right<nums.length)
// 将窗口右边的新元素加入单调递减队列 offer(nums[right++]); // 把窗口左边已经移出窗口的元素,也移出队列 pollIfEquals(nums[left++]); // 单调递减队列,队首元素即为当前窗口的的最大值 res[i++] = peek();
class Solution { LinkedList<Integer> queue; public Solution() { // 用链表类型的 List 来实现 queue = new LinkedList(); } public int[] maxSlidingWindow(int[] nums, int k) { // 最后结果的长度是 nums.length - k + 1 int resLen = nums.length - k + 1; int[] res = new int[resLen]; int left = 0; int right = 0; // 窗口初始化 for (int i=0;i<k;i++) { offer(nums[right++]); } res[0] = peek(); // 窗口右移 int i=1; while(right<nums.length) { // 将窗口右边的新元素加入单调递减队列 offer(nums[right++]); // 按理说应该把窗口左边已经移出窗口的元素,也移出队列 // 但是可能把右边新元素推到队列的过程中,就已经把这个左边的元素移出去了,所以要比较如果相等,才移出,否则 doNothing pollIfEquals(nums[left++]); // 单调递减队列,队首元素即为当前窗口的的最大值 res[i++] = peek(); } return res; } // 将 num 推到队列中,使其维持单调递减 private void offer(int num) { if (queue.isEmpty()) { queue.add(num); return; } // 前面有比它它小的,就移除,直到队列为空 或 遇到一个比它大的,才推进去 while (queue.size() > 0 && queue.getLast() < num) { queue.removeLast(); } queue.add(num); } // 如果队首元素是 n ,则弹出来。如果不是,do nothing private void pollIfEquals(int n) { if (peek() == n) { queue.removeFirst(); } } // 获取队首,因为是单调递减队列,所以也是最大值 private Integer peek() { return queue.getFirst(); } }
560. 和为 K 的子数组
假设 left 到 right 下标的子数组和为 k nums[left...right] = k preSum[right] - preSum[left] = k preSum[left] = preSum[right] - k 所以每次到 right 的时候,找到等于 preSum[right] - k 的 preSum[left] 有多少个
用一个 map 来记录,前缀和的 count (见官方题解动画)
key: 前缀和的值 value: 前缀和为这个值的个数
map 要放入一个初始值 {0,1}
class Solution { public int subarraySum(int[] nums, int k) { Map<Integer, Integer> leftPreSum2CntMap = new HashMap(); leftPreSum2CntMap.put(0, 1); int rightPreSum = 0; // 总的满足条件的子数组个数的计数,最后的结果 int count = 0; for (int right=0;right<nums.length;right++) { rightPreSum += nums[right]; // [...i] // left 到 right 的子数组和为 k // nums[left...right] = k // preSum[right] - preSum[left] = k // preSum[left] = preSum[right] - k // 所以每次到 right 的时候,找到有多少个前多少的个的和等于 preSum[right] - k Integer leftPreSumCnt = leftPreSum2CntMap.get(rightPreSum - k); if (leftPreSumCnt != null) { count += leftPreSumCnt; } Integer rightPreSumCnt = leftPreSum2CntMap.get(rightPreSum); if (rightPreSumCnt == null) { rightPreSumCnt = 0; } leftPreSum2CntMap.put(rightPreSum, ++rightPreSumCnt); } return count; } }
438. 找到字符串中所有字母异位词
滑动窗口的大小固定为 p 的长度
维持一个 int[] count = new int[26] 的数组,用来表示 当前滑动窗口内的字符串 与 p这个字符串 的字母数差异
count[i] 表示第 i 个字母的差异数,例如
- 出现在当前窗口的字母,每出现一次,count[该字母]++。出现在 p 中的字母,每出现一次,count[该字母]--。
- count[0]=4 表示 当前滑动窗口的字符串 比 p字符串 多4个字母a
- count[4]=-1 表示 当前滑动窗口的字符串 比 p字符串 少1个字母e
维持一个整数 differ 表示 当前滑动窗口的字符串 与 p字符串 在多少个字母上有差异。例如:
- count 中的非零值有 count[0]=4 count[4]=-1,则表示在字母 a、e 上有差异,differ=2
为什么需要 differ?
- 判断 当前滑动窗口字符串 是 字符串p 的同分异构词,是通过统计二者在所有字母的计数上,都没有相差实现的。如果没有 differ,那么每次都要遍历 count 数组。如果有 differ , 那么 differ 为 0 就表示没有差异。
过程:
初始窗口统计 l=0 r=0
- count 数组统计 s[0~pLen] 与 p[0~pLen] 的差异:++count[s.charAt(r)-'a'] --[p.charAt(r)-'a'] r++
- if (count[i]!=0) differ++ 统计有差异的字母数量。如果这时 differ==0,则位置0可以加入到结果中
窗口滑动过程:
字符 s[l] 要去掉
-
- 如果 count 数组中这个字符的差异计数为 1,去掉后正好为0,differ--;
- 如果 count 数组中这个字符的差异计数为 0,去掉后为 -1 不为 0,differ++;
- 把 count 数组中这个字符的计数 -1,--count[s.charAt(l)-'a']
- l++
字符 s[r] 要加进来
-
- 如果 count 数组中这个字符的差异计数为 -1,加进来或正好为0,differ--;
- 如果 count 数组中这个字符的差异计数为 0,加进来后为 1 不为 0,differ++;
- 把 count 数组中这个字符的计数 +1,++count[s.charAt(r)-'a']
- r++
如果 differ 为 0,则将 l (同分异构词在 s 中的起始位置)加入到结果 ans 中
class Solution { public List<Integer> findAnagrams(String s, String p) { int sLen = s.length(); int pLen = p.length(); if (sLen < pLen) { return new ArrayList<Integer>(); } List<Integer> ans = new ArrayList<Integer>(); int[] count = new int[26]; int l=0; int r=0; // 刚开始的窗口是 s 上的 0~pLen,统计初始窗口每个字母的差异 for (r=0;r<pLen;r++) { // 计数 s 的 ++count[s.charAt(r)-'a']; // p 有相同的就会抵消 --count[p.charAt(r)-'a']; } // 现在 l=0 r=pLen int differ=0; // 统计刚开始窗口的 differ(累加 count 数组) for (int i=0;i<26;i++) { if (count[i]!=0) { // 例如 s=aa p=bb count=[2,-2,0,0...] // 这时 differ 应该是 4 //differ+=Math.abs(count[i]); //----------------分割线------------------------------- // 不是的。differ 统计的是差的字母数。 // 比如多两个a,那么differ加一,少四个b,diifer也加一。为0时,differ才减一 ++differ; } } // 刚开始的窗口 s 上的 0~pLen, 就符合条件 if (differ == 0) { ans.add(0); } // differ 的引入:不用每次都遍历count数组看是否全0 while (r<sLen) { // 把 s.charAt(l) 减去。 // 原来count[s.charAt(l)-'a']==1 这个字母个数有一个不等的,这个字母一去,就相等了 if (count[s.charAt(l)-'a']==1) { --differ; } // 原来count[s.charAt(r)-'a']==0 这个字母是相等的,这个字母一去,少了一个,又不等了 else if (count[s.charAt(l)-'a']==0) { ++differ; } // 注意窗口的移动也会改变 count 数组 --count[s.charAt(l)-'a']; l++; // 把 s.charAt(r) 加进来 if (count[s.charAt(r)-'a']==-1) { --differ; } else if (count[s.charAt(r)-'a']==0) { ++differ; } ++count[s.charAt(r)-'a']; r++;
if (differ == 0) { ans.add(l); } } return ans; } }
30. 串联所有单词的子串
和上面的同分异构词类似,不同点:
- l 和 w 每次滑动的步长为单词长,而不是1
- 用 map 来统计差异,key:word value:这个word在当前窗口和words数组中的差异个数
下面这个错误例子,说明了 start 不只可以从 0 开始
所以要在外层加一层循环,start 从 0 到单词长
class Solution { public List<Integer> findSubstring(String s, String[] words) { List<Integer> ans = new ArrayList<>(); // 每个单词的长度 Integer wl = words[0].length(); // 单词的个数 Integer wn = words.length; // words[]串联单词串的长度 Integer allWl = wl*wn; if (s.length() < allWl) { return ans; } // l 不单可以从 0 开始,还可以从 0~wl 有 wl 种划分方式 // 但是 start 变量不可以直接用 l 变量代替,l 在后面还会变,会影响这里的 for 循环 // 所以要用一个新变量 start for (int start=0;start<wl;start++) { if (start+allWl > s.length() ) { return ans; } // key:单词 // value:s中这个单词与 words 中这个单词的数量差 // 出现在窗口中的单词,每出现一次,相应的值增加 1 // 出现在words中的单词,每出现一次,相应的值减少 1 Map<String, Integer> map = new HashMap(); // int l=0; // int r=0; // start 从 0~wl int l=start; int r=start; // s与words 有数量差异的单词 的 单词数 int differ=0; // 初始窗口相关的计算 l~l+wl while (r<(l+allWl)) { // 用 r 把单词加进来 String word = s.substring(r, r+wl); // 这时还没用 words 中的词抵消。所以都是多 1 map.put(word, map.getOrDefault(word, 0) + 1); // 一个一个单词的加,所以 r 前进的步长是wl, r从0开始以步长wl前进wn步到allWl r=r+wl; } for (String word : words) { // word 抵消掉一个。如果 s 中没有这个单词->0再减一就变成 -1 代表缺1 // 如果 s 中有这个单词->1再减一就变成0,二者这个单词的数量一致 map.put(word, map.getOrDefault(word, 0)-1); } for (Map.Entry<String, Integer> entry : map.entrySet()) { // differ 累计 s与words有数量差异的单词 的 单词数 if (entry.getValue() != 0) { differ++; } } // 可能刚开始那段就是 if (differ == 0) { ans.add(l); } // 不超过 while (r+wl <= s.length()) { // 要移走的单词 // 左指针这个单词是去掉的,map 里之前一定有 // 所以直接用 get 而不是 getOrDefualt String wordRemove = s.substring(l, l+wl); if(map.get(wordRemove) == 1) { differ--; } if(map.get(wordRemove) == 0) { differ++; } // 移走这个单词,这个单词的相差个数-1 map.put(wordRemove, map.get(wordRemove)-1); l=l+wl; // 右指针这个单词是新加进来的,map 里之前可能没有 // 所以所有 get 都要用 getOrDefualt String wordAdd = s.substring(r, r+wl); if(map.getOrDefault(wordAdd, 0) == -1) { differ--; } if(map.getOrDefault(wordAdd, 0) == 0) { differ++; } // 加入单词,这个单词的相差个数+1 map.put(wordAdd, map.getOrDefault(wordAdd, 0)+1); r=r+wl; if (differ == 0) { ans.add(l); } } } return ans; } }
76. 最小覆盖子串
和前面的 438.找到字符串中所有字母异位词 解法一样
用滑动窗口,维持当前窗口和字串 t 中的字符数差值
不同的是,前面那个题要求子串连续,滑动窗口的大小是固定的
而这个题要求覆盖即可,滑动窗口的大小是可变的,因此每个循环
- 如果当前窗口的字符个数可以覆盖子串的,那么 l 右移,看可不可以使窗口进一步缩短
- 如果当前窗口的字符个数不可以覆盖子串的,那么 r 右移,看新加进来的字符可不可使当前窗口可以覆盖
- 用 differ 计没有覆盖的字符数,只要 map 里的 字符差异计数>=0,就是可以覆盖的。<0 没覆盖, differ++
需要注意的是退出循环的条件,并不是 while(r<s.length())
例如:
s = "ADOBECODEBANC", t = "ABC" 答案是 BANC
当 r 指向最后一个的时候,l 还指向的是 2,也就是说当前窗口是 OBECODEBANC, 可以覆盖
但是此时循环还要继续,l 可以继续右移,使窗口进一步缩短,到最小覆盖子串 BANC
过程:
differ = 3 (A B C)
.........r右移
ADOBECODEBANC differ=2 (B C)
..........r右移
ADOBECODEBANC differ=0
ADOBECODEBANC differ=1 (A)
ADOBECODEBANC differ=1 (A)
...........r右移
ADOBECODEBANC differ=0
ADOBECODEBANC differ=0
...........l左移
ADOBECODEBANC differ=0
ADOBECODEBANC differ=1 (C)
...........r右移
ADOBECODEBANC differ=0
注意退出循环条件:上面这行 r 已经到末尾了,但是还不能退出循环,因为不是最短的,l要继续左移
...........l左移
ADOBECODEBANC differ=0
ADOBECODEBANC differ=1 (B)
dfiffer!=0 && r>=s.length() 退出循环
class Solution { public String minWindow(String s, String t) { Map<Character, Integer> map = new HashMap(); int l=0; int r=0; int differ = 0; int minLen = Integer.MAX_VALUE; String ans = ""; for (int i=0;i<t.length();i++) { char thischar = t.charAt(i); map.put(thischar, map.getOrDefault(thischar, 0)-1); } for (Map.Entry<Character, Integer> entry : map.entrySet()) { // 要可以覆盖的话,每个字符的差异 >=0 就可以了(刚好覆盖,或比它多) // 所以小于 0,就是没有覆盖到的字符 // 注意区别:之前是完全字母异位,这里的条件是==0 if (entry.getValue() < 0) { differ++; } } // 条件不能是 while (l<r) 因为lr初始值都是0,开始不了 while (true) { // 有没有覆盖到的字符(map中有字符的value是<0的) // r 右移,看新加进来的字符可不可以覆盖到 if (differ > 0 && r<s.length()) { char newchar = s.charAt(r); // +1 后变为0,这个字符可以覆盖了 if (map.getOrDefault(newchar, 0) == -1) { differ--; } // 如果 map.get(newchar) == 0,+1后为1,>=0就还是能覆盖,不会改变differ // 新进来的字符,计数+1 map.put(newchar, map.getOrDefault(newchar, 0)+1); r++; } // 都覆盖到了(map中所有字符value>=0) // l 左移,看窗口可不可以缩得更短 else if (differ == 0) { char removechar = s.charAt(l); // 本来刚好覆盖,移走后不能覆盖了 if (map.get(removechar) == 0) { differ++; } // 如果 map.get(newchar) == 1,-1后为0,>=0就还是能覆盖,不会改变differ // 新移走的字符,计数-1 map.put(removechar, map.getOrDefault(removechar, 0)-1); l++; } // r>=s.length() r 不能再移了, 以后只会右移 l 缩短窗口了 // 这时候如果 differ 还不为0,即还有每覆盖到的字符 // 继续移动l的话,只能去掉字符,不能加入字符,是没有用的 if (differ != 0 && r>=s.length()) { break; } if (differ == 0) { int len = r-l; if (len<minLen) { minLen = len; ans = s.substring(l, r); } } } return ans; } }
之前没有引入 differ 的解法
class Solution { public String minWindow(String s, String t) { String res; int resStartIndex=0; int resEndIndex=0; int resShortestLen = Integer.MAX_VALUE; Map<Character, Integer> curWinMap = new HashMap(); // 初始化 map, 放入 t 串每个字符的计数 for (int i=0;i<t.length();i++) { curWinMap.put(t.charAt(i), curWinMap.getOrDefault(t.charAt(i), 0) + 1); } int l = 0; int r = 0; while (true) { boolean winContainsAllt = winContainsAllt(curWinMap); if ((r-l) < resShortestLen && winContainsAllt) { resStartIndex = l; resEndIndex = r; resShortestLen = r-l; } if (winContainsAllt) { char lc = s.charAt(l); // 如果已经包含全部了。那么左边的出去,看能不能继续缩短一些 if (curWinMap.containsKey(lc)) { // 左边的出去了一个可以 cover t 串的,差距计数 +1 curWinMap.put(lc, curWinMap.get(lc) + 1); } l++; } else { // 如果 r 到末尾了,当前窗口可以覆盖,循环还是要继续往下走,看 l 能不能右移使窗口更缩短 // 如果 r 到末尾了,当前窗口的还不能覆盖,那 l 继续往右走后面更都不能覆盖了,所以可以退出循环了 if (r == s.length()) { break; } // 这个要在前面那个 break 的后面,防止数组越界 char rc = s.charAt(r); // 没有包含全部,右边的继续进来新的,看新来的能不能覆盖 if (curWinMap.containsKey(rc)) { // 右边新进来的这个可以 cover t 串,差距计数 -1 curWinMap.put(rc, curWinMap.get(rc) - 1); } r++; } } return s.subSequence(resStartIndex, resEndIndex).toString(); } private boolean winContainsAllt(Map<Character, Integer> curWinMap) { for (Map.Entry<Character, Integer> entry : curWinMap.entrySet()) { // 如果不够 cover 掉 t 里面的字符 // >0 不能 cover; =0 完全 cover ,字符计数相等; <0 完全 cover,并且还有多的 if (entry.getValue() > 0) { return false; } } // 全部 cover 的情况是每一项都 < 0 return true; } }
3. 无重复字符的最长子串
维持一个滑动窗口,使窗口内始终维持没有重复字符,计算此窗口的最大值
怎么实现滑动窗口内没有重复字符呢?
用一个 map ,记录每个字符上一次出现的位置
每次移动 r ,让右边新字符加进来,根据 map 获得这个新字符上次出现的位置
如果在当前窗口内(即>左指针l),说明在当前窗口内
因此要移动左指针到 上次出现的位置 + 1,把这个重复字符排出当前窗口内
class Solution { public int lengthOfLongestSubstring(String s) { if (s == null || "".equals(s)) { return 0; } int res = Integer.MIN_VALUE; int l = 0; int r = 0; // 记录字符上一次出现的位置。 Map<Character, Integer> char2LastIndexMap = new HashMap<>(); while (r<s.length()) { char rc = s.charAt(r); Integer rcLastIndex = char2LastIndexMap.get(rc); // 对于右边新加入的字符,要维持滑动窗口内无重复的 // 就要看它上次出现的位置是不是在窗口内,也就是说它上次出现的位置 > l左指针 // 如果在窗口内,就将 l 移动到它上次出现的位置 +1 if (rcLastIndex != null && rcLastIndex >= l) { l = rcLastIndex + 1; } // 为什么要 + 1,应为这里已经把 r 当成新加进来的了 if (r-l+1 > res) { res = r-l+1; } char2LastIndexMap.put(rc ,r); r++; } return res; } }
209. 长度最小的子数组
minLen 初始值 nums.length+1(一个不可能的大值)
滑动窗口,右指针负责新加进来元素,满组 sum>=target 的条件,就更新 minLen 为当前长度
然后 while(sum<target) 移动左指针,使得 sum 重新小于 target,注意每次移动左指针的时候,进了 while(sum<target) 说明满足这个条件,因此也要更新 minLen
如果最后的 minLen 仍然大于 nums.length,说明仍然为初始值,没有被更新过,没有符合条件的,返回0
public int minSubArrayLen(int target, int[] nums) { int minLen = nums.length+1; int l = 0; int r = 0; int sum = 0; // 为什么条件里没有 l<=r // 因为使 l 发生移动的都是 sum>=target, l 移动到 < 就好了, 有 sum 在控制,所以不会超过 r while (r<nums.length) { sum = sum + nums[r]; if (sum >= target) { minLen = Math.min(minLen, r-l+1); } while (sum >= target) { // 能进来这里的一定满足 sum>=target, 所以可以直接更新 minLen // 但是注意一定要在 l++ 前, l++ 之后可能就不满足 sum>=target 了,该退出循环了 minLen = Math.min(minLen, r-l+1); // l右移 sum = sum-nums[l]; l++; } r++; } // 到最后, minLen 仍然是初始值 nums.length+1, 没有被更新, 说明没有符合条件的答案 if (minLen > nums.length) { minLen = 0; } return minLen; }
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· DeepSeek “源神”启动!「GitHub 热点速览」
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器