文章目录
前言
同向指针算法是一种使用两个指针,它们同时向同一个方向移动,以解决问题的算法技巧。这两个指针可以指向数组或字符串中的不同位置,根据问题的要求,它们可以以不同的速度移动。期间会产生一段区间,因此我们可以比喻成一个窗口,而我们可以在这个窗口内对数据进行一些我们想要的处理,由于这个区间一般都会向后移动,所以我们也可以称之为滑动窗口。
例如,在数组中查找目标元素时,可以使用一个指针从数组的起始位置开始遍历,另一个指针从数组的末尾开始遍历,两个指针同时向数组的中间移动。当一个指针遇到目标元素时,另一个指针可以确定目标元素在数组中的位置。
1. 长度最小的子数组(medium)
1. 题目链接:209. 长度最小的子数组
2. 题目描述:
给定一个含有n
个正整数的数组和一个正整数target
。
找出该数组中满足其总和大于等于target
的长度最小的 连续子数组
[numsl, numsl + 1, ..., numsr - 1, numsr]
,并返回其长度。如果不存在符合条件的子数组,返回0
。
示例 1:
输入:target = 7
, nums = [2,3,1,2,4,3]
输出:2
解释:子数组[4,3]
是该条件下的长度最小的子数组。
解法一:(暴力求解)(会超时)
算法思路:
「从前往后」枚举数组中的任意一个元素,把它当成起始位置。然后从这个「起始位置」开始,然后寻找一段最短的区间,使得这段区间的和「大于等于」目标值。将所有元素作为起始位置所得的结果中,找到「最小值」即可。
C++代码如下:
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
// 记录结果
int ret = INT_MAX;
int n = nums.size();
// 枚举出所有满⾜和⼤于等于 target 的⼦数组[start, end]
// 由于是取到最⼩,因此枚举的过程中要尽量让数组的⻓度最⼩
// 枚举开始位置
for (int start = 0; start < n; start++)
{
int sum = 0; // 记录从这个位置开始的连续数组的和
// 寻找结束位置
for (int end = start; end < n; end++)
{
sum += nums[end]; // 将当前位置加上
if (sum >= target) // 当这段区间内的和满⾜条件时
{
// 更新结果,start 开头的最短区间已经找到
ret = min(ret, end - start + 1);
break;
}
}
}
// 返回最后结果
return ret == INT_MAX ? 0 : ret;
}
};
解法二(滑动窗口):
算法思路:
由于此问题分析的对象是「⼀段连续的区间」,因此可以考虑「滑动窗口」的思想来解决这道题。让窗口满足:从i
位置开始,窗口内所有元素的和小于 target
(那么当窗口内元素之和第一次大于等于目标值的时候,就是i
位置开始,满足条件的最小长度)。
做法:将右端元素划入窗口中,统计出此时窗口内元素的和:
- 如果窗口内元素之和大于等于
target
:更新结果,并且将左端元素划出去的同时继续判断是否满足条件并更新结果(因为左端元素可能很小,划出去之后依旧满足条件) - 如果窗口内元素之和不满足条件:
right++
,另下⼀个元素进入窗口。
为何滑动窗口可以解决问题,并且时间复杂度更低?
- 这个窗口寻找的是:以当前窗口最左侧元素(记为
left1
)为基准,符合条件的情况。也就是在这道题中,从left1
开始,满足区间和sum >= target
时的最右侧(记为right1
)能到哪里。 - 我们既然已经找到从
left1
开始的最优的区间,那么就可以大胆舍去left1
。但是如果继续像方法一 一样,重新开始统计第二个元素(left2
)往后的和,势必会有大量重复的计算(因为我们在求第一段区间的时候,已经算出很多元素的和了,这些和是可以在计算下次区间和的时候用上的)。 - 此时,
rigth1
的作用就体现出来了,我们只需将left1
这个值从sum
中剔除。从right1
这个元素开始,往后找满足left2
元素的区间(此时right1
也有可能是满足的,因为left1
可能很小。sum
剔除掉 left1 之后,依旧满足大于等于target
)。这样我们就能省掉大量重复的计算。 - 这样我们不仅能解决问题,而且效率也会大大提升。
时间复杂度: 虽然代码是两层循环,但是我们的left
指针和right
指针都是不回退的,两者最多都往后移动n
次。因此时间复杂度是O(N)
。
C++代码实现:
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int len = INT_MAX, n = nums.size(), sum = 0;
for(int left = 0, right = 0;right < n;++right)
{
sum += nums[right]; // 进窗口
while(sum >= target) // 判断条件
{
len = min(len, right - left + 1); // 更新结果
sum -= nums[left++]; // 出窗口
}
}
return len == INT_MAX ? 0 : len;
}
};
2. 无重复字符的最长子串(medium)
1. 题目链接:3.无重复字符串的最长子串
2. 题目描述:
给定一个字符串 s
,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: s = "abcabcbb"
输出:3
解释: 因为无重复字符的最长子串是 "abc"
,所以其长度为 3
。
示例 2:
输入: s = "bbbbb"
输出:1
解释: 因为无重复字符的最长子串是 "b"
,所以其长度为 1
。
提示:
• 0 <= s.length <= 5 * 10^4
•s
由英文字目、数字、符号和空格组成 1
3. 解法一:(暴力求解)(不会超时,可以通过)
算法思路:
枚举「从每⼀个位置」开始往后,无重复字符的子串可以到达什么位置。找出其中长度最大的即可。
在往后寻找无重复子串能到达的位置时,可以利用「哈希表」统计出字符出现的频次,来判断什么时候子串出现了重复元素。
C++代码实现:
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int left = 0, n = s.size(), len = 0;
while(left < n)
{
int hash[128] = {0}; // 创建一个数组来模拟哈希表
int right = left;
while(right < n)
{
hash[s[right]]++;
if(hash[s[right]] > 1)
{
break;
}
len = max(len, right - left + 1);
++right;
}
++left;
}
return len;
}
};
4. 解法二:(滑动窗口)
算法思路:
研究的对象依旧是一段连续的区间,因此继续使用「滑动窗口」思想来优化。
让滑动窗口满足:窗口内所有元素都是不重复的。
做法:右端元素 ch
进入窗口的时候,哈希表统计这个字符的频次:
- 如果这个字符出现的频次超过
1
,说明窗口内有重复元素,那么就从左侧开始划出窗口,直到ch
这个元素的频次变为1
,然后再更新结果。 - 如果没有超过
1
,说明当前窗口没有重复元素,可以直接更新结果。
C++代码实现:
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int hash[128] = {0}; // 创建数组来模拟哈希表
int ret = 0, n = s.size();
for(int left = 0, right = 0;right < n;++right)
{
hash[s[right]]++; // 进窗口
while(hash[s[right]] > 1) // 判断条件
{
hash[s[left++]]--; // 出窗口
}
ret = max(ret, right - left + 1); // 更新结果
}
return ret;
}
};
3. 最大连续 1 的个数 III(medium)
1. 题目链接:1004. 最大连续 1 的个数 III
2. 题目描述:
给定一个二进制数组nums
和一个整数 k
,如果可以翻转最多k
个 0
,则返回 数组中连续1
的最大个数 。
示例 1:
输入:nums = [1,1,1,0,0,0,1,1,1,1,0]
, K = 2
输出:6
解释:[1,1,1,0,0,1
,1,1,1,1,1
]
粗体数字从0
翻转到 1
,最长的子数组长度为 6
。
3. 解法(滑动窗口):
算法思路:
不要去想怎么翻转,不要把问题想的很复杂,这道题的结果无非就是⼀段连续的1
中间塞了 k
个 0
。
因此,我们可以把问题转化成:求数组中一段最长的连续区间,要求这段区间内0
的个数不超过k
个。
既然是连续区间,可以考虑使用「滑动窗口」来解决问题。
算法流程:
a. 初始化一些变量left = 0
,right = 0
, ret = 0
;
b. 当right
小于数组大小的时候,一直下列循环:
- 让当前元素进入窗口
- 检查
0
的个数是否超标- 如果超标,依次让左侧元素滑出窗口,直到
0
的个数恢复正
常;
- 如果超标,依次让左侧元素滑出窗口,直到
- 程序到这里,说明窗口内元素是符合要求的,更新结果;
right++
,处理下一个元素;
c. 循环结束后,ret
存的就是最终结果。
C++代码如下:
class Solution {
public:
int longestOnes(vector<int>& nums, int k) {
int n = nums.size(), ret = 0;
int count_zero = 0;
for(int left = 0, right = 0;right < n;++right)
{
if(!nums[right]) // 进窗口时,遇到0,计数器++
++count_zero;
while(count_zero > k)
if(nums[left++] == 0) // 出窗口时,遇到0.计数器--
--count_zero;
ret = max(ret, right - left + 1); // 更新结果
}
return ret;
}
};
4. 将 x 减到 0 的最小操作数 (medium)
1. 题目链接:1658. 将 x 减到 0 的最小操作数
2. 题目描述:
给你一个整数数组nums
和一个整数x
。每一次操作时,你应当移除数组nums
最左边或最右边的元素,然后从x
中减去该元素的值。请注意,需要 修改 数组以供接下来的操作使用。
如果可以将x
恰好 减到0
,返回 最小操作数 ;否则,返回- 1
。
示例 1:
输入:nums = [1,1,4,2,3]
, x = 5
输出:2
解释:最佳解决方案是移除后两个元素,将x
减到0
。
示例 2:
输入:nums = [5,6,7,8,9]
, x = 4
输出:-1
示例 3:
输入:nums = [3,2,20,1,1,3]
, x = 10
输出:5
解释:最佳解决方案是移除后三个元素和前两个元素(总共 5
次操作),将x
减到0
。
3. 解法:(滑动窗口)
算法思路:
题目要求的是数组「左端 + 右端」两段连续的、和为x
的最短数组,信息量稍微多一些,不易理清思路;我们可以转化成求数组内一段连续的、和为sum(nums) - x
的最长数组。此时,就是熟悉的「滑动窗口」问题了。
算法流程:
a. 转化问题:求target = sum(nums) - x
。如果target < 0
,问题无解;
b.初始化左右指针 l = 0
, r = 0
(滑动窗口区间表时为[l, r]
,左右区间是否开闭很重要,必须设定与代码一致),记录当前滑动窗口内数组和的变量sum = 0
,记录当前满足条件数组的最大区间长度 maxLen = -1
;
c. 当 r 小于等于数组长度时,一直循环:
- 如果
sum < target
,右移右指针,直至变量和大于等于target
,或右指针已经移到头; - 如果
sum > target
,右移左指针,直至变量和小于等于target
,或左指针已经移到头; - 如果经过前两步的左右移动使得
sum == target
,维护满足条件数组的最大长度,并让下个元素进入窗口;
d. 循环结束后,如果maxLen
的值有意义,则计算结果返回;否则,返回 -1
。
C++代码实现:
class Solution {
public:
int minOperations(vector<int>& nums, int x) {
int sum_nums = 0, n = nums.size();
for(int& e : nums)
sum_nums += e; // 求出数组的总和
if(sum_nums < x) // 处理细节问题
return -1;
int target = sum_nums - x, cur = 0, maxLen = -1;
for(int left = 0, right = 0;right < n;++right)
{
cur += nums[right]; // 入窗口
while(cur > target) // 判断条件
cur -= nums[left++]; // 出窗口
if(cur == target)
maxLen = max(maxLen, right - left + 1); // 满足条件,更新结果
}
return maxLen == -1 ? -1 : n - maxLen;
}
};
5. 水果成篮(medium)
1. 题目链接:904.水果成篮
2. 题目描述:
你正在探访一家农场,农场从左到右种植了一排果树。这些树用一个整数数组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]
这五棵树。
3. 解法(滑动窗口):
算法思路:
研究的对象是⼀段连续的区间,可以使用「滑动窗口」思想来解决问题。
让滑动窗口满足: 窗口内水果的种类只有两种。
做法: 右端水果进入窗口的时候,用哈希表统计这个水果的频次。这个水果进来后,判断哈希表的大小。
- 如果大小超过 2:说明窗口内水果种类超过了两种。那么就从左侧开始依次将水果划出窗口,直到哈希表的大小小于等于 2,然后更新结果;
- 如果没有超过 2,说明当前窗口内水果的种类不超过两种,直接更新结果 ret。
算法流程:
a. 初始化哈希表 hash
来统计窗口内水果的种类和数量;
b. 初始化变量:左右指针 left = 0
,right = 0
,记录结果的变量 ret = 0
;
c. 当 right
小于数组大小的时候,一直执行下列循环:
-
将当前水果放入哈希表中;
-
判断当前水果进来后,哈希表的大小:
- 将左侧元素滑出窗口,并且在哈希表中将该元素的频次减一;
- 如果这个元素的频次减一之后变成了 0,就把该元素从哈希表中删除;
- 重复上述两个过程,直到哈希表中的大小不超过 2;
-
更新结果 ret
-
++right,让下一个元素进入窗口
d.循环结束后ret存的就是最终结果。
C++代码实现:
class Solution {
public:
int totalFruit(vector<int>& fruits) {
unordered_map<int, int> hash;
int n = fruits.size(), ret = 0;
for(int left = 0, right = 0;right < n;++right)
{
hash[fruits[right]]++; // 入窗口
while(hash.size() > 2) // 判断条件
{
hash[fruits[left]]--;
if(hash[fruits[left]] == 0)
hash.erase(fruits[left]);
++left; // 出窗口
}
ret = max(ret, right - left + 1); // 更新结果
}
return ret;
}
};
6. 找到字符串中所有字母异位词(medium)
1. 题目链接:438.找到字符串中所有字母异位词
2. 题目描述:
给定两个字符串 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
仅包含小写字母
3. 解法(滑动窗口 + 哈希表):
算法思路:
-
研究对象是连续的区间,因此可以尝试使⽤滑动窗口的思想来解决。
-
如何判断当前窗口内的所有字符是符合要求的呢?
我们可以使用两个哈希表,其中⼀个将目标串的信息统计起来,另⼀个哈希表动态的维护窗口内字符串的信息。
当动态哈希表中包含目标串中所有的字符,并且对应的个数都不小于目标串的哈希表中各个字
符的个数,那么当前的窗口就是一种可行的方案。
算法流程:
a.定义两个哈希表: 1
号哈希表 hash1
用来记录子串的信息, 2
号哈希表 hash2
用来记录目标串 t
的信息;
b.实现一个接口函数check()
,判断当前窗口是否满足要求:
- 遍历两个哈希表中对应位置的元素:
如果t
中某个字符的数量大于窗口中字符的数量,也就是2
号哈希表某个位置大于1
号哈希表。说明不匹配,返回false
; - 如果全都匹配,返回
true
主函数中:
a. 先将 t
的信息放入 2
号哈希表中;
b.初始化⼀些变量:左右指针: left = 0
,right = 0
;目标子串的长度: len =INT_MAX
(也可以令它等于字符串的总长度,效果是一样的) ;目标子串的起始位置: retleft
;(通过目标子串的起始位置和长度,我们就能找到结果)
c. 当 right
小于字符串 s
的长度时,一直下列循环:
- 将当前遍历到的元素扔进
1
号哈希表中; - 检测当前窗口是否满足条件:
- 如果满足条件:
- 判断当前窗口是否变小。如果变小:更新长度
len
,以及字符串的起始位置retleft
; - 判断完毕后,将左侧元素滑出窗口,顺便更新
1
号哈希表; - 重复上面两个过程,直到窗口不满足条件;
- 判断当前窗口是否变小。如果变小:更新长度
- 如果满足条件:
right++
,遍历下⼀个元素;
d. 判断 len
的长度是否等于 INT_MAX
:
- 如果相等,说明没有匹配,返回空串;
- 如果不想等,说明匹配,返回
s
中从retleft
位置往后len
长度的字符串。
【注意】
这里题目说明,s 和 p 仅包含小写字母,所以我们可以定义一个数组来记录每个字符出现频次。
C++代码实现:
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
vector<int> ret;
// 创建两个数组模拟哈希表
int hash1[26] = { 0 }; // 存放 p 字符串中的字符
int hash2[26] = { 0 }; // 存放 s 字符串中的字符
for(char e : p) // 将 p 字符串中的字符出现的个数存入到数组中
hash1[e - 'a']++;
int sn = s.size(), pn = p.size();
int count = 0; // 定义一个计数器来判断是否更新结果
for(int left = 0, right = 0;right < sn;++right)
{
// 存入的是hash1中有的字符(先存储再比较),计数器++
char in = s[right];
if(++hash2[in - 'a'] <= hash1[in - 'a'])
++count;
if(right - left + 1 > pn) // 保持我们维护的部分不超过pn
{
char out = s[left];
// 如果超过,判断我们减的数是否为hash1中的数,如果是count--,不是就直接将left向右移
if(hash2[out - 'a']-- <= hash1[out - 'a'])
--count;
++left;
}
if(count == pn)
ret.push_back(left);
}
return ret;
}
};
7. 串联所有单词的子串(hard)
1. 题目链接:串联所有单词的子串
2. 题目描述:
给定一个字符串 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"]
顺序排列的连接。
3. 解法:(滑动窗口 + 哈希表)
算法思路:
如果我们把每⼀个单词看成⼀个⼀个字母,问题就变成了找到「字符串中所有的字母异位词」。无非就是之前处理的对象是⼀个⼀个的字符,我们这里处理的对象是一个一个的单词。
C++代码实现:
class Solution
{
public:
vector<int> findSubstring(string s, vector<string>& words)
{
vector<int> ret;
unordered_map<string, int> hash1; // 保存 words ⾥⾯所有单词的频次
for (auto& s : words) hash1[s]++;
int len = words[0].size(), m = words.size();
for (int i = 0; i < len; i++) // 执⾏ len 次
{
unordered_map<string, int> hash2; // 维护窗⼝内单词的频次
for (int left = i, right = i, count = 0; right + len <= s.size();
right += len)
{
// 进窗⼝ + 维护 count
string in = s.substr(right, len);
hash2[in]++;
if (hash1.count(in) && hash2[in] <= hash1[in]) count++;
// 判断
if (right - left + 1 > len * m)
{
// 出窗⼝ + 维护 count
string out = s.substr(left, len);
if (hash1.count(out) && hash2[out] <= hash1[out]) count--;
hash2[out]--;
left += len;
}
// 更新结果
if (count == m) ret.push_back(left);
}
}
return ret;
}
};
8. 最小覆盖子串(hard)
1. 题目链接:最小覆盖子串
2. 题目描述:
给你一个字符串 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
的子串中,
因此没有符合条件的子字符串,返回空字符串。
3. 解法:(滑动窗口 + 哈希表)
算法思路:
- 研究对象是连续的区间,因此可以尝试使用滑动窗口的思想来解决。
- 如何判断当前窗口内的所有字符是符合要求的呢?
我们可以使用两个哈希表,其中一个将目标串的信息统计起来,另⼀个哈希表动态的维护窗口内字符串的信息。
当动态哈希表中包含目标串中所有的字符,并且对应的个数都不小于目标串的哈希表中各个字符的个数,那么当前的窗口就是一种可行的方案。
算法流程:
a. 定义两个的哈希表: 1
号哈希表 hash1
用来记录子串的信息, 2
号哈希表 hash2
用来记录目标串 t
的信息;
b. 定义一个键值对pos_len
,用来记录在目标串中符合要求的位置和长度。
c. 定义一个count
变量来统计目标串t中的元素种类(不是每个元素出现的频次)
- 如果遇到
hash1
的元素,并且出现频次相同(频次小于或大于都不行),count++
(进窗口); - 判断
count
是否等于hash1
中元素个数(不含元素频次),如果相等且为最小值,保存在键值对pos_len
中。 - 如果在目标串中找到了第一次满足要求的子串,那么就移动
left
减小目标串(出窗口),看是否符合要求,在left
移动的过程中,还要判断出窗口的元素是否与hash1
中该元素的种类和频次是否相同,如果相同,count--
C++代码实现:
class Solution {
public:
string minWindow(string s, string t) {
if(t.size() > s.size()) // 如果t字符串的长度比s大,那么可以直接返回空串
return "";
unordered_map<char, int> hash1; // 统计字符串 t 中每个字符出现的频次
unordered_map<char, int> hash2; // 统计窗口内每个字符出现的频次
string ret;
int n = s.size();
pair<int,int> pos_len(-1,n + 1); // 定义一个键值对,记录最小的子串的起始位置以及长度
for(char e : t) // 将字符串 t 中的字符映射到哈希表中
hash1[e]++;
// count 变量用来统计字符的种类(不是次数),
// 如果种类相同,则说明窗口内包含t中的所以字符(频次在下面的操作可控制)
for(int left = 0, right = 0, count = 0;right < n;++right)
{
char in = s[right];
++hash2[in]; // 进窗口
// 判断进入窗口的字符的频次是否等于hash1中对应该字符的频次,如果是count++
if(hash1.count(in) && hash2[in] == hash1[in]) ++count;
while(count == hash1.size())
{
// 如果count==hash1.size(),说明此时窗口内的已包含有t中所有字符
// 此时再看最小长度是否需要更新即可
if(count == hash1.size() && (pos_len.second > right - left + 1))
pos_len = pair<int,int>(left, right - left + 1);
char out = s[left];
// 判断出窗口的字符的频次是否等于hash1中对应该字符的频次,--count
if(hash1.count(out) && hash2[out]-- == hash1[out]) --count;
++left; // 出窗口
}
}
if(pos_len.second != n + 1)
ret = s.substr(pos_len.first, pos_len.second);
return ret;
}
};