算法通关村第十六关——滑动窗口经典问题(白银)
1 最长子串专题
1.1 无重复字符的最长子串
思路:
- 什么情况下,left需要调整位置?当right的值,等于,left的值时。那么,我们需要记录这个值的数据结构。
- 选什么作为记录值的数据结构,还方便查询是否存在呐?使用key,value,也就是Hashmap,方便,好用。
- 如果遇到了存在的值,那么left怎么移动?每移动一次,判断一次是否还有相等的数即可
- 其他跟青铜里的最后一题大差不差
class Solution {
public int lengthOfLongestSubstring(String s) {
HashMap<Character, Integer> map = new HashMap<>();
int left = 0;
int len = 0;
int res = 0;
for(int right = 0; right < s.length(); right++){
while(map.containsKey(s.charAt(right))){
map.remove(s.charAt(left));
left++;
}
map.put(s.charAt(right), 1);
len = right - left + 1;
res = Math.max(res, len);
}
return res;
}
}
不过这样写之后有多余的计算,在while循环里面,一个个移动,那么怎样一次性直接将left移动到位置,也就是重复值的位置,的下一个位置。
我们用了key,value的数据结构,所以可以存key的时候,设置value为当前的位置,这样移动就可以直接找到位置
class Solution {
public int lengthOfLongestSubstring(String s) {
// 如果字符串s为空,则直接返回0
if (s.length() == 0) {
return 0;
}
// 创建一个HashMap对象,用于存储字符和其在字符串中最后一次出现的索引
HashMap<Character, Integer> map = new HashMap<Character, Integer>();
// 初始化变量max为0,表示最长不重复子串的长度
int max = 0;
// 初始化左指针left为0,表示子串的起始位置
int left = 0;
// 使用for循环遍历字符串s,从左到右逐个字符进行处理
for (int right = 0; right < s.length(); right++) {
// 如果map中包含当前字符,表示该字符已经在当前子串中出现过
if (map.containsKey(s.charAt(right))) {
// 更新左指针left为当前重复字符的下一个位置(即map.get(s.charAt(right)) + 1)
left = Math.max(left, map.get(s.charAt(right)) + 1);
}
// 将当前字符添加到map中,并用右指针right作为值
map.put(s.charAt(right), right);
// 更新最长不重复子串的长度为右指针right与左指针left之间的距离加1(即当前子串的长度)
max = Math.max(max, right - left + 1);
}
// 返回最长不重复子串的长度
return max;
}
搞定~
1.2 至多包含两个不同字符的最长子串
没有会员,略~~
代码给上,自行理解:
注意!!!md!!没看清题目,原来是找至多包含两个不同字符,是不同字符,不是数量,靠!!
public int lengthOfLongestSubstringTwoDistinct(String s) {
// 如果字符串长度小于3,直接返回字符串长度
if (s.length() < 3) {
return s.length();
}
// 初始化左指针和右指针为0,创建HashMap来存储字符和其位置的映射关系
int left = 0, right = 0;
HashMap<Character, Integer> hashmap = new HashMap<>();
// 初始化最大长度为2
int maxLen = 2;
while (right < s.length()) {
// 如果HashMap的大小小于3,表示当前窗口中的字符种类还没有达到3个
// 将当前字符及其位置加入HashMap,并将右指针向右移动一位
if (hashmap.size() < 3) {
hashmap.put(s.charAt(right), right++);
}
// 如果HashMap的大小等于3,表示当前窗口中的字符种类已经达到了3个
if (hashmap.size() == 3) {
// 找到要删除的字符位置,即HashMap中值最小的位置
int del_idx = Collections.min(hashmap.values());
// 删除该位置对应的字符及其位置
hashmap.remove(s.charAt(del_idx));
// 更新左指针的位置为要删除的位置的下一个位置
left = del_idx + 1;
}
// 更新最大长度为当前窗口的长度和之前的最大长度中的较大值
maxLen = Math.max(maxLen, right - left);
}
return maxLen;
}
1.3 至多包含k个不同字符的最长子串
没有会员,略~~
直接把2改成K即可,过程代码不写了
2 长度最小的子数组
这题比较经典了,做了好多次了,主要就是多了一个判断,跟前面差不多
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int left = 0;
int sum = 0;
int minLen = Integer.MAX_VALUE;
for (int right = 0; right < nums.length; right++) {
sum += nums[right];
while (sum >= target) {
minLen = Math.min(minLen, right - left + 1);
sum -= nums[left];
left++;
}
}
return minLen == Integer.MAX_VALUE ? 0 : minLen;
}
}
3 盛最多水的容器
这题主要理解两个地方:
- 面积的计算
- 短板和长版与面积的关系
本题看似复杂,但其实简单的很。设两指针 i , j ,指向的水槽板高度分别为 h[i] , h[j] ,此状态下水槽面积为S(i,j) 。由于可容纳水的高度由两板中的 短板 决定,因此可得如下面积公式 : S(i,j)=min(h[i],h[j])×(j−i)
在每个状态下,无论长板或短板向中间收窄一格,都会导致水槽底边宽度−1 变短:
-
若向内移动短板 ,水槽的短板min(h[i],h[j]) 可能变大,因此下个水槽的面积可能增大 。
-
若向内移动长板 ,水槽的短板min(h[i],h[j]) 不变或变小,因此下个水槽的面积一定变小 。
因此,只要初始化双指针分列水槽左右两端,循环每轮将短板向内移动一格,并更新面积最大值,直到两指针相遇时跳出;即可获得最大面积。
public int minSubArrayLen(int target, int[] nums) {
int i = 0, j = height.length - 1, res = 0;
while(i < j) {
res = height[i] < height[j] ?
Math.max(res, (j - i) * height[i++]):
Math.max(res, (j - i) * height[j--]);
}
return res;
}
4 寻找子串异位词
4.1 字符串的排列
本题因为字符串s1的异位词长度一定是和s2字符串的长度一样的,所以很自然的想到可以以s1.length()为大小截图一个固定窗口,然后窗口一边向右移动,一边比较就行了。此时可以将窗口内的元素和s1先做一个排序,然后再比较即可,但是这样做的问题是排序代价太高了,我们需要考虑性能更优的方法。
所谓的异位词不过两点:字母类型一样,每个字母出现的个数也是一样的。
-
题目说s1和s2都仅限小写字母,因此我们可以创建一个大小为26的数组,每个位置就存储从a到z的个数,
-
为了方便操作,索引我们使用index=s1.charAt(i) - ‘a’ 来表示,这是处理字符串的常用技巧。
-
此时窗口的right向右移动就是执行:
charArray2[s2.charAt(right) - 'a']++;
-
而left向右移动就是执行:
int left = right - sLen1; charArray2[s2.charAt(left) - 'a']--;
public boolean checkInclusion(String s1, String s2) {
int sLen1 = s1.length(), sLen2 = s2.length();
if (sLen1 > sLen2) {
return false;
}
// 初始化两个长度为26的整型数组,用于记录每个字符出现的次数
int[] charArray1 = new int[26];
int[] charArray2 = new int[26];
// 统计s1和s2前sLen1个字符中每个字符出现的次数
for (int i = 0; i < sLen1; ++i) {
// 计算字符在字母表中的位置,并将该位置上的计数加1
++charArray1[s1.charAt(i) - 'a'];
++charArray2[s2.charAt(i) - 'a'];
}
// 判断s1是否为s2的子串
if (Arrays.equals(charArray1, charArray2)) {
return true;
}
// 滑动窗口遍历s2,判断是否存在匹配的子串
for (int right = sLen1; right < sLen2; ++right) {
// 右移窗口,即将右边界向右移动一个位置
charArray2[s2.charAt(right) - 'a']++;
// 左移窗口,即将左边界向右移动一个位置
int left = right - sLen1;
charArray2[s2.charAt(left) - 'a']--;
// 判断s1是否为当前窗口内的子串
if (Arrays.equals(charArray1, charArray2)) {
return true;
}
}
return false;
}
4.2 找到字符串中所有字母异位词
这道题跟上面几乎一模一样,唯一不同的是需要用一个List,如果出现异位词,还要记录其开始位置,那直接将其add到list中就可以了。
public List<Integer> findAnagrams(String s, String p) {
int sLen = s.length(), pLen = p.length();
if (sLen < pLen) {
return new ArrayList<Integer>();
}
// 初始化两个长度为26的整型数组,用于记录字符串中每个字符出现的次数
int[] sCount = new int[26];
int[] pCount = new int[26];
// 统计p和s的前pLen个字符中每个字符出现的次数
for (int i = 0; i < pLen; i++) {
// 计算字符在字母表中的位置,并将该位置上的计数加1
sCount[s.charAt(i) - 'a']++;
pCount[p.charAt(i) - 'a']++;
}
// 判断是否存在字母异位词的起始索引
List<Integer> ans = new ArrayList<Integer>();
if (Arrays.equals(sCount, pCount)) {
ans.add(0);
}
// 滑动窗口遍历s,判断是否存在字母异位词的起始索引
for (int left = 0; left < sLen - pLen; left++) {
// 左移窗口,即将左边界向右移动一个位置
sCount[s.charAt(left) - 'a']--;
// 右移窗口,即将右边界向右移动一个位置
int right = left + pLen;
sCount[s.charAt(right) - 'a']++;
// 判断当前窗口内的字符是否与p中字符的出现次数相同
if (Arrays.equals(sCount, pCount)) {
// 上面left多减了一次,所以需要加1
ans.add(left + 1);
}
}
return ans;
}
over~