🔥个人主页: 中草药
🔥专栏:【算法工作坊】算法实战揭秘
🦌一.水果成篮
题目链接:904.水果成篮
算法原理
算法原理是使用“滑动窗口”(Sliding Window)策略,结合哈希表(Map)来高效地统计窗口内不同水果的种类数量。以下是详细分析:
-
初始化:创建一个空的哈希表
map用来存储每种水果的数量,初始化左右指针left和right为 0,同时初始化结果变量ret为 0,用于记录最大的水果段长度。 -
扩展右边界:右指针
right逐渐向右移动,每移动一步,就在哈希表map中增加对应水果的数量。这代表尝试将新的水果加入当前的采摘窗口。 -
处理超过两种水果的情况:使用
while循环检查哈希表map的大小是否大于 2,即窗口内是否含有超过两种水果。如果是,则需要收缩左边界,即从窗口中移除最左边的水果(即减少其在哈希表中的计数,并在计数为 0 时从哈希表中移除该水果),然后将左指针left向右移动一位,缩小窗口范围。 -
更新结果:每次右指针移动后,都检查当前窗口(
right-left+1)的长度是否大于之前记录的最大长度ret,如果是,则更新ret为当前窗口的长度。 -
遍历结束:当右指针遍历完整个数组后,
ret中存储的就是能够收集到的、每个篮子中只含有两种类型水果的最长连续段的长度。
时间复杂度与空间复杂度
- 时间复杂度:O(n),其中 n 是数组
fruits的长度。每个元素最多被遍历一次。 - 空间复杂度:O(1),虽然使用了哈希表,但由于最多只存储两种类型的水果,所以哈希表的大小是常数级别,整体空间复杂度为 O(1)。
代码
public int totalFruit(int[] fruits) {
Map<Integer,Integer> map=new HashMap<>();
int ret=0;
for(int left=0,right=0;right<fruits.length;right++){
map.put(fruits[right],map.getOrDefault(fruits[right],0)+1);
while(map.size()>2){
map.put(fruits[left],map.get(fruits[left])-1);
if (map.get(fruits[left])==0){
map.remove(fruits[left]);
}
left++;
}
ret=Math.max(ret,right-left+1);
}
return ret;
}
举例
测试用例 [1,2,3,2,2]
初始状态
- 初始化
left = 0,right = 0,ret = 0,以及一个空的哈希表map用于记录每种水果的数量。
执行过程
-
右指针移动与计数:
- 当
right = 0,fruits[right]=1,哈希表变为{1:1}。 - 当
right = 1,fruits[right]=2,哈希表变为{1:1, 2:1}。 - 当
right = 2,fruits[right]=3,哈希表变为{1:1, 2:1, 3:1}。此时哈希表大小超过2,触发左指针移动。
- 当
-
左指针移动与计数调整:
- 移动
left,从left = 0开始,哈希表变为{1:0, 2:1, 3:1},然后移除键值对{1:0},哈希表变为{2:1, 3:1},此时left = 1。
- 移动
-
继续右指针移动:
- 当
right = 3,fruits[right]=2,哈希表变为{2:2, 3:1}。 - 当
right = 4,fruits[right]=2,哈希表变为{2:3, 3:1}。此时窗口内还是两种水果,不触发左指针移动。
- 当
结果计算
- 在整个过程中,不断更新
ret以记录最长子数组长度。当右指针遍历结束时,ret记录的是满足条件的最长子数组长度。 - 对于测试用例
[1,2,3,2,2],最长的连续子数组内包含两种水果的最大长度为从索引1到4,即[2,3,2,2],长度为4。
返回结果
- 因此,这段代码在处理完测试用例后返回的结果是
4。
🦣二.找到字符串中的所有字母异位词
题目链接:438.找到字符串中所有字母异位词
算法原理
-
初始化:首先将输入的字符串
ss和pp分别转换成字符数组s和p,便于后续操作。然后,创建两个大小为 26 的整型数组hash1和hash2作为哈希表,用于记录字符计数。hash2用于预存储字符串pp中各字符出现的频次。 -
预处理:遍历字符串
pp,在hash2中记录每个字符出现的次数。比如,如果pp是 "abc",那么hash2['a' - 'a'](即hash2[0])将增加 1,以此类推。 -
滑动窗口遍历:
- 使用两个指针
left和right初始化为 0,定义一个count记录当前窗口内满足条件的字符数量。 - 当
right指针遍历s时,更新hash1中对应字符的计数,并检查如果该字符的计数不大于hash2中的计数,则增加count。 - 当窗口大小(
right - left + 1)超过pp的长度时,说明需要收缩窗口左侧。此时,检查left指针所指字符的计数,如果它之前满足条件(即计数不大于hash2中的计数),则减少count,并减少hash1中该字符的计数,然后left指针右移。 - 当窗口内满足条件的字符数
count等于pp的长度时,说明找到了一个异位词,将left索引添加到结果列表ret中。
- 使用两个指针
-
返回结果:遍历结束后,返回存储了所有异位词起始索引的列表
ret。
时间复杂度与空间复杂度
- 时间复杂度:O(N),其中 N 为字符串
ss的长度。每个字符最多被遍历一次。 - 空间复杂度:O(1),虽然使用了两个大小为 26 的数组作为哈希表,但它们的空间需求是固定的,不依赖于输入字符串的大小。
代码
public List<Integer> findAnagrams(String ss, String pp) {
List<Integer> ret= new ArrayList<>();
char[] s=ss.toCharArray();
char[] p=pp.toCharArray();
int[] hash1=new int[26];
int[] hash2=new int[26];
//将pp对照的范本储存在hash2
for (char a:p){
hash2[a-'a']++;
}
int len=p.length;
for(int left=0,right=0,count=0;right<s.length;right++){
hash1[s[right]-'a']++;
if (hash1[s[right]-'a']<=hash2[s[right]-'a']){
count++;
}
if (right-left+1>len){
if (hash1[s[left]-'a']<=hash2[s[left]-'a']){
count--;
}
hash1[s[left++]-'a']--;
}
if (count==len){
ret.add(left);
}
}
return ret;
}
举例
测试用例 s = "cbaebabacd" ; p ="abc"
-
初始化: 定义两个长度为 26 的列表
hash1和hash2用于存储字符计数。hash2用于存储模式字符串p中每个字符出现的次数。 -
构建模式哈希: 遍历模式字符串
p,并使用 ASCII 值减去'a'的 ASCII 值作为索引来记录每个字符的出现次数到hash2中。 -
双指针遍历:
- 初始化两个指针
left和right以及一个计数器count来追踪当前窗口是否为有效异位词。 - 使用
right指针向右移动,增加右侧字符在hash1中的计数,如果该字符的计数小于等于hash2中的计数,则增加count。 - 当窗口大小(即
right - left + 1)超过模式字符串长度时,说明需要收缩窗口:- 减少左侧字符在
hash1中的计数,如果之前该字符的计数也小于等于hash2中的计数,则减少count。 - 左指针
left向右移动一位,缩小窗口。
- 减少左侧字符在
- 检查
count是否与模式字符串长度相等,如果相等则说明当前窗口是一个有效异位词,记录下其起始索引left。
- 初始化两个指针
-
收集结果: 所有满足条件的起始索引被收集到列表
ret中,并最终返回。
现在,针对给定的测试用例:
-
字符串
s = "cbaebabacd" -
模式
p = "abc" -
遍历开始,首先构建
hash2,hash2对于"abc"会是[1, 1, 1](因为每个字符出现一次)。 -
双指针开始滑动:
- 当
right = 0时,"c"计数增加,但不满足异位词条件(因为没有比较字符)。 - 移动到
"b"(right = 1),此时窗口"cb",仍然不构成异位词。 - 添加
"a"(right = 2),窗口变为"cba",这时hash1与hash2匹配(各字符计数都是 1),因此count = 3(与模式长度相等),记录索引0。 - 继续滑动,当
"e"进入窗口时(right = 3),由于它不在模式中,之前"c"的计数减一并不影响count(仍为 3),但窗口大小已超过模式长度,所以收缩左侧。 - 收缩窗口时,移除
"c"(left = 1),检查并可能更新count,但在这个例子中,直到"b"(left = 2) 被移除时count才会减少,因为它与模式中的字符匹配。 - 当窗口滑动到最后,到达
"bac"(在原字符串中的位置为索引 6 开始),再次满足异位词条件,记录索引6。
- 当
最终,函数正确返回了异位词子串的起始索引 [0, 6],这与我们的手动分析相符。
🐮三.串联所有单词的子串
题目链接:30.串联所有单词的子串
算法原理
-
初始化:
- 创建一个结果列表
ret用来存放符合条件的子串起始索引。 - 创建一个哈希表
hash1来记录单词数组words中每个单词出现的次数。 - 计算单个单词的长度
len和单词数组的大小size。
- 创建一个结果列表
-
构建单词频率哈希表:
- 遍历
words数组,使用哈希表hash1记录每个单词及其出现次数。
- 遍历
-
滑动窗口遍历字符串:
- 以单词长度
len为步长,从字符串s的每个可能的起始位置开始滑动窗口。- 对于每个起始位置
i,初始化一个新的哈希表hash2来记录窗口内各个单词的出现次数。 - 使用两个指针
left和right表示窗口的左右边界,初始时left = right = i。 - 移动
right指针,每次向右移动len个单位,将新进入窗口的单词in计数加入hash2。- 如果
hash2中in的计数不大于hash1中的计数,说明当前单词匹配成功,计数器count加一。
- 如果
- 当窗口大小(即
right - left + 1)超过所有单词组合的总长度(即len * size)时:- 移出窗口最左边的单词
out,更新hash2并相应减少count(如果必要)。 - 同时,左边界
left向右移动len个单位,缩小窗口。
- 移出窗口最左边的单词
- 如果
count等于size,说明窗口内的单词完全匹配了words中的所有单词(考虑顺序和数量),将left索引添加到结果列表ret中。
- 对于每个起始位置
- 以单词长度
-
返回结果: 遍历完成后,返回包含所有符合条件子串起始索引的列表
ret。
时间复杂度分析
- 外层循环遍历字符串
s,时间复杂度为 O(n),其中 n 是字符串s的长度。 - 内层循环虽然也是遍历字符串,但由于每次滑动窗口实际上是在“跳跃”(每次跳过一个单词长度),其复杂度受到单词长度和单词数组大小的影响,大致可视为 O(m*k),其中 m 是单词数组的大小,k 是单个单词的平均长度。
- 因此,总体时间复杂度大约为 O(nmk)。
空间复杂度
- 主要由哈希表占用,空间复杂度为 O(m+k),其中 m 是单词数组的大小,k 是不同单词的数量(在极端情况下,所有单词都不重复)。
代码
public List<Integer> findSubstring(String s, String[] words) {
List<Integer> ret = new ArrayList<>();
Map<String,Integer> hash1=new HashMap<>();
int len=words[0].length();
int size=words.length;
for(String str:words){
hash1.put(str,hash1.getOrDefault(str,0)+1);
}
for(int i=0;i<len;i++){
Map<String,Integer> hash2=new HashMap<>();
for(int left=i,right=i,count=0;right+len <=s.length();right+=len){
String in=s.substring(right,right+len);
hash2.put(in,hash2.getOrDefault(in,0)+1);
if(hash2.get(in)<=hash1.getOrDefault(in,0)){
count++;
}
if(right-left+1>len*size){
String out=s.substring(left,left+len);
if(hash2.get(out)<=hash1.getOrDefault(out,0)){
count--;
}
hash2.put(out,hash2.get(out)-1);
left+=len;
}
if(count==size){
ret.add(left);
}
}
}
return ret;
}
举例
测试用例 s ="barfoofoobarthefoobarman";words =["bar","foo","the"]
-
初始化:
- 初始化结果列表
ret。 - 创建哈希表
hash1存储单词及其计数,初始化为空。 - 单词长度
len = 3(因为words[0].length()是 "bar" 的长度)。 - 单词数组大小
size = 3(因为有三个单词)。 - 遍历
words,填充hash1,使其内容为{bar=1, foo=1, the=1}。
- 初始化结果列表
-
滑动窗口遍历:
-
关键在于理解滑动窗口如何准确地识别出所有匹配的子串起始位置。
-
第一个匹配:
- 窗口从索引 0 开始,但首个匹配实际上从索引 6 开始,因为
"foobarthefoobarman"中从索引 6("f") 开始的子串包含了"foo","bar","the"(按照"foobarthe"的顺序)。此时,count达到size(3),因此6被添加到结果列表中。
- 窗口从索引 0 开始,但首个匹配实际上从索引 6 开始,因为
-
第二个匹配:
- 窗口继续右移,当右边界达到索引 9 时,子串变为
"oobarthefoo",其中包含了另一个"foo","bar","the"序列(注意,尽管"foo"重叠,但因为我们是寻找无序的组合,所以依然有效)。此时,也会触发count == size,因此索引9被记录。
- 窗口继续右移,当右边界达到索引 9 时,子串变为
-
第三个匹配:
- 最后一个匹配发生在索引 12,从这里开始的子串为
"obarfoobarman",同样包含了所需的三个单词(忽略顺序)。因此,索引12也被加入结果列表。
- 最后一个匹配发生在索引 12,从这里开始的子串为
-
-
结果收集:
- 正确的输出是
[6, 9, 12],这三个位置开始的子串分别包含了给定单词数组["bar", "foo", "the"]中所有单词的一个排列。
- 正确的输出是
🐏 四.最小覆盖串
题目链接:76.最小覆盖子串
算法原理
-
初始化:
- 将输入字符串
ss和tt转换为字符数组s和t以便于操作。 - 初始化两个哈希表(在这里使用整型数组
hash1和hash2)来记录字符出现的次数。数组的索引对应字符的 ASCII 码值,值表示字符出现的次数。 - 初始化变量
kind记录需要匹配的字符种类数(即tt中不同字符的数量),初始化最小长度minlen为最大整数值,记录子串起始索引begin为-1。 - 遍历字符串
tt,统计每个字符的出现次数到hash1,同时增加kind的值,表示需要匹配的字符种类。
- 将输入字符串
-
滑动窗口遍历:
- 使用两个指针
left和right构成一个滑动窗口,初始时left = right = 0。 - 当
right指针遍历s时,对进入窗口的字符in增加hash2的计数,若该字符在hash2中的计数等于其在hash1中的计数,则说明这个字符已经匹配完成,增加计数器count。 - 当
count等于kind时,说明窗口内的字符已经包含了tt中所有字符至少各一次,此时尝试缩小窗口以寻找最小覆盖子串:- 检查当前窗口长度是否小于已记录的最小长度
minlen,若是,则更新minlen和子串起始索引begin。 - 然后,从窗口左侧移除字符
out(即s[left]),并更新hash2和count,随后移动left指针向右,继续寻找可能更小的覆盖子串。
- 检查当前窗口长度是否小于已记录的最小长度
- 使用两个指针
-
结果处理:
- 循环结束后,检查是否有找到符合条件的子串,如果没有(即
begin == -1),返回空字符串;否则,返回ss中从begin开始长度为minlen的子串。
- 循环结束后,检查是否有找到符合条件的子串,如果没有(即
算法特性
- 时间复杂度: O(N),其中 N 是字符串
ss的长度。每个字符最多被访问两次:一次作为窗口的右边界,一次作为窗口的左边界。 - 空间复杂度: O(1),尽管使用了额外的哈希表,但因为字符集固定(ASCII码范围),所以空间复杂度是常数级别的。
代码
public String minWindow(String ss, String tt) {
char[] s=ss.toCharArray();
char[] t=tt. toCharArray();
int[] hash1=new int[128];
int[] hash2=new int[128];
int kind=0;
int minlen=Integer.MAX_VALUE;
int begin=-1;
for(char ch:t){
if(hash1[ch]++==0)kind++;
}
for(int left=0,right=0,count=0;right<s.length;right++){
char in=s[right];
if(++hash2[in]==hash1[in])count++;
while(kind==count){
if(right-left+1<minlen){
begin=left;
minlen=right-left+1;
}
char out=s[left];
if(hash2[out]--==hash1[out])count--;
left++;
}
}
if(begin==-1){
return new String();
}
return ss.substring(begin,begin+minlen);
}
举例
测试用例 s ="ADOBECODEBANC" ; t ="ABC"
-
初始化:
- 将字符串
s和t转换为字符数组。 - 初始化两个长度为 128 的数组
hash1和hash2作为哈希表,记录字符出现次数。 - 初始化变量
kind为 0,用于记录目标字符串t中不同字符的种类数;minlen设置为Integer.MAX_VALUE,用于记录最小覆盖子串长度;begin设置为-1,用于记录最小覆盖子串的起始位置。 - 遍历目标字符串
t,对hash1进行填充并计算种类数kind。在此例中,hash1['A'] = 1,hash1['B'] = 1,hash1['C'] = 1,因此kind = 3。
- 将字符串
-
滑动窗口遍历:
- 使用双指针
left和right构建滑动窗口,初始时left = 0,right = 0,count = 0。 - 遍历字符串
s:- 当右指针
right移动时,遇到的字符in = s[right],在hash2中计数递增,如果这个递增使得hash2[in]等于hash1[in],说明字符in已经匹配完成,于是count++。 - 当
count等于kind时,说明窗口内的字符已经包含了t中所有字符至少各一次。此时开始收缩窗口:- 检查当前窗口长度是否小于
minlen,若是,则更新minlen和begin。 - 移除窗口左侧的字符
out = s[left],在hash2中对应的计数递减,如果递减后hash2[out]等于hash1[out],说明移除的字符不再满足匹配条件,于是count--。 - 然后
left++,继续检查下一个可能的窗口。
- 检查当前窗口长度是否小于
- 当右指针
- 在这个过程中,对于测试用例,窗口会遍历并调整位置,直到找到包含
"ABC"所有字符的最小子串。具体来说,当窗口覆盖"BANC"时,满足条件,此时count = 3,并且窗口长度是最小的。
- 使用双指针
-
结果处理:
- 最终,当遍历结束,如果找到了有效的子串(即
begin != -1),根据记录的begin和minlen截取并返回最小覆盖子串。对于给定测试用例,返回结果是"BANC"。
- 最终,当遍历结束,如果找到了有效的子串(即
🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀
以上,就是本期的全部内容啦,若有错误疏忽希望各位大佬及时指出💐
制作不易,希望能对各位提供微小的帮助,可否留下你免费的赞呢🌸






















