前言
兄弟们,互联网寒冬期,算法刷着走。上篇文章讲了双指针的左右指针,双指针是数组类算法题中最重要的一个分支之一。这篇文章讲双指针技巧的滑动窗口。遇到双指针的题目,直接套用模板就完事儿。另外,数组有下图这些知识点与技巧。
思路
滑动窗口一般用于解决主串是否满足子串的某些需求问题,比如,是否包含某个字串,是否含有字串的所有字符等。 滑动窗口有固定的解题模板。但是细节众多,需要反复练习。
//s为原字符串,t为子字符串
public void slidingWindow(String s, String t) {
//初始化窗口应该满足的条件needMap,key=子串t中的字符,value=字符在t字符串中出现的次数
Map<Character, Integer> needMap = new HashMap<>();
for (int i = 0; i < t.length(); i++) {
Character k = t.charAt(i);
needMap.put(k, needMap.getOrDefault(k, 0) + 1);
}
//滑动窗口中包含的字符,及其数量,key=滑动窗口中的字符,value=字符出现的次数
Map<Character, Integer> winMap = new HashMap<>();
//l:窗口左指针,r窗口右指针,validCount:窗口中有多少个字符满足了条件。
int l = 0, r = 0, validCount = 0;
while (r < s.length()) {
//即将加入窗口的字符
char in = s.charAt(r);
//窗口右指针右移
r++;
//进行窗口内数据的操作
...
while (窗口是否应该收缩) {
//即将从窗口移除的字符
char out = s.charAt(l);
//窗口左指针右移
l++;
//进行窗口内数据的操作
...
}
}
}
最小覆盖子串
解题思路
套用滑动窗口模板。窗口左右指针下标从0开始遍历原字符串。
窗口的右指针右移后,判断加入窗口的字符是否是子串的字符,若是则更新winMap。更新后若winMap中该字符的value=needMap中该字符的value,说明滑动窗口中该字符数量与子串该字符的数量相等,则validCount+1。
当validCount=needMap.size()时,说明子串中的字符全部都包含在了滑动窗口中,且每个字符的字符数量也满足。这时就把窗口的左指针右移,直到窗口中的字符串不在符合要求为止。
重复上诉步骤,找出最小的窗口。
复杂度分析
时间复杂度:O(nlogz+mlogz),z是字符集的大小,n是原字符串的长度,m是子串长度。最坏情况下,窗口右指针会扫描一遍原数组,窗口左指针会扫描一边原数组,所以最多扫描2n次,而每扫描一次,就有若干次的map.get与map.put,则复杂度是nlogz。初始化needMap时,需要扫描一遍子串,且也有map.get与map.put。则复杂度为O(nlogz + mlogz)。
空间复杂度:O(z)。因为需要建立两个map分别存滑动窗口与字串中字符的出现次数,且每个map的键值对最多不会存放超过字符集的大小。
代码
class Solution {
public String minWindow(String s, String t) {
Map<Character, Integer> needMap = new HashMap<>();
for (int i = 0; i < t.length(); i++) {
Character k = t.charAt(i);
needMap.put(k, needMap.getOrDefault(k, 0) + 1);
}
Map<Character, Integer> winMap = new HashMap<>();
int l = 0, r = 0, validCount = 0, start = 0, minWin = Integer.MAX_VALUE;
while (r < s.length()) {
char in = s.charAt(r);
r++;
if (needMap.containsKey(in)) {
winMap.put(in, winMap.getOrDefault(in, 0) + 1);
//窗口中该字符的数量等于字串中该字符的数量
if (winMap.get(in).equals(needMap.get(in))) {
validCount++;
}
}
//窗口中所有字符的数量都大于等于字串中字符的相应数量,说明这是一个满足题意的子串
while (validCount == needMap.size()) {
//记录满足条件时的最小窗口
if (r - l < minWin) {
start = l;
minWin = r - l;
}
char out = s.charAt(l);
l++;
if (needMap.containsKey(out)) {
winMap.put(out, winMap.getOrDefault(out, 0) - 1);
//当窗口中该字符的数量小于了字串中的数量
if (winMap.get(out) < needMap.get(out)) {
validCount--;
}
}
}
}
return Integer.MAX_VALUE == minWin ? "" : s.substring(start, start + minWin);
}
}
字符串的排列
解题思路
套用滑动窗口模板。窗口左右指针下标从0开始去遍历s2。
窗口的右指针右移后,判断加入窗口的字符是否是s1的字符,若是则更新winMap。更新后若winMap中该字符的value=needMap中该字符的value,说明滑动窗口中该字符数量与s1中的该字符的数量相等,则validCount+1。
当validCount=needMap.size()时,说明s1中的字符全部都包含在了滑动窗口中。此时进行判断,当窗口的长度=s1的长度时,说明是一个合法序列,则返回true。
重复上诉步骤,若遍历结束后都没找到合法序列,则返回false。
复杂度分析
时间复杂度:O(nlogz + mlogz),z是字符集的大小。n 是s1字符串的长度,m是s2字串长度。最坏情况下,窗口右指针会扫描一遍s2,窗口左指针会扫描一遍s2,所以最多扫描2m次,而每扫描一次,就用若干次的map.get与map.put,则复杂度是mlogz。初始化needMap时,需要扫描一遍s1,每次扫描也有map.get与map.put。则复杂度为O(nlogz + mlogz)。
空间复杂度:O(z)。因为需要建立两个map分别存滑动窗口与字串中字符的出现次数,且每个map的键值对最多不会存放超过字符集的大小。
代码
class Solution {
public boolean checkInclusion(String s1, String s2) {
Map<Character, Integer> needMap = new HashMap<>();
for (int i = 0; i < s1.length(); i++) {
Character k = s1.charAt(i);
needMap.put(k, needMap.getOrDefault(k, 0) + 1);
}
Map<Character, Integer> winMap = new HashMap<>();
int l = 0, r = 0, validCount = 0;
while (r < s2.length()) {
char in = s2.charAt(r);
r++;
if (needMap.containsKey(in)) {
winMap.put(in, winMap.getOrDefault(in, 0) + 1);
if (winMap.get(in).equals(needMap.get(in))) {
validCount++;
}
}
while (validCount == needMap.size()) {
if (r - l == s1.length()) {
return true;
}
char out = s2.charAt(l);
l++;
if (needMap.containsKey(out)) {
winMap.put(out, winMap.getOrDefault(out, 0) - 1);
if (winMap.get(out) < needMap.get(out)) {
validCount--;
}
}
}
}
return false;
}
}
找到字符串中所有字母异位词
解题思路
套用滑动窗口模板。窗口左右指针下标从0开始去遍历s。
窗口的右指针右移后,判断加入窗口的字符是否是p的字符,若是则更新winMap。更新后若winMap中该字符的value=needMap中该字符的value,说明滑动窗口中该字符数量与p中的该字符的数量相等,则validCount+1。
当validCount=needMap.size()时,说明p中的字符全部都包含在了滑动窗口中。此时进行判断,当窗口的长度=s的长度时,说明是一个合法序列,则加入列表中。
重复上诉步骤,找出所有的合法序列。
复杂度分析
时间复杂度:O(nlogz + mlogz),n是s字符串的长度,m是p字串长度。最坏情况下,窗口右指针会扫描一遍s,窗口左指针会扫描一遍s,所以最多扫描2n次,而每扫描一次,就用若干次的map.get与map.put,则复杂度是nlogz。初始化needMap时,需要扫描一遍p,每次扫描也有map.get与map.put。则复杂度为O(nlogz + mlogz)。
空间复杂度:O(z)。z是字符集的大小。因为需要建立两个map分别存滑动窗口与字串中字符的出现次数,且每个map的键值对最多不会存放超过字符集大小。
代码
class Solution {
public List<Integer> findAnagrams(String s, String p) {
Map<Character, Integer> needMap = new HashMap<>();
for (int i = 0; i < p.length(); i++) {
Character k = p.charAt(i);
needMap.put(k, needMap.getOrDefault(k, 0) + 1);
}
Map<Character, Integer> winMap = new HashMap<>();
List<Integer> list = new ArrayList<>();
int l = 0, r = 0, validCount = 0;
while (r < s.length()) {
char in = s.charAt(r);
r++;
if (needMap.containsKey(in)) {
winMap.put(in, winMap.getOrDefault(in, 0) + 1);
if (winMap.get(in).equals(needMap.get(in))) {
validCount++;
}
}
while (validCount == needMap.size()) {
if (r - l == p.length()) {
list.add(l);
}
char out = s.charAt(l);
l++;
if (needMap.containsKey(out)) {
winMap.put(out, winMap.get(out) - 1);
if (winMap.get(out) < needMap.get(out)) {
validCount--;
}
}
}
}
return list;
}
}
无重复字符的最长子串
解题思路
此题不能用常规的滑动窗口模板。由于没有子串,所以不需要needMap去统计子串的字符,也不需要validCount统计窗口内满足子串相关条件的字符数。
窗口左右指针下标从0开始去遍历s。窗口的右指针右移后,将加入窗口的字符更新进winMap。
当winMap中刚加入窗口的字符的value大于1,则说明窗口中有重复的字符了。开始右移左指针,并实时更新winMap,直到winMap中刚加入窗口的字符的value不大于1。此时窗口长度就是一个无重复的子串长度。
重复上诉步骤,找出最大子串长度。
复杂度分析
时间复杂度:O(nlogz),n 是s字符串的长度。最坏情况下,窗口右指针会扫描一遍s,窗口左指针会扫描一遍s,所以最多扫描2n次。而每扫描一次,就用若干次的map.get与map.put,则复杂度为O(nlogz)。
空间复杂度:O(z)。z是字符集的大小。因为需要建立一个map存滑动窗口中字符的出现次数,且map的键值对最多不会存放超过z是字符集的大小。
代码
class Solution {
public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> winMap = new HashMap<>();
int l = 0, r = 0, max = Integer.MIN_VALUE;
while (r < s.length()) {
char in = s.charAt(r);
r++;
winMap.put(in, winMap.getOrDefault(in, 0) + 1);
while (winMap.get(in) > 1) {
char out = s.charAt(l);
l++;
winMap.put(out, winMap.get(out) - 1);
}
max = Math.max(r - l, max);
}
return max == Integer.MIN_VALUE ? 0 : max;
}
}
结尾
滑动窗口套路固定,遇到类型题时,直接模板默写代码,屡试不爽。 下篇文章讲二分搜索。
感谢各位人才的点赞、收藏和评论,干货文章持续更新中,下篇文章再见!