Leetcode子串-day4
记录自己刷力扣备战秋招的刷题笔记❤️
------wosz
子串
子串(Substring) 是指在一个字符串或数组中,连续的一段字符串或者数组。必须是一段连续的字符串或者字符数组,不能进行跳跃跨过某一个元素。

如图中描述一样,子串就是一段连续的序列。下面可以简单区分一下几个概念:
- 子串:连续的字符串序列,如上面的
a b c。 - 子序列:可以不连续,但是字符顺序不能变,如
a c r。 - 子数组:是连续的数组序列,和子串差不多就是将字符串换成了数组。
取子串方法
取子串的方法有几种:1.直接利用API来实现 2.利用滑动窗口来取子串
在C++中对于 string 取子串可以使用
cpp
s.substr(pos, len)
- pos:子串的起始位置
- len:子串的长度
对于子数组则可以使用
cpp
vector<int> sub(nums.begin() + left, nums.begin() + right + 1);
只需要知道这个的取值范围是左闭右开,即尾部要到无效的地方。比如:
cpp
vector<int> nums = {1,2,3,4,5};
int left = 1, right = 3;
vector<int> sub(nums.begin() + left, nums.begin() + right + 1);
则取出来的子数组是 {2,3,4}。
和为 K 的子数组
给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 。子数组是数组中元素的连续非空序列。
**输入:**nums = 1,1,1, k = 2
**输出:**2
自己的题解
cpp
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
int num_size=nums.size();
if(num_size==0) return 0;
unordered_map<int,int>map; //值,次数
map[0]=1; //添加默认未开始此时直接就是0,就是防止第一个坐标问题
int tag_count=0;
int cur_sum=0; //遍历的一个和
for(int i=0;i<num_size;i++)
{
cur_sum+=nums[i];
int tag_temp=cur_sum-k; //需要寻找的值
if(map.find(tag_temp)!=map.end()) //有满足的前缀和
{
tag_count+=map[tag_temp];
}
map[cur_sum]+=1; //当前的一个前缀入哈希表
}
return tag_count;
}
};
我的思路:我首先准备使用动态滑动窗口 来解决,因为这个涉及到一个和为 k 则我可以通过动态的滑动窗口,如果说窗口内的值大于 k 我就减小窗口让左边的窗口进行缩小;如果大于 k 我就让右边的窗口进行扩张;如果等于 k 的话就先对计数加加之后,感觉这里既可以缩小也可以扩大没啥区别影响。

但是后面我写出来发现思路还是有问题:只有在全是正数的情况下才可以使用滑动窗口 进行单调的移动控制增长和减少,此题有可能存在不是单调的情况即负数和零。
在AI大哥的提示下,它让我尝试去往 前缀和+哈希表 的方向去思考一下。
前缀和:
cpp
sum[r] - sum[l-1] = k
//变形
sum[l-1] = sum[r] - k
相当于就是在一段

