目录
水果成篮
算法分析
这道题其实就是在数组中找连续的的最长子数组,且子数组里的不同数字不能超过两个。 如果采用暴力枚举数组中的每一个字串,并借助哈希表来存储子数组中有多少种水果,并且每种水果的数量有多少。时间复杂度会达到O(n),因此,可以在暴力枚举的举出上进行优化,使用滑动窗口来解决。
算法步骤
- 初始化:设置双指针left和right并初始化为0,表示滑动窗口的边界。设置一个空的HashMap来存储水果种类及数量。定义ans并初始化为0,用于记录能够采摘的数目。
- 右边界(right)移动:让right往右移动,并将nums[right]在HashMap中的数量+1
- 判断水果种类:当HashMap.size()>2时,说明此时采摘的水果中有3种,需要去除一种。此时就需要让left往右移动,并将nums[left]在HashMap中的数量-1,当nums[left]对应的数量为0时,说明此时已经将一种水果去除,调用 map.remove(fruits[left]),将水果移除。
- 更新结果:在右指针right遍历数组的时,每次都需要更新一下ans的值,判断ans此时的值是否比(right-left+1)大,若大于则进行更新。
- 遍历结束:当right遍历完数组,此时返回ans的值即可。
时间复杂度:O(n),n是数组长度,每个元素最多只会被遍历两次。在扩大窗口的时候,right会遍历一遍数组。在最坏情况下,left也只会遍历一遍数组。
空间复杂度:O(1),这里只用到了几个固定的变量,虽然使用了HashMap,但最多也只存储了三种。
示例
以[1,2,3,2,2]为例
第一步:初始化
让left=right=0,定义一个map(哈希表),初始化ans=0;
第二步:扩大窗口,判断种类
让right往右移,在map中不超过2种时,同时更新ans值。
第三步:左指针移动
此时map中种类超过2种,需要移动左指针,直到map中某个水果的数量为0。将数量为0的水果从map中移除。
第四步:右指针继续移动,重复二三步,直到右指针right遍历完数组。当遍历完数组,此时ans=4.即在数组中[2,3,2,2]。
第五步:返回结果,将ans=4返回。
算法代码
/**
* 计算最长的水果数组段,其中只包含两种不同的水果。
*
* @param fruits 水果数组,数组中的每个元素代表一种水果。
* @return 返回最长的水果数组段的长度。
*/
public int totalFruit(int[] fruits) {
/* 初始化结果变量为0,用于记录最长的水果数组段的长度 */
int ans = 0;
/* 初始化左指针为0,用于标记当前水果数组段的起始位置 */
int left = 0;
/* 初始化右指针为0,用于标记当前水果数组段的结束位置 */
int right = 0;
/* 使用HashMap来记录每种水果的数量,键为水果的类型,值为该类型水果的数量 */
HashMap<Integer, Integer> map = new HashMap<>();
/* 遍历水果数组,更新HashMap和计算最长数组段的长度 */
for (; right < fruits.length; right++) {
/* 将右指针指向的水果添加到HashMap中,如果该水果已存在,则数量加1 */
map.put(fruits[right], map.getOrDefault(fruits[right], 0) + 1);
/* 当HashMap中的水果种类超过2种时,需要缩小数组段直到满足条件 */
while (map.size() > 2) {
/* 从HashMap中减去左指针指向的水果的数量,如果数量减到0,则从HashMap中移除该水果 */
map.put(fruits[left], map.getOrDefault(fruits[left], 0) - 1);
if (map.get(fruits[left]) == 0) {
map.remove(fruits[left]);
}
/* 左指针向右移动,缩小数组段 */
left++;
}
/* 更新最长数组段的长度 */
ans = Math.max(ans, right - left + 1);
}
/* 返回最长数组段的长度 */
return ans;
}
找到字符串中所有字母异位词
算法分析
这道题题意说明了要在字符串s中找到所有p的异位词。所以这道题可以使用暴力枚举+哈希表,但是时间复杂度达到O(n^2),我们可以使用滑动窗口+哈希表来使用。
算法步骤
- 初始化:定义双指针left和right并初始化为0,将两个字符串s和p转化为字符数组ss和pp,且这里需要用到两个hash表,这里设置为hash1和hash2,hash1用来存放字符串p的字符个数,hash2用来后续的遍历操作。【这里由于限制了是小写字母,所以hash数组的大小可以设置为26】定义变量m记录p的长度。定义ans顺序表,用来存放在符合条件的子串的起始位置。
- 扩展右边界:右指针right往右移动,用变量in记录字符nums[right],将in字符存进hash2中,但需要注意在存放时需要让in-‘a’,即hash2[in-'a']。
- 处理窗口:由于这里是限定了窗口的大小,即字符串p的长度。所以当right-left+1大于m时,说明窗口过大,需要进行缩小窗口,让left往右移动一位,同时需要将out【这里定义变量out字符为hash2[left]】在hash2中➖1。当right-left+1==m,说明此时窗口大小刚好等于p的长度,调用equal方法来判断hash1和hash2是否相等,若相等,则将left添加到ans中。
时间复杂度:O(n),n为s的长度,每个字符最多被访问2次。
空间复杂度:O(1),虽然使用hash1和hash2数组,但都是固定大小的,且使用的变量固定。
示例
算法代码
/**
* 查找字符串s中所有p的字母异位词的起始索引。
*
* @param s 输入的字符串
* @param p 指定的模式字符串
* @return 返回一个包含所有字母异位词起始索引的列表
*/
public List<Integer> findAnagrams1(String s, String p) {
List<Integer> ans = new ArrayList<>(); // 用于存储结果的列表
int left = 0; // 窗口的左边界
int right = 0; // 窗口的右边界
char[] ss = s.toCharArray(); // 将字符串s转换为字符数组
char[] pp = p.toCharArray(); // 将字符串p转换为字符数组
int[] hash1 = new int[26]; // 用于存储模式字符串p的字符计数
for (char ch : pp) hash1[ch - 'a']++; // 统计模式字符串p中每个字符的数量
int[] hash2 = new int[26]; // 用于存储当前窗口中字符的计数
int m = pp.length; // 模式字符串p的长度
while (right < s.length()) { // 窗口右边界不超过字符串s的长度
char in = ss[right]; // 当前纳入窗口的字符
hash2[in - 'a']++; // 窗口内字符计数加一
if (right - left + 1 > m) { // 当窗口大小超过模式字符串p的长度时
char out = ss[left++]; // 移除窗口最左边的字符
hash2[out - 'a']--; // 窗口内字符计数减一
}
if (right - left + 1 == m) { // 当窗口大小等于模式字符串p的长度时
if (Arrays.equals(hash1, hash2)) { // 比较窗口内字符计数和模式字符串的字符计数
ans.add(left); // 如果相同,则将窗口左边界添加到结果列表中
}
}
right++; // 窗口右边界向右移动
}
return ans; // 返回结果列表
}
优化
当然,在上述中,若面对比较复杂的问题,不是最优解法,可以对其进行优化。
主要是第二、三步进行处理
在上述算法中,我们是将ss[right]的值直接放在hash2中,但我们可以进行优化:
- 定义一个count,用来判断判断当前窗口是否符合条件,当count=m时,说明找到了符合条件的子串,将left添加到ans中即可;count++的前提条件是hash2中的计数小于等于hash1中的计数,才能让count++。
- 当right-left+1>m,即窗口过大时,要缩小窗口,但同时需要判断此时以ss[left]为下标的hash2中的计数是否小于等于hash1中对应位置的计数,若小于,则需要让count--。
算法代码
/**
* 查找字符串s中所有p的字母异位词的起始索引。
*
* @param s 输入的字符串
* @param p 指定的字母异位词模式
* @return 返回一个包含所有字母异位词起始索引的列表
*/
public List<Integer> findAnagrams(String s, String p) {
List<Integer> ans = new ArrayList<>();
char[] ss = s.toCharArray();
char[] pp = p.toCharArray();
// 初始化hash1数组,用于存储模式p中每个字符出现的次数
int[] hash1 = new int[26];
for (char ch : pp) {
hash1[ch - 'a']++;
}
// count用于记录当前窗口中满足p中字符比例的子串数量
int count = 0;
int m = pp.length;
// hash2数组用于存储当前窗口中每个字符出现的次数
int[] hash2 = new int[26];
int left = 0;
int right = 0;
// 滑动窗口遍历字符串s
for (; right < s.length(); right++) {
char in = ss[right];
// 当前字符加入窗口,如果其出现次数不超过p中对应字符的次数,则count增加
// 判断是否满足条件,进窗口
if (++hash2[in - 'a'] <= hash1[in - 'a']) {
count++;
}
// 如果窗口大小超过了p的长度,则需要移除最左边的字符
// 判断长度,
if (right - left + 1 > m) {
char out = ss[left++];
// 移除字符,如果移除后窗口中该字符出现次数不超过p中对应字符的次数,则count减少
if (hash2[out - 'a']-- <= hash1[out - 'a']) {
count--;
}
}
// 如果当前窗口中满足p中字符比例的子串数量等于p的长度,则当前窗口起始索引加入结果列表
if (count == m) {
ans.add(left);
}
}
return ans;
}
串联所有单词的子串
算法分析
这道题与前一道题优化算法的思路基本一样,要求在字符串s中找到words字符串数组中所有字符串任意顺序串联起来的子串。
算法步骤
- 初始化:设置双指针left和right并初始化为0;设置两个HahsMap<String,Integer>,map1用来存放words字符串数组中的字符串及其个数,map2用来存放s字符串中符合条件的字符串;设置ans顺序表,用来存放符合条件的子串起始位置。设置count并初始化为0,用来记录符合条件的子串个数;定义len并初始化为words[0].length()[即字符串数组中字符串的长度],定义m并初始化为words数组的长度。
- 预处理:将words字符串数组中的字符串存放到map1中。
- 滑动窗口遍历字符串:
- 由于在s字符串中,我们不知道words数组中字符串在s中是在起始位置上开始的,所以根据len,可以有len种起始位置。示例:
- 每个单词的长度为len,所以设置双指针在移动时,步长为len。
- 遍历每一个有可能的起始位置i,并每次定义map2用来存放不同起始位置下的单词。
- 让left=right=i,count=0,移动窗口,并将分割出来的单词in放进map2中,判断in在map2中的数量是否小于等于map1的数量,若小于,则让count++;
- 判断当前窗口的长度(right-left+1)是否大于字符串数组中所有字符串的总长度(len*m),若大于,说明窗口过大,需要将【left,left+len】位置的字符出窗口。需要出窗口的字符串out,需要判断是否在map2中的数量小于等于map1的数量,若小于,则让count--;当出完窗口后,需要让out在map2中对应的数量-1,再让left+=len;
- 当count等于m【words数组的长度】,说明此时已经找到了符合条件的串联子串,将left添加到ans中。
4.返回结果:当遍历完所有可能的情况,此时返回结果ans即可。
示例
以s = "barfoothefoobarman", words = ["foo","bar"]为例
第一步:初始化,并预处理
在初始化完所需的变量之后,将words数组中的字符串存放到map1中,得到
map1={["foo",1],["bar",1]}
第二步:滑动窗口遍历s(此处只列举第一种起始位置情况)
- 此时left=right=i=0,count=0(此处len为3)
- 截断s中[right,right+len)位置的字符串作为in,in=“bar”,将in存放到map2中,map2={["bar",1]},并且判断一下in在map2中的数量是否小于等于在map1中的数量,若小于等于,则让count++;
- 判断窗口的大小(right-left+1=3-0+1=4<len*m=4*2=8,不需要进行出窗口操作。
- 由于count此时依旧为0,小于m=2,不满足条件,left不用添加到ans中。
- 重复上述操作。
- 第二次:map2={["bar",1],["foo",1]} count=2 添加left到ans中 ans=[0]
- 第三次:map2={["bar",1],["foo",1],["the",1]} count=2
- 出窗口:map2={["foo",1],["the",1]} count=1
- 第四次: map2={["foo",2],["the",1]} count=1
- 出窗口: map2={["foo",1],["the",1]} count=1
- 第五次: map2={["foo",1],["the",1],["bar",1]} count=2
- 出窗口:map2={["foo",1],["bar",1]} count=2 添加left到ans中,此时left=9 ans=[0,9]
- 第六次: map2={["foo",1],["bar",1],["man",1]} count=2
- 出窗口:map1={["bar",1],["man",1]} count=1
- 此时right已经走到s的末尾,遍历结束,返回结果ans=[0,9]。
算法代码
/**
* 查找所有包含给定单词数组中所有单词的子字符串的起始索引。
*
* @param s 输入的字符串。
* @param words 给定的单词数组。
* @return 包含所有单词的子字符串的起始索引列表。
*/
public List<Integer> findSubstring(String s, String[] words) {
// 用于存储结果的列表
List<Integer> ans = new ArrayList<>();
// 统计每个单词出现的次数
// 借助hash表
HashMap<String,Integer> map1=new HashMap<>();
//统计words中有多少单词
for(String word:words) map1.put(word,map1.getOrDefault(word,0)+1);
//用于后序操作
// 单词的长度
int len=words[0].length();// 单词长度
// 单词的数量
int m=words.length;
// 当前窗口中包含的所有单词的数量
int count=0;
// 窗口的左右边界
int left=0;
int right=0;
// 遍历字符串s,寻找符合条件的子字符串
for(int i=0;i<len;i++) {
// 当前窗口中单词的统计信息
HashMap<String, Integer> map2 = new HashMap<>();
// 移动窗口,查找符合条件的子字符串
for (left = i, right = i,count=0; right+ len <= s.length() ; right += len) {
// 窗口中的当前单词
//进窗口维护
String in = s.substring(right, right + len);
// 更新当前单词在窗口中的统计信息
map2.put(in, map2.getOrDefault(in, 0) + 1);
// 如果当前单词在窗口中的数量不超过其应有数量,则增加count
if (map2.get(in) <= map1.getOrDefault(in, 0)) count++;
// 如果窗口大小超过了最大允许值,则需要移除左边界上的单词
//出窗口
if (right - left + 1 > len*m) {
// 移除的单词
String out = s.substring(left, left + len);
// 如果移除的单词在窗口中的数量不超过其应有数量,则减少count
if (map2.get(out) <= map1.getOrDefault(out, 0)) count--;
// 更新窗口中移除单词的统计信息
map2.put(out, map2.get(out) - 1);
// 移动左边界
left += len;
}
// 如果当前窗口中包含了所有单词,则将左边界索引添加到结果列表中
if (count == words.length) ans.add(left);
}
}
// 返回结果列表
return ans;
}
最小覆盖子串
算法分析
本题要求在s中找到t的最小覆盖子串,我们可以使用暴力枚举+hash表来实现,但时间复杂度会达到O(n^2),我们可以在此进行优化,使用滑动窗口。
算法步骤
-
初始化:设置双指针left和right,并初始化为0作为滑动窗口的边界;将s和t字符串转化为字符数组ss和tt方便操作;设置两个hash表(hash1和hash2),长度为128,hash1用来统计t中的字符;设置begin并初始化为-1,mixLen初始化为Integer.MAX_VALUE,作为最小覆盖子串的起始位置以及末尾位置;定义count(计数器)并初始化为0,用来匹配符合条件的字符。
-
预处理:设置kind并初始化为0,用来计算t中的字符种类有多少种。将tt字符数组中的字符通过hash1进行计数,当hash[ch]为0时,kind++;
-
处理窗口:右指针移动,并将in【in=ss[right]】添加到hash2中,同时判断in在hash2中的数量是否与hash1的相同,若相同,则让count++。当计数器count等于kind时,说明已经找到了符合条件的子串,此时,若minLen==-1或者right-left+1<minLen时,更新begin=left和minLen=right-left+1;当添加完后,缩小窗口,让left往右移,若hash2[left]==hash1[left],需要让count--,同时让left++;寻找更小的覆盖子串。
-
重复上述操作,直到遍历完s。
-
返回结果:若minLen==-1,说明没有找到符合条件的子串,如不为0,则返回[begin,begin+minLen)的截断字符串。
时间复杂度:O(n),n为ss的长度,每个字符最多被遍历两次。
空间复杂度:O(1),虽然使用了hash,但长度是固定的。
示例
以s = "ADOBECODEBANC", t = "ABC"为例
第一步:初始化
- left=right=0
- 初始化hash1和hash2并设置长度为128.
- 将s和t转化为字符数组ss和tt
- kind=0,用于记录不同字符的种类,遍历数组tt并计数到hash1中,此处kind=3,hash['A']=1,hash['B']=1,hash['C']=1.
第二步:处理窗口
- 使用双指针left和right,扩大窗口,并将字符in计数到hash2中,若in在hash2的数量等于hash1的数量,则让count++;
- 当count=kind时,说明此事已经找到了匹配的覆盖子串,判断right-left+1<minLen或者minLen=-1时,则进行替换,让begin=left,minLen=right-left+1;当判断完之后,移除左窗口out,同时判断out在hash2和hash1的计数是否相等,若相等,则让count--。
- 重复上述操作,当right遍历完s字符串时,结束循环。
- 返回结果,此时,由于begin不为-1,根据begin和minLen,截取s中此位置的字符串,为“BANC”。
算法代码
/**
* 寻找字符串s中包含字符串t所有字符的最短子串。
* @param s 原始字符串
* @param t 目标字符串,需要在原始字符串中找到包含所有这些字符的最短子串
* @return 返回最短子串的字符串,如果不存在则返回空字符串
*/
public String minWindow(String s, String t) {
// 初始化两个字符数组,用于统计字符串t和字符串s中字符出现的次数
int[] hash1=new int[128];
int[] hash2=new int[128];
// 将字符串t转换为字符数组,方便后续处理
char[] tt=t.toCharArray();
// 计算字符串t中不同字符的数量
int kind=0;
for(char ch:tt){
if(hash1[ch]++==0) kind++;
}
// 将字符串s转换为字符数组,方便后续处理
char[] ss=s.toCharArray();
// 初始化指针begin和minlen,begin用于标记最短子串的起始位置,minlen用于记录最短子串的长度
int begin=-1;
int minlen=Integer.MAX_VALUE;
// 初始化滑动窗口的左指针left、右指针right和当前窗口中包含t中字符的数量count
int left,right,count;
for(left=0,right=0,count=0;right<ss.length;right++){
char in=ss[right];
// 当前字符在窗口中出现的次数等于在t中出现的次数时,count加1
if(++hash2[in]==hash1[in]) count++;
// 当窗口中包含了t中所有字符时
while(count==kind){
// 更新最短子串的起始位置和长度
if(right-left+1<minlen||minlen==-1){
begin=left;
minlen=right-left+1;
}
// 移动窗口的左指针,并更新count和hash2数组
char out=ss[left++];
if(hash2[out]--==hash1[out]) count--;
}
}
// 根据begin和minlen的值,返回最短子串或空字符串
if(begin==-1){
return new String();
}else{
return s.substring(begin,begin+minlen);
}
}
以上就是本篇所有内容,滑动窗口的题目就先到这了,后序若有相关题目,将会更新!
若有不足,欢迎指正~