leetcode刷题记录
一、滑动窗口
- 题目介绍
给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其总和大于等于 target 的长度最小的 子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
- 解题思路
由于是找最小子数组,便很容易想到如果排序后用最大的一部分数求和,便能得到最小数组的长度。备考408时快速排序写的十分熟练,这里便直接用上了。
相关代码段:
int partition(int *nums, int left, int right)
{
int mid = nums[left];
while (left < right)
{
while (nums[right] >= mid && left < right)
{
right--; // 确保每次划分右边的数都比此次确定位置的元素值大
}
nums[left] = nums[right];
while (nums[left] <= mid && left < right)
{
left++; // 确保每次划分左边的数都比此次确定位置的元素值小
}
nums[right] = nums[left];
}
nums[left] = mid;
return left; // 返回划分位置
}
void Quicksort(int *nums, int left, int right)
{
if (left >= right)
return;
int M = partition(nums, left, right);
Quicksort(nums, left, M - 1);//采用递归左右分别快排
Quicksort(nums, M + 1, right);
}
int minSubArrayLen(int target, int *nums, int numsSize)
{
int res = 0, sum = 0;
Quicksort(nums, 0, numsSize - 1);
for (int i = numsSize - 1; i >= 0; i--)
{
sum += nums[i];
res++;
if (sum >= target)
{
break;
}
}
if (sum < target)
return 0;
return res;
}
但在提交时,只通过了一半的测试,原因在于没有注意到题目中给出的示例:子数组 [numsl, numsl+1, …, numsr-1, numsr]子数组的元素必须在所给最初的数组中连续,而快排后改变了数组排序,可能使得原来相邻的元素分离。
再容易想到的就是用两个for循环暴力求解,但这种方法实际情况一般是无奈之举,这里并不考虑。
这里主要介绍滑动窗口的方法:滑动窗口的实质就是将暴力求解的两个for循环通过一个for循环来实现,原理是不再是像暴力求解中的枚举每种情况,而是在已符合的情况中通过移动左指针来寻找更小的子数组。
附上源码:
#include <stdint.h>
int minSubArrayLen(int target, int *nums, int numsSize)
{
int res = INT32_MAX;
int left = 0, sum = 0;
for (int right = 0; right < numsSize; right++)
{
sum += nums[right];
while (sum >= target)
{
res = res < (right - left + 1) ? res : right - left + 1;//满足条件记录一次数组长度
sum -= nums[left++];//左指针右移
}
}
return res == INT32_MAX ? 0 : res;//判断res值是否改变,若未改变,即未找到
}
- 相关题目
-904.水果成篮
最开始我把res更新语句(与right-left+1比大小)放到了while循环中,这是个逻辑错误,res的更新应该在满足条件的前提下,在最小子数组中,每次收缩时均需要sum>=target,此时更新是满足要求的,而该题中while循环条件是kind>2,循环体中不满足题目要求,不应该在此时更新res,调整后可以得到正确答案。
附上源码:
int totalFruit(int *fruits, int fruitsSize)
{
int left = 0, right = 0;
int trees[100000] = {0}; // 存放各种果树棵数
int kind = 0; // 记录果树种类
int res = 0;
for (; right < fruitsSize; right++)
{
trees[fruits[right]]++; // 该种果树棵数加一
if (trees[fruits[right]] == 1)
kind++; // 第一次加入,种类加一
while (kind > 2) // 种类大于2时收缩左边界
{
trees[fruits[left]]--; // 该种果树棵数减一
if (trees[fruits[left]] == 0)
kind--; // 不存在此类果树种类减一
left++;
}
res = res > right - left + 1 ? res : right - left + 1;
}
return res;
}
-76.最小覆盖子串
这个题卡了我好久,主要的问题是如何判断移动窗口中是否覆盖所给字符串。最开始想到的是暴力求解,但时间开销太大。在求解方法中看到了灵茶山艾府的方法,感觉不错。将涵盖理解为cntS 中的每个字母及其出现次数,如都大于等于 cntT 中的字母出现次数,让我眼前一亮。其余的部分在最小子数组的基础上稍加修改即可。
附上源码:
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
bool is_covered(int cnt_s[], int cnt_t[])
{
for (int i = 'A'; i <= 'Z'; i++)
{
if (cnt_s[i] < cnt_t[i])
{
return false;
}
}
for (int i = 'a'; i <= 'z'; i++)
{
if (cnt_s[i] < cnt_t[i])
{
return false;
}
}
return true;
}
char *minWindow(char *s, char *t)
{
int ans_left = -1, ans_right = INT32_MAX / 2;
int cnt_s[128] = {0}; // s 子串字母的出现次数
int cnt_t[128] = {0}; // t 中字母的出现次数
for (int i = 0; t[i]; i++)
{
cnt_t[t[i]]++; // 初始化待查询字符串中字符出现次数
}
int left = 0;
for (int right = 0; s[right]; right++)
{ // 移动子串右端点
cnt_s[s[right]]++; // 右端点字母移入子串
while (is_covered(cnt_s, cnt_t))
{ // 涵盖时才能够更新左右指针以及收缩左指针
if (right - left < ans_right - ans_left)
{ // 找到更短的子串
ans_left = left; // 记录此时的左右端点
ans_right = right;
}
cnt_s[s[left]]--; // 左端点字母移出子串
left++;
}
}
if (ans_left < 0)
{
return "";
}
s[ans_right + 1] = '\0';
return s + ans_left;
}
二、总结
滑动窗口的使用需要满足一种“单调性”,如数组越大越满足条件。基础格式为右指针逐渐右移,在满足所给题目的情况下,收缩数组,即左指针右移。要注意在满足条件的情况下更新返回值(更新语句放在while函数体内还是外)。而且在最小覆盖子串题目中,第一次见到For循环中拿字符串元素当循环条件(字符类型不为’\0’时执行循环体),以及返回值是指针+整型,收获颇多。