携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第13天,点击查看活动详情
一、前言
滑动窗口算法的思路: 维护一个窗口,不断滑动,更新答案。
// 大致逻辑如下:
int left = 0, right = 0;
while (right < s.size()) {
// 1. 增大窗口(右边)
window.add(s[right]);
++right;
// 2. 缩小窗口(左边)
while (window needs shrink) {
window.remove(s[left]);
++left;
}
}
需要思考 4 个问题:
- 移动
right
扩大窗口时,需要更新什么? - 什么时候开始移动
left
缩小窗口? - 移动
left
缩小窗口时,需要更新什么? - 结果是在扩大窗口时更新,还是在缩小窗口时更新?
二、题目
(1)最长无重复子串(中)
题干分析
给定一个字符串 s
,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
思路解法
思路:滑动窗口
- 定义
left
和right
: 滑动窗口的左右两边。
-
定义
counts[256]
数组: 记录字符出现的次数。- 如果
counts[c] > 1
,说明当前窗口中存在重复字符,就需要移动left
缩小窗口了。
- 如果
// Time: O(n), Space: O(m), m 是字符集大小, Faster: 92.19%
public int lengthOfLongestSubstring(String s) {
int [] counts = new int[256]; // 记录字符出现的次数
int left = 0, right = 0; // 定义滑动窗口左右指针
int ans = 0; // 记录结果
while (right < s.length()) {
char c = s.charAt(right);
++right; // 右指针右移动 +1
++counts[c]; // 更新窗口内数据
// 判断左侧窗口是否要收缩
while (counts[c] > 1) {
char d = s.charAt(left);
++left;
// 更新窗口内数据
--counts[d];
}
// 更新最大值
ans = Math.max(ans, right - left);
}
return ans;
}
优化:滑动窗口,避免不必要判断
- 当遇到重复字符时: 左侧窗口
left
不再一个个移动,而是直接移动到重复字符的下一位。 index[256]
数组: 记录字符当前最大小标。
// Time: O(n), Space: O(m),m 是字符集大小, Faster: 92.19%
public int lengthOfLongestSubstring1N(String s) {
int[] index = new int[256];
Arrays.fill(index, -1); // 初始,下标均为 -1
int maxLen = 0;
for (int left = 0, right = 0; right < s.length(); ++right) {
left = Math.max(index[s.charAt(right)] + 1, left); // 左侧边下标
maxLen = Math.max(maxLen, right - left + 1); // 比较最大值
index[s.charAt(right)] = right; // 更新下标
}
return maxLen;
}
(2)找到字符串中所有字母异位词(中)
题干分析
这个题目说的是,给你字符串 s 和 p,你要在 s 中找到所有 p 的变位词,并返回它们的开始下标。变位词指的是使用相同字母以不同顺序构成的单词。在这个题目中,字符串 s 和 p 都只由小写字母组成,并且长度不会超过 100。
# 比如说,给你的字符串 s 和 p 是:
s: bcbababac
p: abc
# 在字符串 s 中,abc 的变位词有 cba 和 bac,它们的组成字符都是 a/b/c,只是排列顺序不一样。
# 我们要返回这两个变位词的开始下标。第一个变位词的开始下标是 1,第二个变位词的开始下标是 6,因此返回:
[1,6]
思路解法
首先想到滑动窗口(固定大小) :即滑动窗口大小已固定
- 初始化滑动窗口: 初始化滑动窗口后,需判断此窗口是否满足题意
- 移动滑动窗口: 以固定窗口大小,不断移动
// Time: O(n * n), Space: O(n), Faster: 86.62%
public List<Integer> findAnagrams(String s, String p) {
if (s == null || p == null || s.length() < p.length()) return Collections.emptyList();
int sLen = s.length(), pLen = p.length();
char[] pc = new char[26];
char[] sc = new char[26];
// 1. 初始化滑动窗口
for (int i = 0; i < pLen; ++i) {
pc[p.charAt(i) - 'a']++;
sc[s.charAt(i) - 'a']++;
}
List<Integer> result = new ArrayList<>();
// 1.2. 判断初始化滑动窗口 是否 满足题意
if (Arrays.equals(sc, pc)) result.add(0);
// 2. 滑动窗口不断移动
for (int i = pLen; i < sLen; ++i) {
sc[s.charAt(i) - 'a']++; // 右侧边扩
sc[s.charAt(i - pLen) - 'a']--; // 左侧边进
if (Arrays.equals(sc, pc)) result.add(i - pLen + 1); // 判断是否满足题意
}
return result;
}
这里有个优化方案: 先统计字符数量。
// Time: O(n), Space: O(k), Faster: 94.93%
public List<Integer> findAnagramsOn(String s, String p) {
List<Integer> result = new ArrayList<>();
if (s == null || p == null || s.length() < p.length()) return result;
int sLen = s.length(), pLen = p.length();
char[] pc = new char[26];
for (int i = 0; i < pLen; ++i) {
pc[p.charAt(i) - 'a']++;
}
int left = 0, right = 0;
while (right < sLen) {
if (pc[s.charAt(right) - 'a'] > 0) {
pc[s.charAt(right) - 'a']--;
++right;
} else {
pc[s.charAt(left) - 'a']++;
++left;
}
if (right - left == pLen) result.add(left);
}
return result;
}