对于公式变形理解就应该是我固定右边界,然后去前面找一段满足前缀和等于 sum[r] - k 的前缀值,就比如我要满足前缀和等于6,那么他们的前缀和终点坐标是不一样的。
之后利用哈希表去存储一下当前的一个前缀和,之后直接查找哈希表即可,判断哈希表的前缀和值就可以得到。
官方的题解
cpp
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
unordered_map<int, int> mp;
mp[0] = 1;
int count = 0, pre = 0;
for (auto& x:nums) {
pre += x;
if (mp.find(pre - k) != mp.end()) {
count += mp[pre - k];
}
mp[pre]++;
}
return count;
}
};
官方的思路也是通过 前缀和+哈希表 去进行寻找的, 只是官方的在遍历的时候比我们的简洁一点。
滑动窗口最大值
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。返回 滑动窗口中的最大值。
**输入:**nums = 1,3,-1,-3,5,3,6,7, k = 3
输出:3,3,5,5,6,7
解释:
滑动窗口的位置 最大值
1 3 -1 -3 5 3 6 7 3
1 3 -1 -3 5 3 6 7 3
1 3 -1 -3 5 3 6 7 5
1 3 -1 -3 5 3 6 7 5
1 3 -1 -3 5 3 6 7 6
1 3 -1 -3 5 3 6 7 7
自己的题解
cpp
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
int num_size=nums.size();
if(num_size==0||num_size<k) return {};
int windows_max=nums[0];
vector<int>tag;
for(int i=0;i<k;i++) //初始第一个窗口
{
if(nums[i]>windows_max)
{
windows_max=nums[i];
}
}
tag.push_back(windows_max);
int index=0;
while(index+k<num_size)
{
index++;
if(nums[index+k-1]>windows_max) //左移动
{
windows_max=nums[index+k-1]; //er:下标窗口-1
}
if(nums[index-1]==windows_max) //左边界已经被移动出去了
{
windows_max=nums[index]; //er:忘记重置窗口内最大值
//重新遍历查找
for(int i=index;i<index+k;i++)
{
if(nums[i]>windows_max)
{
windows_max=nums[i];
}
}
}
tag.push_back(windows_max);
}
return tag;
}
};
我的思路:首先这个题是子数组,但是更像是固定大小的滑动窗口 ,我的第一想法肯定就是直接滑动加一个数减一个数 来和当前窗口最大的值进行判断,和我们前面学习的滑动窗口一样来解决。但是有一个问题就是如果最大的值刚好在左边界该怎么处理,我的想法是如果被移出去的刚好是最大值的话,我就在窗口中再进行一次遍历找最大值。不是的话就可以直接移动。(哦不,超时了过了50/52个例程😂)。
刚请教了一下AI大哥,他建议我再添加一个 max_index 下标来标记位置,避免每一次都去重新扫一次,主要是我的移出判断太粗糙了。
cpp
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
int num_size=nums.size();
if(num_size==0||num_size<k) return {};
int windows_max=nums[0];
int max_index=0;
vector<int>tag;
for(int i=0;i<k;i++) //初始第一个窗口
{
if(nums[i]>=windows_max)
{
windows_max=nums[i];
max_index=i;
}
}
tag.push_back(windows_max);
int index=0;
while(index+k<num_size)
{
index++;
if(nums[index+k-1]>=windows_max) //左移动
{
windows_max=nums[index+k-1]; //er:下标窗口-1
max_index=index+k-1;
}
if(max_index<index) //左边界已经被移动出去了
{
windows_max=nums[index]; //er:忘记重置窗口内最大值
max_index=index;
//重新遍历查找
for(int i=index;i<index+k;i++)
{
if(nums[i]>windows_max)
{
windows_max=nums[i];
max_index=i;
}
}
}
tag.push_back(windows_max);
}
return tag;
}
};
就算目前我已经优化了我的我用下标来进行计算的方法,目前还是过不了这个目前过了51/52个,依旧超时;看来只能学习一下官方题解的思路来进行解题了。
官方题解
cpp
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
int n = nums.size();
deque<int> q;
for (int i = 0; i < k; ++i) {
while (!q.empty() && nums[i] >= nums[q.back()]) {
q.pop_back();
}
q.push_back(i);
}
vector<int> ans = {nums[q.front()]};
for (int i = k; i < n; ++i) {
while (!q.empty() && nums[i] >= nums[q.back()]) {
q.pop_back();
}
q.push_back(i);
while (q.front() <= i - k) {
q.pop_front();
}
ans.push_back(nums[q.front()]);
}
return ans;
}
};
官方的思路是采用单调队列,OK啊老铁那这里先去补一下单调队列学习一下吧。
补充:单调队列
单调队列故名思意就是:拥有单调性 的双端队列。

