文章目录
- 基础理论介绍
- 长度最小的子数组
- 无重复字符的最长字串
- 解法1 : 哈希表计数逐步缩进
- 解法2 : 哈希表更新下标跳跃缩进
- 最小覆盖字串
- 替换子串获得平衡字符串
- K个不同整数的子数组
基础理论介绍
1. 滑动窗口简介 : 滑动窗口其实就是维持了一段区间(l边界与r边界), 并且对于这个窗口的两个边界来说, 左右指针都不会进行回退, 来求解一些关于字符组跟字串相关的问题(所以说我们的滑动窗口的大流程的时间复杂度一般都是O(n), 明显优于遍历每一个子区间的时间复杂度O(n^2)
2. 滑动窗口的核心 : 找到窗口的范围跟所求指标之间的单调性关系(这里的范围我们指的是对其中的单一窗口来说, 扩大或者缩小范围的影响)
3. 滑动窗口的过程 : 用简单变量或者简单结构来维持信息(通常是左右两个指针, 有时候也可以根据实际情况对指针的数量进行调整)
4. 求解大流程 : 求解子数组在每个位置开头或者结尾时的答案
5. 关于单调性分析经验 : 对于一个窗口来说, 我们考虑这个窗口向右侧左侧扩大是不是满足条件(上下界), 分析窗口是否回退的经验就是, 对当前窗口添加下一个元素, 看当前的窗口是不是也可以满足(不回退的分析)
6. 关于大流程经验 : 关于求解大流程, 如果是以结尾, 那么我们首先加入右侧的元素然后尝试缩进左侧元素(这里我们的边界是左闭右闭), 如果是以开头, 我们尝试向右侧扩充然后再删除左侧元素(这里我们的边界是左闭右开)
(补充)关于滑动窗口维持最大值/最小值的更新结构在单调队列章节讲述
长度最小的子数组
leetcode209题目链接
问题解析 :
- 首先看到是一个子数组字串的问题, 很自然的会联想到滑动窗口的相关技巧,下面就是找出来其中蕴含的范围与指标间的单调性关系
- 单调性分析 : 对于一个范围来说, 范围越大, 就表示这个区间内的数组总和越大, 就越逼近指标target, 所以有一个下限, 最短的数组长度
- 滑动窗口的过程 : 这个采用的就是经典的左右两个指针的方案
- 求解大流程 : 本题我们以结尾跟开头都进行尝试一下
以某个位置为结尾的答案
class Solution {
//本次的解法采用的是以每一个位置为结尾的方案(左闭右闭的方案)
public int minSubArrayLen(int target, int[] nums) {
//返回的答案结果
int resLen = Integer.MAX_VALUE;
//求出来的和sum
int sum = 0;
for(int l = 0, r = 0; r < nums.length; r++){
//首先添加右侧的元素
sum += nums[r];
//判断左边界是否可以向右侧缩进
while(sum - nums[l] >= target){
sum -= nums[l++];
}
//判断此时是否是答案
if(sum >= target){
resLen = Math.min(resLen, r - l + 1);
}
}
return resLen == Integer.MAX_VALUE ? 0 : resLen;
}
}
以某个位置为开头的答案
注意这种情况的分析, 一般需要注意边界的判断(因为我们这里采用的都是左闭右开的结构, 不同于结尾的大流程, 因为如果是左闭右闭的话,
很容易造成数据的重复)
class Solution {
//本次的求解流程是以每一个位置开头
public int minSubArrayLen(int target, int[] nums) {
//返回的结果
int resLen = Integer.MAX_VALUE;
//子数组的求和sum
int sum = 0;
//下面就是求解的大流程(左闭右开)
for(int l = 0, r = 0; l < nums.length; l++){
//尝试向右侧扩展
while(r < nums.length && sum < target){
sum += nums[r++];
}
//判断此时是不是答案
if(sum >= target){
resLen = Math.min(resLen, r - l);
}
//删除左侧的元素
sum -= nums[l];
}
return resLen == Integer.MAX_VALUE ? 0 : resLen;
}
}
无重复字符的最长字串
leetcode3题目链接
问题解析
- 首先看到这是一个求解子串的相关问题, 所以我们自然的想到了滑窗
- 单调性分析 : 对于一个范围来说, 范围越大就越容易产生重复的字符, 所以存在一个上限值, 也就是最长的子串长度
- 滑动窗口的过程 : 这个题没什么好说的, 还是维持两个指针进行滑动
- 求解大流程 : 从开头或者结尾开始均可
解法1 : 哈希表计数逐步缩进
也就是我们创建一个哈希表, 用来对每一个字符进行计数, 说是计数, 其实就是判断里面是不是有这一种字符, 所以map中的值其实只有1和2两种情况, 如果之前就没有出现过这一种字符, 那么直接加入map然后不用缩进继续进行循环, 如果之前有这种字符, 那么我们就尝试缩小区间的范围直到这种字符只剩下一个, 下一轮的循环不用回退(自己尝试画图理解单调性的本质)
以某个位置为结尾的答案
class Solution {
//首先还是滑动窗口的大的流程(我们用一个map做一下词频统计)
public int lengthOfLongestSubstring(String s) {
//特殊字符串直接返回
if(s == null || s.length() == 0) return 0;
//返回的最大长度
int resLen = 1;
//把字符串变为字符数组方便操作
char[] chs = s.toCharArray();
//定义一个哈希表用来词频统计
HashMap<Character, Integer> map = new HashMap<>();
//下面是求解的大流程(以每一个字符为结尾)
for(int l = 0, r = 0; r < chs.length; r++){
//查看是否有右侧字符(如果没有直接加入继续, 如果有加入之后进行左边界的缩进)
boolean isContains = map.containsKey(chs[r]);
map.put(chs[r], map.getOrDefault(chs[r], 0) + 1);
//进行左侧边界的缩进
while(isContains && l < r){
if(chs[l++] == chs[r]){
map.put(chs[r], 1);
break;
}else{
map.remove(chs[l - 1]);
}
}
//进行长度的更新
resLen = Math.max(resLen, r - l + 1);
}
return resLen;
}
}
以某个位置为开头的答案
class Solution {
//首先还是滑动窗口的大的流程(我们用一个map做一下词频统计)
public int lengthOfLongestSubstring(String s) {
//特殊的字符串直接返回
if(s == null || s.length() == 0) return 0;
//返回的最大长度
int resLen = 1;
//把字符串变为字符数组方便操作
char[] chs = s.toCharArray();
//定义一个哈希表用来进行词频统计
HashMap<Character, Integer> map = new HashMap<>();
//下面就是求解的大流程(以每一个字符为开头)
for(int l = 0, r = 0; l < chs.length; l++){
//首先进行右侧边界的扩充
while(r < chs.length && !map.containsKey(chs[r])){
map.put(chs[r], map.getOrDefault(chs[r++], 0) + 1);
}
//进行边界的更新(左闭右开)
resLen = Math.max(resLen, r - l);
//删除左侧的值
map.remove(chs[l]);
}
return resLen;
}
}
解法2 : 哈希表更新下标跳跃缩进
上面的解法1虽说也是滑窗的思路, 但是不够好, 因为我们的两个边界都是逐步缩进的, 也就是说, 时间复杂度是两个O(n), 那有没有什么优化的方法呢, 我们把map改为元素以及对应的下标位置, 那么我们更新左边界的时候, 就可以通过获取元素上一个下标的位置的下一个下标以及原左边界进行比较大小, 大的那一个作为新的左边界, 这样就实现了左边界的跳跃更新, 虽然指标还是O(n), 但是可以在一定程度上加速原过程
以某个位置为结尾的答案
class Solution {
//还是滑窗的思路, 但是我们的map存储的字符对应的下标位置
public int lengthOfLongestSubstring(String s) {
//特殊的字符串直接返回
if(s == null || s.length() == 0) return 0;
//返回的最大长度
int resLen = 1;
//把字符串变为字符数组方便操作
char[] chs = s.toCharArray();
//定义一个哈希表来进行下标映射
HashMap<Character, Integer> map = new HashMap<>();
//下面就是求解的大流程(以每一个字符为结尾)
for(int l = 0, r = 0; r < chs.length; r++){
//首先获取该元素的上一个位置下标
int lastIndex = map.getOrDefault(chs[r], Integer.MIN_VALUE);
//尝试更新左边界
l = Math.max(l, lastIndex + 1);
//尝试更新长度
resLen = Math.max(resLen, r - l + 1);
//加入新的下标位置
map.put(chs[r], r);
}
return resLen;
}
}
最小覆盖字串
leetcode76题目链接
问题解析 :
- 还是老思路, 子数组子串的问题, 我们可以联想到滑窗的相关技巧
- 单调性分析, 对于一个范围, 区间范围越大就越容易进行覆盖, 所以有一个下限也就是最下的子串长度
- 滑动窗口的过程 : 这个题没什么好说的, 还是维持两个指针进行滑动
- 求解大流程 : 以某一个位置为结尾
代码的实现直接看下面的代码(思路都写在题目上)
class Solution {
public String minWindow(String s, String t) {
//特殊情况直接返回
if(s.length() < t.length()) return "";
//转化为字符数组方便操作(也可以不转化)
char[] cs = s.toCharArray();
char[] ts = t.toCharArray();
//创建一个词频统计的数组
int[] cnt = new int[256];
//对字符串的操作就是直接进行词频统计a
for(char elem : ts){
cnt[elem]--;
}
//创建一个欠债信息debt
int debt = ts.length;
//下面进行的滑动窗口的主流程(以某一个字符为结尾)
int start = 0;
int len = Integer.MAX_VALUE;
for(int l = 0, r = 0; r < cs.length; r++){
//添加当前右侧字符(中if就是一次有效的还款)
if(cnt[cs[r]]++ < 0){
debt--;
}
//只有debt为0才考虑进行答案收集
if(debt == 0){
//首先尝试往左侧回收区间
while(cnt[cs[l]] > 0){
cnt[cs[l++]]--;
}
//尝试收集答案
if(len > r - l + 1){
len = r - l + 1;
start = l;
}
}
}
return len == Integer.MAX_VALUE ? "" : s.substring(start, start + len);
}
}
替换子串获得平衡字符串
leetcode1234题目链接
class Solution {
// 替换字串获得平衡的字符串(还是滑动窗口的思路)
// 这个问题的单调性分析 : 范围越大, 则更容易得到平衡的字符串
// 该问题我们从开头进行分析
public int balancedString(String s) {
// 把s转化为一个字符数组进行操作
char[] cs = s.toCharArray();
// 所需要的每种字符的个数
int requ = cs.length / 4;
// 把Q看作0, W看作1, E看作2, R看作3 进行词频统计
int[] cnt = new int[4];
for (char elem : cs) {
cnt[getNum(elem)]++;
}
//返回的长度答案(最长就是字符串的答案)
int resLen = cs.length;
//下面是滑动窗口的主流程
for(int l = 0, r = 0; l < cs.length; l++){
//尝试向右侧进行扩充
while(r < cs.length && !satisfy(cnt, l, r, requ)){
int elem = getNum(cs[r]);
cnt[elem]--;
r++;
}
if(satisfy(cnt, l, r, requ)){
resLen = Math.min(resLen, r - l);
}
cnt[getNum(cs[l])]++;
}
return resLen;
}
//根据词频判断该区间是不是满足要求的区间(cnt是不包含该区间的词频统计)
private boolean satisfy(int[] cnt, int l, int r, int requ){
//只要是其中有一种字符大于所需字符那就一定不行
if(cnt[0] > requ || cnt[1] > requ || cnt[2] > requ || cnt[3] > requ) return false;
if(4 * requ - (cnt[0] + cnt[1] + cnt[2] + cnt[3]) == r - l) return true;
return false;
}
private int getNum(char elem){
if(elem == 'Q') return 0;
if(elem == 'W') return 1;
if(elem == 'E') return 2;
if(elem == 'R') return 3;
return -1;
}
}
K个不同整数的子数组
leetcode992题目链接
class Solution{
public static int longestSubstring(String str, int k) {
char[] s = str.toCharArray();
int n = s.length;
int[] cnts = new int[256];
int ans = 0;
// 每次要求子串必须含有require种字符,每种字符都必须>=k次,这样的最长子串是多长
for (int require = 1; require <= 26; require++) {
Arrays.fill(cnts, 0);
// collect : 窗口中一共收集到的种类数
// satisfy : 窗口中达标的种类数(次数>=k)
for (int l = 0, r = 0, collect = 0, satisfy = 0; r < n; r++) {
cnts[s[r]]++;
if (cnts[s[r]] == 1) {
collect++;
}
if (cnts[s[r]] == k) {
satisfy++;
}
// l....r 种类超了!
// l位置的字符,窗口中吐出来!
while (collect > require) {
if (cnts[s[l]] == 1) {
collect--;
}
if (cnts[s[l]] == k) {
satisfy--;
}
cnts[s[l++]]--;
}
// l.....r : 子串以r位置的字符结尾,且种类数不超的,最大长度!
if (satisfy == require) {
ans = Math.max(ans, r - l + 1);
}
}
}
return ans;
}
}