找往期文章包括但不限于本期文章中不懂的知识点:
个人主页:我要学编程(ಥ_ಥ)-CSDN博客
所属专栏: 优选算法专题
想要了解滑动窗口算法的介绍,可以去看下面的博客:滑动窗口算法的介绍
目录
904. 水果成篮
438. 找到字符串中所有字母异位词
30. 串联所有单词的子串
76. 最小覆盖子串
904. 水果成篮
题目:
你正在探访一家农场,农场从左到右种植了一排果树。这些树用一个整数数组
fruits
表示,其中fruits[i]
是第i
棵树上的水果 种类 。你想要尽可能多地收集水果。然而,农场的主人设定了一些严格的规矩,你必须按照要求采摘水果:
- 你只有 两个 篮子,并且每个篮子只能装 单一类型 的水果。每个篮子能够装的水果总量没有限制。
- 你可以选择任意一棵树开始采摘,你必须从 每棵 树(包括开始采摘的树)上 恰好摘一个水果 。采摘的水果应当符合篮子中的水果类型。每采摘一次,你将会向右移动到下一棵树,并继续采摘。
- 一旦你走到某棵树前,但水果不符合篮子的水果类型,那么就必须停止采摘。
给你一个整数数组
fruits
,返回你可以收集的水果的 最大 数目。示例 1:
输入:fruits = [1,2,1] 输出:3 解释:可以采摘全部 3 棵树。示例 2:
输入:fruits = [0,1,2,2] 输出:3 解释:可以采摘 [1,2,2] 这三棵树。 如果从第一棵树开始采摘,则只能采摘 [0,1] 这两棵树。示例 3:
输入:fruits = [1,2,3,2,2] 输出:4 解释:可以采摘 [2,3,2,2] 这四棵树。 如果从第一棵树开始采摘,则只能采摘 [1,2] 这两棵树。示例 4:
输入:fruits = [3,3,3,1,2,1,1,2,3,3,4] 输出:5 解释:可以采摘 [1,2,1,1,2] 这五棵树。提示:
1 <= fruits.length <= 105
0 <= fruits[i] < fruits.length
思路:首先得读懂这个题目。有一个数组,这个数组中的值是水果种类的编号,我们要做的就是确保水果种类(编号类别)是两种的情况下,将连续的编号个数全部记录下来,返回一个最长的连续编号。从这里我们就可以看出来这个题目就是用滑动窗口算法来解决问题。找一个长度最长子数组。
代码实现:
class Solution {
public int totalFruit(int[] fruits) {
int left = 0;
int right = 0;
int count = 0; // 记录水果种类
// 哈希表是水果篮,得装得下其编号
int[] hash = new int[fruits.length];
int len = 0;
while (right < fruits.length) {
// 判断这个水果是不是可以用篮子装的新水果
if (count == 2 && hash[fruits[right]] == 0) {
// 先记录
len = Math.max(len, right-left); // right此时不符合要求
// 得去掉一种水果,然后继续遍历
while (count == 2) {
hash[fruits[left]]--; // 丢水果
// 篮中水果没了,那么就有一个篮子是空着的
if (hash[fruits[left]] == 0) {
count--;
}
left++;
}
} else {
if (hash[fruits[right]] == 0) { //新水果
count++;
}
// 不管是不是新水果,其个数都得++
hash[fruits[right]]++;
right++;
}
}
// 避免可能只有一种或两种水果的情况
len = Math.max(len, right-left); // right此时也是非法位置
return len;
}
}
注意:这里的提示告诉我们:水果的编号范围是小于 fruits 数组的长度的,因此我们直接把哈希表的长度设置为这个数组的长度即可。
滑动窗口算法有固定的步骤,按套路走,基本上不是很难,主要是难在怎么联想到使用这个算法。
438. 找到字符串中所有字母异位词
题目:
给定两个字符串
s
和p
,找到s
中所有p
的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。异位词:字母异位词是通过重新排列不同单词或短语的字母而形成的单词或短语,并使用所有原字母一次。
示例 1:
输入: s = "cbaebabacd", p = "abc" 输出: [0,6] 解释: 起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。 起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。示例 2:
输入: s = "abab", p = "ab" 输出: [0,1,2] 解释: 起始索引等于 0 的子串是 "ab", 它是 "ab" 的异位词。 起始索引等于 1 的子串是 "ba", 它是 "ab" 的异位词。 起始索引等于 2 的子串是 "ab", 它是 "ab" 的异位词。提示:
1 <= s.length, p.length <= 3 * 104
s
和p
仅包含小写字母
思路:经过四五到题目的磨炼,现在我们应该培养出了一种思维:在一个序列中寻找子序列的常见算法 ——> 滑动窗口。 这里是让我们在字符串s中寻找一段序列和字符串p是字母异位词。
首先得知道怎么找字母异位词?根据定义,我们可以推测出就是一些字符通过全排列的方式得出的一段序列。因此如果两个序列是字母异位词的话,那么其中两者的同一字母出现的次数一定是相同的。因此这里我们就可以联想到哈希表。
一般解法(暴力枚举):
直接在字符串s中遍历长度和字符串p一样的序列,判断两者是否为字母异位词即可。
代码实现:
class Solution {
public List<Integer> findAnagrams(String s, String p) {
// 现将字符串p中的字符插入哈希表中
int[] hashP = new int[26];
int len = p.length()-1;
for (int i = 0; i <= len; i++) {
char x = p.charAt(i);
hashP[x-'a']++;
}
// 遍历字符串s判断其中的元素是否满足异位词
int[] hashS = new int[26];
int left = 0;
int right = len;
List<Integer> list = new ArrayList<>();
int count = 0;
while (right < s.length()) {
// 将[left,right]之间字符全部插入到哈希表中
if (count == 0) { // 只需做一次
for (int i = left; i <= right; i++) {
char x = s.charAt(i);
hashS[x-'a']++;
}
count++;
}
// 再遍历[left,right]之间的字符,再两个哈希表中对比查找
boolean flag = false;
for (int i = left; i <= right; i++) {
char x = s.charAt(i);
int a = hashS[x-'a'];
int b = hashP[x-'a'];
if (a != b) {
flag = true;
break;
}
}
// 判断结果
if (!flag) {
list.add(left);
}
// 将left位置的字符从哈希表中删除
char x = s.charAt(left++);
hashS[x-'a']--; // 这里不能置为0
// 将right下一个位置的字符插入哈希表
if (right == s.length()-1) {
break;
}
x = s.charAt(++right);
hashS[x-'a']++;
}
return list;
}
}
上面的代码虽然可以通过,但是还有不足的地方:每一次判断都需要遍历p字符串的长度,这里也就导致时间复杂度过高。因此我们得想办法将这里进行优化。即有没有另外一种更简洁的方法来判断异位字母词呢?
下面这种方法就属于大佬才能想到的了。
用一个计数器来记录 ‘有效字符’ 的个数。如果计数器的长度和s的长度相等(保持[left, right]之间的长度和字符串s的长度相等),那么就说明此时是字母异位词。
1、先判断right位置的字符在哈希表出现的次数 是否满足少于 这个字符在另一个哈希表中出现的次数。如果小于等于的话,就是有效字符,计数器++;反之,则不是有效字符,不做任何处理。
2、接着再去判断窗口大小是否符合要求(必须是p字符串的长度)。如果符合要求,继续往后遍历,不符合要求就缩小窗口。
3、最后再判断count是否等于p字符串的长度。如果等于的话,符合条件,添加left;反之,则不做任何处理(继续遍历肯定大小会不满足条件)。
代码实现:
class Solution {
public List<Integer> findAnagrams(String s, String p) {
// 将p字符串的元素插入哈希表
int[] hashP = new int[26];
int len = p.length();
for (int i = 0; i < len; i++) {
char x = p.charAt(i);
hashP[x-'a']++;
}
// 遍历s字符串寻找字母异位词
int count = 0;
int left = 0;
int right = 0; // 注意:因为需要统计有效字符的个数,因此得从头开始
List<Integer> list = new ArrayList<>();
int[] hashS = new int[26];
while (right < s.length()) {
// 1、判断是否为有效字符
char x = s.charAt(right);
hashS[x-'a']++;
if (hashS[x-'a'] <= hashP[x-'a']) { // 有效字符
count++;
}
// 2、判断窗口大小是否符合要求
if (right-left+1 > len) {
// 这里窗口大小一定得符合要求,
// 因此不会出现right 超出很多的情况,因此不需要用到while循环
// 先得判断left是否是有效字符
char i = s.charAt(left);
if (hashS[i-'a'] <= hashP[i-'a']) {
count--;
}
hashS[i-'a']--;
left++;
}
// 3、这里窗口大小一定是符合要求的,因此可以判断是否为字母异位词
if (count == len) {
list.add(left);
}
right++;
}
return list;
}
}
上述代码就是通过count计数器来优化遍历两个哈希表的过程,从而提高了时间效率。
注意:
1、哈希表存储的是字符串的元素,而不是 left 与 right 对应的值。因此我们在判断该字符是否是有效字符时,应该是通过 left 与 right 得到的字符去寻找。
2、这里在判断是否为有效字符时,之所以条件是 <= ,是因为我们在判断之前,已经将这个字符出现的次数++了,因此就会出现 == 的情况,而这种情况也是对的,不能忽略。但是在判断 left 对应的字符是否为有效字符时,这个一定是要包含 == 的情况的。
30. 串联所有单词的子串
题目:
给定一个字符串
s
和一个字符串数组words
。words
中所有字符串 长度相同。
s
中的 串联子串 是指一个包含words
中所有字符串以任意顺序排列连接起来的子串。
- 例如,如果
words = ["ab","cd","ef"]
, 那么"abcdef"
,"abefcd"
,"cdabef"
,"cdefab"
,"efabcd"
, 和"efcdab"
都是串联子串。"acdbef"
不是串联子串,因为他不是任何words
排列的连接。返回所有串联子串在
s
中的开始索引。你可以以 任意顺序 返回答案。示例 1:
输入:s = "barfoothefoobarman", words = ["foo","bar"] 输出:[0,9]
解释:因为 words.length == 2 同时 words[i].length == 3,连接的子字符串的长度必须为 6。 子串 "barfoo" 开始位置是 0。它是 words 中以 ["bar","foo"] 顺序排列的连接。 子串 "foobar" 开始位置是 9。它是 words 中以 ["foo","bar"] 顺序排列的连接。 输出顺序无关紧要。返回 [9,0] 也是可以的。示例 2:
输入:s = "wordgoodgoodgoodbestword", words = ["word","good","best","word"]输出:[]
解释:因为 words.length == 4 并且 words[i].length == 4,所以串联子串的长度必须为 16。 s 中没有子串长度为 16 并且等于 words 的任何顺序排列的连接。 所以我们返回一个空数组。示例 3:
输入:s = "barfoofoobarthefoobarman", words = ["bar","foo","the"] 输出:[6,9,12] 解释:因为 words.length == 3 并且 words[i].length == 3,所以串联子串的长度必须为 9。 子串 "foobarthe" 开始位置是 6。它是 words 中以 ["foo","bar","the"] 顺序排列的连接。 子串 "barthefoo" 开始位置是 9。它是 words 中以 ["bar","the","foo"] 顺序排列的连接。 子串 "thefoobar" 开始位置是 12。它是 words 中以 ["the","foo","bar"] 顺序排列的连接。提示:
1 <= s.length <= 104
1 <= words.length <= 5000
1 <= words[i].length <= 30
words[i]
和s
由小写英文字母组成
思路:如果这个题目去硬写的话,简直就是来恶心人的。如果联想到了上面那个找字母异位词的题目的话,就简单很多了。把words数组中的元素看成一个字母,然后在字符串s中再去寻找长度为字母长度 x 数组长度 的子串即可。
代码实现:
class Solution {
public List<Integer> findSubstring(String s, String[] words) {
int len = words[0].length();
HashMap<String, Integer> hashW = new HashMap<>();
for (String str : words) {
hashW.put(str, hashW.getOrDefault(str, 0)+1);
}
List<Integer> list = new ArrayList<>();
for (int i = 0; i < len; i++) {
int left = i;
int right = i;
HashMap<String, Integer> hashS = new HashMap<>();
int count = 0; // 记录有效字符串的个数
while (right+len <= s.length()) {
// 先拿到[right,right+len]区间的字符
String s1 = s.substring(right, right+len); // 此方法为左闭右开
hashS.put(s1, hashS.getOrDefault(s1, 0)+1);
// 有效字符串
// s1一定存在于hashS中
if (hashS.get(s1) <= hashW.getOrDefault(s1, 0)) {
count++;
}
// 检查窗口的大小
if ((right-left+1) > len * words.length) {
// 先检查[left,left+len]是否为有效字符串
String s2 = s.substring(left, left+len);
// s2一定存在于hashS中
if (hashS.get(s2) <= hashW.getOrDefault(s2, 0)) {
count--;
}
// 对应的次数得减少
hashS.put(s2, hashS.getOrDefault(s2, 0)-1);
left += len;
}
if (count == words.length) {
list.add(left);
}
right += len;
}
}
return list;
}
}
注意:
1、在使用Java库中的哈希表统计元素时,一定判断表中是否出现过这个元素。即找到出现的次数,若是没有出现就默认为0;
2、因为这里的字母是随机组合而成,因此在最外层一定还得有一个循环将不重复的组合全部计算进去。当 i = len 时,我们仔细观察会发现是有重复的,只是少了最前一个元素而已;
3、while 循环一定得是 right+len <= s.length()。因为substring 方法是前闭后开的,即使我们的right+len 为 s.length() 时,也是不会影响切割的。并且这样也会包含全部的结果集。实在不理解的话,可以将 = 去掉,然后再去LeetCode上面运行看看,观察其给的示例进行分析 或者 理解下面这个例子也行;
4、 窗口大小的维持。题目是让我们把数组中所有的字符串都在s这个字符串中找出来,那么我们的窗口肯定也只能是最大维持这个数组的中所有字母的长度大小,而不能超过。
5、left 与 right 在增大时,也是要和数组中字符串的大小一样的增大,从而达到不重复、不遗漏的情况。
以上就是本题的全部了。
76. 最小覆盖子串
题目:
给你一个字符串
s
、一个字符串t
。返回s
中涵盖t
所有字符的最小子串。如果s
中不存在涵盖t
所有字符的子串,则返回空字符串""
。注意:
- 对于
t
中重复字符,我们寻找的子字符串中该字符数量必须不少于t
中该字符数量。- 如果
s
中存在这样的子串,我们保证它是唯一的答案。示例 1:
输入:s = "ADOBECODEBANC", t = "ABC" 输出:"BANC" 解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。示例 2:
输入:s = "a", t = "a" 输出:"a" 解释:整个字符串 s 是最小覆盖子串。示例 3:
输入: s = "a", t = "aa" 输出: "" 解释: t 中两个字符 'a' 均应包含在 s 的子串中, 因此没有符合条件的子字符串,返回空字符串。提示:
m == s.length
n == t.length
1 <= m, n <= 105
s
和t
由英文字母组成
思路:题目是让我们在字符串s 中找到包含字符串t 的最短子序列。因此还是首先联想到滑动窗口算法。有前面几题的基础,这个题目应该是比较简单的。
遍历字符串s,遇到目标字符就让计数器++,当计数器的长度与字符串t 的长度相等时,就可以开始更新结果了。在更新结果时,也要注意维护计数器。
代码实现:
class Solution {
public String minWindow(String ss, String tt) { // 参数名是可以修改的
// 先统计tt字符串中字符出现的次数
char[] t = tt.toCharArray(); // 可以更好的去拿到字符
char[] s = ss.toCharArray();
int[] hashT = new int[58];
for (char x : t) {
hashT[x-'A']++;
}
// 遍历ss字符串找到符合要求的字符串
int left = 0;
int right = 0;
int[] hashS = new int[58];
int count = 0;
int lenT = t.length;
int lenS = s.length;
String ans = ""; // 这里不能初始化为null,注意区分两者
while (right < lenS) {
char x = s[right];
hashS[x-'A']++;
// 判断是否为有效字符
if (hashS[x-'A'] <= hashT[x-'A']) {
count++;
}
// 更新结果
while (count == lenT) {
if (ans.equals("")) {
ans = ss.substring(left, right+1);
} else {
ans = ans.length() > right-left ? ss.substring(left, right+1) : ans;
}
// 先得判断left对应位置的字符是不是有效的字符
char i = s[left++];
if (hashS[i-'A'] <= hashT[i-'A']) { // 如果是有效字符的话,count得减少
count--;
}
hashS[i-'A']--; // 对应出现的字符次数也得减少
}
right++;
}
return ans;
}
}
上面的方法是记录有效字符的个数,下面的方法是记录有效字符的种类。
class Solution {
public String minWindow(String ss, String tt) { // 参数名是可以修改的
// 先统计tt字符串中字符出现的次数
char[] t = tt.toCharArray(); // 可以更好的去拿到字符
char[] s = ss.toCharArray();
int[] hashT = new int[58];
int kinds = 0; // 记录字符串tt中字符的种类
for (char x : t) {
if (hashT[x-'A'] == 0) {
kinds++;
}
hashT[x-'A']++;
}
// 遍历ss字符串找到符合要求的字符串
int left = 0;
int right = 0;
int[] hashS = new int[58];
int count = 0; // 记录有效字符的种类
int lenS = s.length;
String ans = ""; // 这里不能初始化为null,注意区分两者
while (right < lenS) {
char x = s[right];
hashS[x-'A']++;
// 判断是否为有效字符
if (hashS[x-'A'] == hashT[x-'A']) { // 确保只记录一次
count++;
}
// 更新结果
while (count == kinds) {
if (ans.equals("")) {
ans = ss.substring(left, right+1);
} else {
ans = ans.length() > right-left ? ss.substring(left, right+1) : ans;
}
// 先得判断left对应位置的字符是不是有效的字符
char i = s[left++];
if (hashS[i-'A'] == hashT[i-'A']) { // 如果是有效字符的话,count得减少
count--;
}
hashS[i-'A']--; // 对应出现的字符次数也得减少
}
right++;
}
return ans;
}
}
注意:题目这里给了我们数据范围:全是英文字母,我们可以参考ASCII码表,去找到算出范围即可。当然也可以直接申请128大小的数组。
好啦!本期 滑动窗口算法专题(2)的学习之旅就到此结束啦!我们下一期再一起学习吧!