就像我画的图一样,单调队列主要涉及到了队首和队尾弹出:
- 队首弹出:队首弹出的情况,主要是后续进来的数大于等于原本单调队列的头(队首就是最大的值),此时就要清除前面的队列将队首的元素弹出。(过期)
- 队尾弹出:队尾的弹出情况,是因为进入到单调队列的元素大于此时队尾的元素,同时小于队首的值,此时不满足单调性就需要将队 尾元素进行弹出保证单调性。 (单调性)
单调队列涉及的操作包括了队首的弹出和队尾的弹出,此时普通的 queue 就不能满足了,必须使用双端队列。
双端队列
其实和普通队列最主要的差别就是
- 普通队列只能头出尾进
- 双端队列头能出,尾也能出,进也可以头进、尾进
双端队列的使用
其实使用和普通队列都差不多,只是多了头进和尾进的操作。
双端队列的声明
cpp
#include <deque>
using namespace std;
deque<int> dq;
队尾插入
cpp
dq.push_back(x);
队头插入
cpp
dq.push_front(x);
队尾删除
cpp
dq.pop_back();
队头删除
cpp
dq.pop_front();
访问队头元素
cpp
dq.front();
访问队尾元素
cpp
dq.back();
判断队列是否为空
cpp
dq.empty();
获取队列大小
cpp
dq.size();
之后自己再做了一次滑动窗口最大值
cpp
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
int num_size = nums.size();
if (num_size == 0 || num_size < k) return {};
vector<int> tag;
deque<int> dq; // 存下标
// 初始化第一个窗口
for (int i = 0; i < k; i++) {
while (!dq.empty() && nums[dq.back()] <= nums[i]) {
dq.pop_back();
}
dq.push_back(i);
}
tag.push_back(nums[dq.front()]);
// 滑动窗口
for (int i = k; i < num_size; i++) {
// 1. 删除过期元素
if (!dq.empty() && dq.front() <= i - k) {
dq.pop_front();
}
// 2. 维护单调递减
while (!dq.empty() && nums[dq.back()] <= nums[i]) {
dq.pop_back();
}
// 3. 当前元素入队
dq.push_back(i);
// 4. 队头就是当前窗口最大值
tag.push_back(nums[dq.front()]);
}
return tag;
}
};
最小覆盖子串
给定两个字符串 s 和 t,长度分别是 m 和 n,返回 s 中的 最短窗口子串 ,使得该子串包含 t 中的每一个字符(包括重复字符 )。如果没有这样的子串,返回空字符串 ""。测试用例保证答案唯一。
**输入:**s = "ADOBECODEBANC", t = "ABC"
输出: "BANC"
**解释:**最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
自己题解
cpp
class Solution {
public:
string minWindow(string s, string t) {
int s_size = s.size();
int t_size = t.size();
if (s_size < t_size) return "";
if (s == t) return s;
vector<int> win_pc(128, 0);
vector<int> t_pc(128, 0);
//初始统计频次
for (int i = 0; i < t_size; i++) {
t_pc[t[i]]++;
}
int left = 0, right = 0;
int count = 0; //匹配上的字符
int min_len = INT_MAX;
int start = 0;
while (right < s_size) {
// 入频次
win_pc[s[right]]++;
// 这次加入是否是有效匹配
if (t_pc[s[right]] > 0 && win_pc[s[right]] <= t_pc[s[right]]) {
count++;
}
// 满足则进行缩小
while (count == t_size) {
if (right - left + 1 < min_len) {
min_len = right - left + 1;
start = left;
}
// 左边字符移出是否会破坏覆盖
if (t_pc[s[left]] > 0 && win_pc[s[left]] <= t_pc[s[left]]) {
count--;
}
win_pc[s[left]]--;
left++;
}
right++;
}
return min_len == INT_MAX ? "" : s.substr(start, min_len);
}
};
我的思路:我想了一下我准备利用滑动窗口 加频次比较 的方法来做,我的窗口右边往外扩 然后将这个写入窗口的频次表 中,然后对比 t 中子串的每一个字符的频次都去等于我的窗口中该字符的频次则说明此为解,此时我们就开始左边界滑动同时将窗口内的频次进行剔除 ,就是我们找到最小的符合的子串。
官方的题解
cpp
class Solution {
public:
unordered_map <char, int> ori, cnt;
bool check() {
for (const auto &p: ori) {
if (cnt[p.first] < p.second) {
return false;
}
}
return true;
}
string minWindow(string s, string t) {
for (const auto &c: t) {
++ori[c];
}
int l = 0, r = -1;
int len = INT_MAX, ansL = -1, ansR = -1;
while (r < int(s.size())) {
if (ori.find(s[++r]) != ori.end()) {
++cnt[s[r]];
}
while (check() && l <= r) {
if (r - l + 1 < len) {
len = r - l + 1;
ansL = l;
}
if (ori.find(s[l]) != ori.end()) {
--cnt[s[l]];
}
++l;
}
}
return ansL == -1 ? string() : s.substr(ansL, len);
}
};
先用一个哈希表 ori统计字符串 t 中每个字符需要出现的次数,再用另一个哈希表 cnt 记录当前窗口内这些目标字符出现的次数。然后用两个指针维护一个窗口,右边界不断往外扩,每加入一个字符时,如果这个字符本身是 t 中需要的字符,就把它加入窗口的频次表中。每次扩张之后,就通过一个 check() 函数去比较 ori 和 cnt,只要 t 中所有字符在当前窗口中的出现次数都大于等于需求次数,就说明当前窗
口已经覆盖了 t,此时这个窗口就是一个可行解。接着为了找到最小的符合条件的子串,就开始让左边界滑动,同时把移出窗口的字符从频次表中剔除;如果左边移走的字符是 t 需要的字符,就在 cnt 中减一。只要窗口还满足覆盖条件,就继续缩小,并不断更新最短答案;一旦缩小后不再满足覆盖条件,就停止收缩,再继续让右边界向右扩张。整个过程就是不断执行"右边扩张找到可行解,左边收缩找到最优解",最终得到最短覆盖子串。