数组 滑动窗口
209.长度最小的子数组●●
给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, …, numsr-1, numsr],并返回其长度。
如果不存在符合条件的子数组,返回 0 。
1、暴力解法
逐个寻找以 n u m s [ i ] nums[i] nums[i] ( i ++ ) 开头 且满足条件的数组,比较长度。
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int n = nums.size();
int ans = n + 1;
for (int i = 0; i < n; i++){
int sum = nums[i];
if (sum >= target){ // 存在大于或等于 target 的数字,则得到最小长度为 1
return 1;
}
for (int j = i+1; j < n; j++){ // 逐个寻找以 nums[i] 开头 且满足条件的数组
sum += nums[j];
if (sum >= target){
if (j-i+1 < ans){ // 取更小长度
ans = j-i+1;
}
break;
}
}
}
if (ans > n){ // 未找到满足条件的数组
ans = 0;
}
return ans;
}
};
- 时间复杂度: O ( n 2 ) O(n^2) O(n2)
- 空间复杂度: O ( 1 ) O(1) O(1)
2、滑动窗口
滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出结果。
在本题中实现滑动窗口,主要确定如下三点:
- 窗口内是什么?
- 如何移动窗口的起始位置?
- 如何移动窗口的结束位置?
窗口就是 满足其和 ≥ s 的长度最小的 连续 子数组。
窗口的起始位置如何移动:如果当前窗口的值大于s了,窗口就要向前移动了(也就是该缩小了)。
窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,窗口的起始位置设置为数组的起始位置就可以了。
- 时间复杂度: O ( n ) O(n) O(n) :看每一个元素被操作的次数,每个元素在滑动窗后进来操作一次,出去操作一次,每个元素都是被被操作两次,所以时间复杂度是 2 × n 也就是 O ( n ) O(n) O(n)。
- 空间复杂度: O ( 1 ) O(1) O(1)
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int left = 0;
int sum = 0;
int n = nums.size();
int ans = n+1;
for(int right = 0; right < n; right++){
sum += nums[right];
while (sum >= target){
if (right-left+1 < ans){
ans = right-left+1;
}
sum -= nums[left++];
}
}
if (ans > n){
ans = 0;
}
return ans;
}
};
904. 水果成篮●●
最多含有两个不同字符的最长子串
思路:(双指针(或者说是 滑动窗口))
右指针遍历数组,判断当前位置是否符合采摘条件,如符合则水果个数加1;如不符合则直接更新当前右指针结尾时的两种水果种类(即 right 与 right -1 ),然后将左指针移到 right - 1 处并往前遍历,进行采摘判断;直到右指针遍历完成。
class Solution {
public:
int totalFruit(vector<int>& fruits) {
int n = fruits.size();
if (n<3) return n; // 少于3直接返回树的棵树
int left = 0;
int ans = 2; // 最少 2 个水果
int a = fruits[0], b; // a,b存放当前的两种水果
for(int right = 0; right < n; right++){ // 右指针遍历
if (fruits[right] == a || fruits[right] == b){
if(right-left+1 > ans) ans = right-left+1; // 如果当前位置的水果符合采摘条件,则直接比较水果个数
}
else{ // fruits[right]为第三种水果,不符合采摘条件
a = fruits[right-1]; // 更新当前位置的水果种类,即为 right 与 right -1 位置的水果
b = fruits[right];
left = right - 1; // 将左指针移到 right - 1 处,并往前遍历判断
while (left >= 1 && (fruits[left-1] == a || fruits[left-1] == b)){
left--; // 当左指针前一位符合采摘条件,则左移一位并比较水果个数
if(right-left+1 > ans) ans = right-left+1;
}
}
} // 直到右指针遍历完成
return ans;
}
};
76. 最小覆盖子串●●●
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。
注意:
对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
如果 s 中存在这样的子串,我们保证它是唯一的答案。
输入:s = “ADOBECODEBANC”, t = “ABC”
输出:“BANC”
数组 exit 记录 t 中存在的字母,
数组 cnts 表示当前字母的剩余覆盖数量(负数是表示窗口内该字母冗余),
整数 rest 记录当前剩余覆盖的字母数(>= 0)。
class Solution {
public:
string minWindow(string s, string t) {
int sl = s.length(), tl = t.length();
if(sl < tl) return "";
int rest = tl;
int cnts[129] = {0}; // 记录字母个数
bool exit[129] = {0}; // 记录字母是否存在
for(char ch : t){
++cnts[ch]; // 统计字符串 t 中的字母
exit[ch] = true;
}
int minLen = sl + 1; // 最小长度
int start = -1; // 对应的子串开头
int left = 0; // 左指针
for(int right = 0; right < sl; ++right){
if(exit[s[right]]){ // 如果当前字母存在 t 中,那么维护cnts数组,和剩余覆盖的字符数量 rest
--cnts[s[right]];
if(cnts[s[right]] >= 0)--rest; // 排除某字母cnts为负数的情况
}
while(rest == 0){ // 成功覆盖的情况,移动左指针
if(minLen > right-left+1){ // 判断最小长度,更新子串开头
minLen = right-left+1;
start = left;
}
if(exit[s[left]]){ // 对移出窗口的左指针字母进行统计
++cnts[s[left]];
if(cnts[s[left]] > 0) ++rest;
}
++left;
}
}
if(start == -1) return "";
return s.substr(start, minLen); // 返回子串
}
};
模拟
59. 螺旋矩阵 II ●●
给你一个正整数 n ,生成一个包含 1 到 n 2 n^2 n2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix .
1、一圈一圈循环填充
坚持循环不变量原则,模拟顺时针画矩阵的过程:
- 填充上行从左到右
- 填充右列从上到下
- 填充下行从右到左
- 填充左列从下到上
按照固定规则来遍历,每画一条边都要坚持一致的左闭右开,或者左开右闭的原则,这样这一圈才能按照统一的规则画下来。
按照左闭右开的原则画一圈:
- 时间复杂度: O ( n 2 ) O(n^2) O(n2)
- 空间复杂度:O(1)
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> ans(n,vector<int>(n,0));// 二维矩阵
int times = n / 2; //循环的圈数
int value = 1; // 赋值
for(int i = 0; i < times; i++){ // 第 i 圈模拟,【左闭右开,注意边界条件】,起点即为(i, i)
for(int j = i; j < n-i-1; j++){ // 上行 从左到右
ans[i][j] = value++;
}
for(int j = i; j < n-i-1; j++){ // 右列 从上到下
ans[j][n-i-1] = value++;
}
for(int j = n-i-1; j > i; j--){ //下行 从右到左
ans[n-i-1][j] = value++;
}
for(int j= n-i-1; j > i; j--){ // 左列 从下到上
ans[j][i] = value++;
}
}
if(n%2>0) ans[n/2][n/2] = n*n; // 奇数时 中点赋值
return ans;
}
};
2、四条边顺序填充、移动边界、判断总数
- 生成一个 n×n 空矩阵 ans,随后模拟整个向内环绕的填入过程:
- 定义当前左右上下边界 l,r,t,b,初始值 num = 1,迭代终止值 numsize = n * n;
- 当 num <= numsize 时,始终按照 从左到右 从上到下 从右到左 从下到上 填入顺序循环,每次填入后:
- 执行 num += 1:得到下一个需要填入的数字;
- 更新边界:例如从左到右填完后,上边界 t += 1,相当于上边界向内缩 1。
- 使用 num <= numsize 而不是 l < r || t < b 作为迭代条件,是为了解决当 n 为奇数时,矩阵中心数字无法在迭代过程中被填充的问题。
- 最终返回 ans 即可。
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
int top = 0, right = n-1, left = 0, buttom = n-1;
int numsize = n*n;
int num = 1;
vector<vector<int>> ans(n, vector<int>(n));
while(true){
for(int i = left; i <= right; i++){
ans[top][i] = num++;
}
top++;
if(num>numsize) break; // 填充 判断总数
for(int i = top; i <= buttom; i++){
ans[i][right] = num++;
}
right--;
if(num>numsize) break;
for(int i = right; i >= left; i--){
ans[buttom][i] = num++;
}
buttom--;
if(num>numsize) break;
for(int i = buttom; i >= top; i--){
ans[i][left] = num++;
}
left++;
if(num>numsize) break;
}
return ans;
}
};
54. 螺旋矩阵 ●●
给你一个 m 行 n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。
四条边按顺序遍历,移动、判断边界。
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
int m = matrix.size(); // m 行
int n = matrix[0].size(); // n 列
int top = 0, right = n-1, left = 0, buttom = m-1; //边界索引值
int numsize = m*n;
int num = 0;
vector<int> ans(numsize);
while(true){
for(int i = left; i <= right; i++){
ans[num++] = matrix[top][i];
}
top++;
if(top>buttom) break; // 遍历 判断边界
for(int i = top; i <= buttom; i++){
ans[num++] = matrix[i][right];
}
right--;
if(left>right) break;
for(int i = right; i >= left; i--){
ans[num++] = matrix[buttom][i];
}
buttom--;
if(top>buttom) break;
for(int i = buttom; i >= top; i--){
ans[num++] = matrix[i][left];
}
left++;
if(left>right) break;
}
return ans;
}
};
3. 无重复字符的最长子串 ●●
- 暴力:
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int left = 0;
int right = 0;
int ans = 0;
int n = s.length();
int i;
while(right < n){
for(i = left; i < right; i++){ // 循环遍历判断当前子串是否存在重复字符
if(s[right] == s[i]){ // 可用哈希集合来判断,从而减少时间复杂度
left = i + 1;
break;
}
}
if(i >= right - 1){
ans = max((right - left + 1), ans);
right++;
}
}
return ans;
}
};
- 哈希集合判断是否重复,减少时间复杂度
- 时间复杂度:O(N),其中 N 是字符串的长度。左指针和右指针分别会遍历整个字符串一次。
- 空间复杂度:O(∣Σ∣),其中 Σ 表示字符集(即字符串中可以出现的字符),∣Σ∣ 表示字符集的大小。在本题中没有明确说明字符集,因此可以默认为所有 ASCII 码在 [0,128) 内的字符,即 ∣Σ∣=128。
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int left = 0;
int right = -1; // 确保right从零遍历
int ans = 0;
int n = s.length();
unordered_set<char> occ;
int i;
while(right < n-1){
if (occ.count(s[right+1])){ // 右移会出现重复字符
occ.erase(s[left]); // 哈希集合中移除left
left++; // left 右移
}
else{
right++; // 右移不会出现重复字符
occ.insert(s[right]);
ans = max((right - left + 1), ans);
}
}
return ans;
}
};
- 哈希数组
class Solution {
public:
int lengthOfLongestSubstring(string s) {
bool cnts[128] = {false};
int ret = 0, left = 0;
for(int right = 0; right < s.length(); ++right){
while(cnts[s[right]] == true){
cnts[s[left++]] = false;
}
cnts[s[right]] = true;
ret = max(ret, right-left+1);
}
return ret;
}
};