1.长度最小的子数组
解法一:暴力解法
时间复杂度:O(N^2)
算法思路:
从前往后枚举数组中的任意⼀个元素,把它当成起始位置。然后从这个「起始位置」开始,然 后寻找⼀段最短的区间,使得这段区间的和「⼤于等于」⽬标值。 将所有元素作为起始位置所得的结果中,找到最⼩值即可。
cpp
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;
}
};
解法二:滑动窗口
时间复杂度:O(N)
算法思路:
由于此问题分析的对象是「一段连续的区间」,因此可以考虑「滑动窗口」的思想来解决这道题。
让滑动窗口满足:从 i 位置开始,窗口内所有元素的和小于 target(那么当窗口内元素之和第一次大于等于目标值的时候,就是 i 位置开始,满足条件的最小长度)。
做法:将右端元素划入窗口中,统计出此时窗口内元素的和:
- 如果窗口内元素之和大于等于 target:更新结果,并且将左端元素划出去的同时继续判断是否满足条件并更新结果(因为左端元素可能很小,划出去之后依旧满足条件)
- 如果窗口内元素之和不满足条件:right++,另下一个元素进入窗口。
为何滑动窗⼝可以解决问题,并且时间复杂度更低?
-
这个窗口寻找的是:以当前窗口最左侧元素(记为 left1)为基准,符合条件的情况。也就是在这道题中,从 left1 开始,满足区间和 sum >= target 时的最右侧(记为 right1)能到哪里。
-
我们既然已经找到从 left1 开始的最优的区间,那么就可以大胆舍去 left1。但是如果继续像方法一一样,重新开始统计第二个元素(left2)往后的和,势必会有大量重复的计算(因为我们在求第一段区间的时候,已经算出很多元素的和了,这些和是可以在计算下次区间和的时候用上的)。
-
此时,right1 的作用就体现出来了,我们只需将 left1 这个值从 sum 中剔除。从 right1 这个元素开始,往后找满足 left2 元素的区间(此时 right1 也有可能是满足的,因为 left1 可能很小。sum 剔除掉 left1 之后,依旧满足大于等于 target)。这样我们就能省掉大量重复的计算。
-
这样我们不仅能解决问题,而且效率也会大大提升。
时间复杂度:虽然代码是两层循环,但是我们的 left 指针和 right 指针都是不回退的,两者最多都往后移动 n 次。因此时间复杂度是 O (N)。
cpp
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int left = 0,right = 0;
int n = nums.size();
int sum = 0;
int len = INT_MAX;
for(left = 0,right = 0;right<n;right++)
{
sum+=nums[right];//进窗口
while(sum>=target)//判断
{
len = min(len,right-left+1);
sum-=nums[left];//出窗口
left++;
}
}
return len==INT_MAX?0:len;
}
};
2.无重复字符的最长字串
算法一:暴力解法
算法思路: 枚举从每⼀个位置开始往后,⽆重复字符的⼦串可以到达什么位置。找出其中⻓度最⼤的即可。 在往后寻找⽆重复⼦串能到达的位置时,可以利⽤哈希表统计出字符出现的频次,来判断什么 时候⼦串出现了重复元素。
cpp
//暴力解法
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int ret = 0; // 记录结果
int n = s.length();
// 1. 枚举从不同位置开始的最长重复子串
// 枚举起始位置
for (int i = 0; i < n; i++) {
// 创建一个哈希表,统计频次
int hash[128] = { 0 };
// 寻找结束为止
for (int j = i; j < n; j++) {
hash[s[j]]++; // 统计字符出现的频次
if (hash[s[j]] > 1) { // 如果出现重复的
break;
}
// 如果没有重复,就更新ret
ret = max(ret, j - i + 1);
}
}
// 2. 返回结果
return ret;
}
};
算法二:
算法思路: 研究的对象依旧是⼀段连续的区间,因此继续使⽤「滑动窗⼝」思想来优化。 让滑动窗⼝满⾜:窗⼝内所有元素都是不重复的。
做法:右端元素 left进⼊窗⼝的时候,哈希表统计这个字符的频次: 如果这个字符出现的频次超过 1 ,说明窗⼝内有重复元素,那么就从左侧开始划出窗⼝, 直到 left这个元素的频次变为 1 ,然后再更新结果。 如果没有超过 1 ,说明当前窗⼝没有重复元素,可以直接更新结果。
cpp
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int hash[128] = {0};
int left = 0,right = 0,n = s.size();
int ret = 0;
while(right<n)
{
hash[s[right]]++;//进窗口
while(hash[s[right]]>1)//判断
{
hash[s[left++]]--;//出窗口
}
//更新
ret = max(ret,right-left+1);
right++;
}
return ret;
}
};
3.最大连续1的个数III
类似这种连续区间,我们可以考虑使用滑动窗口
算法思路:滑动窗口
初始化一个计数器;初始化一些变量 left = 0,right = 0,ret = 0;
当right小于数组大小的时候,一直有以下循环
让当前元素进入窗口,同时要统计0的个数:如果0的数量超过了k,那么我们需要让左侧元素滑出窗口,顺便更新我们的计数器,直到我们的计数器恢复为小于k。
然后right继续向后走,处理下一个元素。
循环结束后,ret 存的就是最终结果。
cpp
class Solution {
public:
int longestOnes(vector<int>& nums, int k) {
int left = 0,right = 0,n = nums.size();
int count = 0;//计数
int ret = 0;
while(right<n)
{
if(nums[right]==0)
{
count++;
}
while(count>k)//判断0的数量是否合法
{
if(nums[left++]==0)//出窗口
{
count--;
}
}
ret = max(ret,right-left+1);
right++;//入窗口
}
return ret;
}
};
4.将x减到0的最小操作数
算法思路:滑动窗口,时间复杂度O(N)
转化问题:
将问题转化为求 target = sum(nums) - x。
如果 target < 0,问题无解,return -1。
初始化:
左右指针 left = 0,right= 0(滑动窗口区间表示为 [l, r],左右区间是否开闭很重要,必须设定与代码一致)。
记录当前滑动窗口内数组和的变量 sum = 0。
记录当前满足条件数组的最大区间长度 maxLen = -1。
循环条件:
进窗口
当 r 小于等于数组长度时,一直循环:
如果 sum < target,右移右指针,直至变量和大于等于 target,或右指针已经移到头。
出窗口
如果 sum > target,右移左指针,直至变量和小于等于 target,或左指针已经移到头。
等于target时才会更新
如果经过前两步的左右移动使得 sum == target,维护满足条件数组的最大长度,并让下个元素进入窗口。
循环结束后:
如果 maxLen 的值有意义,则计算结果返回;否则,返回 -1。
cpp
class Solution {
public:
int minOperations(vector<int>& nums, int x) {
int left = 0, right = 0;
int n = nums.size();
int len = -1;
int sum = 0;
for(int i = 0;i<n;i++)
{
sum+=nums[i];
}
int target = sum-x;
if(target<0) return -1;
sum = 0;
while(right<n)
{
sum+=nums[right];//进窗口
while(sum>target)//出窗口
{
sum-=nums[left++];
}
if(sum==target)
{
len = max(len,right-left+1);
}
right++;
}
return len==-1?len:n-len;
}
};
5.水果成蓝
这道题目,我们先来思考一下暴力解法,暴力解法的思路比较简答,给一个hash表统计每一个水果出现了多少种,就是把所有符合要求的长度都枚举出来,取最长的即可。
然后讲解一下为什么要用滑动窗口来解决:
滑动窗口解题思路:
a.初始化哈希表hash来统计窗⼝内⽔果的种类和数量;
b. 初始化变量:左右指针left=0,right=0,记录结果的变量ret=0;
c. 当right⼩于数组⼤⼩的时候,⼀直执⾏下列循环:
i. 将当前⽔果放⼊哈希表中;
ii. 判断当前⽔果进来后,哈希表的⼤⼩:
•
如果超过2:
将左侧元素滑出窗⼝,并且在哈希表中将该元素的频次减⼀;
如果这个元素的频次减⼀之后变成了0,就把该元素从哈希表中删除;
重复上述两个过程,直到哈希表中的⼤⼩不超过2;
iii. 更新结果ret;
iv. right++,让下⼀个元素进⼊窗⼝;
d. 循环结束后,ret存的就是最终结果。
这个长度是有限制的,所以我们这里直接用一个自定义一个hash数组就行了,可以提高一点效率。
cpp
class Solution {
public:
int totalFruit(vector<int>& fruits) {
// 用于记录每种水果出现的频次,数组大小设为一个较大值,因为水果种类的值范围已经给定,这里设为100001
int hash[100001] = {0};
int ret = 0;
int n = fruits.size();
// 滑动窗口的左右指针,kind用于记录当前窗口内水果的种类数
for (int left = 0, right = 0, kind = 0; right < n; right++) {
// 当新水果进入窗口时
if (hash[fruits[right]] == 0) {
kind++;
}
hash[fruits[right]]++;
// 如果窗口内水果种类超过2种
while (kind > 2) {
// 将左指针指向的水果移出窗口
hash[fruits[left]]--;
if (hash[fruits[left]] == 0) {
kind--;
}
left++;
}
// 更新最长符合条件的子数组长度
ret = max(ret, right - left + 1);
}
return ret;
}
};
6.找到字符串中所有字母异位词
算法思路:滑动窗口(定长滑窗)
时间复杂度O(N)
因为字符串 p 的异位词的⻓度⼀定与字符串 p 的⻓度相同,所以我们可以在字符串 s 中构 造⼀个⻓度为与字符串 p 的⻓度相同的滑动窗⼝,并在滑动中维护窗⼝中每种字⺟的数量;
当窗⼝中每种字⺟的数量与字符串 p 中每种字⺟的数量相同时,则说明当前窗⼝为字符串 p 的异位词;
因此可以⽤两个⼤⼩为 26 的数组来模拟哈希表,⼀个来保存 s 中的⼦串每个字符出现的个 数,另⼀个来保存 p 中每⼀个字符出现的个数。这样就能判断两个串是否是异位词。
cpp
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
int len = p.size();
int hash1[26] = { 0 };
for (auto ch : p)
{
hash1[ch - 'a']++;
}
int len1 = len;
vector<int> vt;
int left = 0, right = 0;
int hash2[26] = { 0 };
while (right < s.size())
{
hash2[s[right] - 'a']++;//进窗口
if (right - left + 1 > len)//出窗口
{
hash2[s[left] - 'a']--;
left++;
}
if (right - left + 1 == len)//判断
{
int i = 0;
while (hash1[i++] == hash2[i])
{
if (i == 25)
{
vt.push_back(left);
break;
}
}
}
right++;
}
return vt;
}
};
小优化:
我们这里给一个count 来统计窗口当中有效字符个数。
cpp
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
int len = p.size();
int hash1[26] = { 0 };//统计字符串p中出现的字符个数
for (auto ch : p)
{
hash1[ch - 'a']++;
}
int count = 0;//
vector<int> vt;
int left = 0, right = 0;
int hash2[26] = { 0 };//统计窗口中每一个字符出现的个数
while (right < s.size())
{
hash2[s[right] - 'a']++;//进窗口
if(hash2[s[right]-'a']<=hash1[s[right]-'a'])
{
count++;
}
if (right - left + 1 > len)
{
if(hash2[s[left]-'a']<=hash1[s[left]-'a'])
{
count--;
}
hash2[s[left] - 'a']--;//出窗口
left++;
}
if (count==len)
{
vt.push_back(left);
}
right++;
}
return vt;
}
};
7.串联所有子串
算法思路:滑动窗口(同上题)
时间复杂度 O(N)
如果我们把每⼀个单词看成⼀个⼀个字⺟,问题就变成了找到「字符串中所有的字⺟异位词」。⽆ ⾮就是之前处理的对象是⼀个⼀个的字符,我们这⾥处理的对象是⼀个⼀个的单词。
代码:
cpp
class Solution {
public:
vector<int> findSubstring(string s, vector<string>& words) {
unordered_map<string,int> hash1;//保存word里面所有单词的频次
for(auto& s:words)
{
hash1[s]++;
}
vector<int> vt;
int len = words[0].size(),m = words.size();
for(int i = 0;i<len;i++)
{
unordered_map<string,int> hash2;//统计窗口内单词的频次
for(int left = i,right = i,count = 0;right+len<=s.size();right+=len)
{
//进窗口
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)
{
vt.push_back(left);
}
}
}
return vt;
}
};
8.最小覆盖字串
做题思路:
最开始想的是和之前一样用哈希表统计两个字符串中的字母数量,然后用滑动窗口在该区间内对比。这题我们依然可以发现left左移,right不用右移,所以自然可以想到用滑动窗口解题。
滑动窗口的套路都差不多,但是这里如果用count优化时,需要处理好count的数量,不像之前,我们的hash[out]<=hash[out],count--即可,因为这里我们出窗口的时候,窗口内的有效字母是可以重复的,需要特别注意一下。
附加代码:
cpp
#include <iostream>
#include <string>
#include <unordered_map>
class Solution {
public:
std::string minWindow(std::string s, std::string t) {
std::unordered_map<char, int> hash1;
// 存储目标字符串 t 中字符及其出现的次数
for (auto& c : t) {
hash1[c]++;
}
std::string str;
int len = t.size();
int minLen = INT_MAX, ret = 0;
std::unordered_map<char, int> hash2;
for (int left = 0, right = 0, count = 0; right < s.size(); right++) {
// 进窗口
char in = s[right];
hash2[in]++;
// 如果当前字符是目标字符串中的字符,并且其出现次数不超过目标字符串中的次数,增加计数
if (hash1[in]&&hash2[in] <= hash1[in]) {
count++;
}
// 判断是否满足包含 t 中的所有字符
while (count == len) {
ret = right - left + 1;
if (ret < minLen) {
minLen = ret;
str = s.substr(left, ret);
}
// 出窗口
char out = s[left];
hash2[out]--;
// 如果出窗口的字符是目标字符串中的字符,并且其出现次数小于目标字符串中的次数,减少计数
if (hash1[out]&&hash2[out] < hash1[out]) {
count--;
}
left++;
}
}
return str;
}
};
优化思路:
这里发现用count统计字母有效数量对比起来似乎有点麻烦,其实可以发现,我们只需要统计有效字母的种类。比如hash1中A的数量有3个,那么只有当hash2中A的数量等于3时,我们的count才会++,为什么不大于的时候也加呢,大于不也是有效的吗,大于的时候我们的count会多加的,比如在下图中这个例子中,大于等于的话,B会让count加2次,但这时count的数量是符合去判断的,但窗口里只有ABB,是不符合覆盖子串的,记住,我们统计的是种类,不是数量,那么出窗口的时候也是同理,在出窗口前,如果hash2[out]==hash1[out]时count才会--,我们这里的判断条件是count == hash.size();
cpp
class Solution {
public:
string minWindow(string s, string t) {
int hash1[128] = { 0 };
int hash2[128] = { 0 };
int kinds = 0;//计算hash1的大小
for (auto ch : t)
{
if (hash1[ch]++ == 0)
kinds++;
}
int minlen = INT_MAX, begin = -1;
for (int left = 0, right = 0, count = 0; right < s.size(); right++)
{ //count用来统计有效字符的种类,不是数量。
//进窗口
char in = s[right];
if (++hash2[in] == hash1[in])//维护count
{
count++;
}
while (count == kinds)//判断
{
if (right - left + 1 < minlen)//更新
{
minlen = right - left + 1;
begin = left;
}
char out = s[left];
if (hash2[out]-- == hash1[out])//出窗口
{
count--;
}
left++;
}
}
return begin == -1 ? "" : s.substr(begin, minlen);
}
};
int main() {
Solution sol;
std::string s = "ABBCA";
std::string t = "ABC";
std::string result = sol.minWindow(s, t);
std::cout << result << std::endl;
return 0;
}