算法训练营 Day1-数组 2 | 长度最小的子数组 | 螺旋矩阵 II | 区间和 | 开发商购买土地
查阅文档地址:programmercarl.com/
本期题目地址:
本期题目答案地址:
本期同类题目地址:
- 904. 水果成篮 - 中等 - 力扣;
- 76. 最小覆盖子串 - 困难 - 力扣;
- 54.螺旋矩阵 - 中等 - 力扣
- LCR 146. 螺旋遍历二维数组 - 简单 - 力扣
- 开发商购买土地 - 卡网
目录:
- 基本概念(做题前要理解的概念)
- 我的解法
- 疑问点(过程中产生了问题并且查找资料解决)
语言
采用C++,一些分析也是用于 C++,请注意。
基本概念
- 子数组(Subarray) 是数组的一个片段,由数组中的连续元素组成。
- 滑动窗口 需要想清楚如何移动开始位置。和同向快慢指针一个道理。
- INT32_MAX 是一个常量,表示有符号 32 位整数类型(int32)能够表示的最大值。
209.长度最小的子数组 (容易出错)
做题过程
我最初的想法是暴力,两个 for 循环找所有区间,记录最短的 min 子数组个数,这个时间复杂度是 O(n^2)。
看了算法文档,get 滑动窗口新思路!~~倒着取区间(意思是遍历结束位置/快指针),不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果。
滑动窗口用一个 for 循环来完成区间搜索操作
- 循环的索引,一定是表示滑动窗口的终止位置,和快指针一个道理。
- 根据当前子序列和大小的情况,不断调节子序列的开始位置,类似调整慢指针。
- O(n^2) 暴力解法降为 O(n)。
我的代码 1
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int left = 0; // 滑动窗口的左边界
int sum = 0; // 当前窗口内元素的和
int len = INT32_MAX; // 初始化最短长度为最大值
for (int right = 0; right < nums.size(); right++) { // 右边界向右扩展
sum += nums[right]; // 将当前元素加入窗口
while (sum >= target) { // 如果当前窗口的和满足条件
len = min(len, right - left + 1); // 更新最短长度
sum -= nums[left++]; // 移动左边界,缩小窗口
}
}
return len == INT32_MAX ? 0 : len; // 如果未找到满足条件的子数组,返回 0
}
};
我的代码 2
// 使用同向的快慢指针去理解 acm 模式
#include <bits/stdc++.h>
using ll = long long;
using namespace std;
int minSubArrayLen(int target, vector<int>& nums);
signed main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int target;cin >> target;
int num;
vector<int> nums;
while(cin >> num) {
nums.push_back(num);
}
cin.clear();
cin.ignore(numeric_limits<streamsize>::max(), '\n');
cout << minSubArrayLen(target, nums) << endl;
}
int minSubArrayLen(int target, vector<int>& nums) {
int n = nums.size();
ll sum = 0;
int slow = 0;
int len = INT32_MAX;
for(int fast = 0; fast <= n - 1; fast ++) {
sum += nums[fast];
while(sum >= target) {
len = min(len, fast - slow + 1);
sum -= nums[slow ++]; // 下一个起始位置
}
}
return (len == INT_MAX? 0 : len);
}
疑问点
- **for 循环 + while 循环,为什么时间复杂度是 O(n)。**因为:虽然有嵌套的 for 和 while 循环,但每个元素最多被访问两次(一次加入窗口,一次移出窗口)。因此,总的操作次数是线性的,时间复杂度为 O(n)
904. 水果成篮
我的代码
// 力扣模式
class Solution {
public:
// 成员函数 totalFruit,接收一个整数向量 fruits 作为参数,返回一个整数
int totalFruit(std::vector<int>& fruits) {
// 获取水果数组的长度
int n = fruits.size();
// 如果数组长度小于等于 1,那么能采摘的最大水果数量就是数组长度本身
if(n <= 1) return n;
// 定义滑动窗口的左指针,初始化为 0
int slow = 0;
// 定义一个 map 容器 mp,用于存储每种水果及其出现的次数
// 这里使用 map 是因为它会自动按照键(水果种类)的升序排序,方便后续处理
std::map<int, int> mp;
// 定义一个变量 result 用于记录最大的水果采摘数量,初始化为 0
int result = 0;
// 右指针 fast 从 0 开始遍历数组
for(int fast = 0; fast <= n - 1; fast ++) {
// 将当前水果种类存入 map 中,并将其出现次数加 1
// 如果该水果种类之前不存在,会自动插入一个新的键值对,值初始化为 0 后再加 1
mp[fruits[fast]] ++;
// 当 map 中存储的水果种类超过 2 种时,需要调整窗口
while(mp.size() > 2) {
// 获取当前左指针指向的水果种类
int o = fruits[slow];
// 将该水果种类的出现次数减 1
mp[o] --;
// 如果该水果种类的出现次数变为 0,说明该种类的水果在当前窗口中已经没有了
// 则从 map 中删除该水果种类的记录
if(mp[o] == 0) {
mp.erase(o);
}
// 左指针右移,缩小窗口
slow ++;
}
// 计算当前窗口的长度,即当前可以采摘的水果数量
// fast - slow + 1 表示当前窗口内水果的数量
// 更新最大采摘数量
result = std::max(result, fast - slow + 1);
}
// 返回最大采摘数量
return result;
}
};
我的疑问点
- 要保持字典序的种类不超多 2,怎么实现?while 循环使用 size 函数和 erase 函数。mp.size() > 2; if(mp[o]) == 0 { mp.erase(o) };
- unordered_map:基于哈希表实现。它通过哈希函数将键映射到哈希表的特定位置,以此来存储和查找键值对。哈希函数的设计目的是尽可能均匀地将键分布到哈希表中,以减少哈希冲突。当发生哈希冲突(即不同的键映射到了同一个位置)时,通常会使用链表或红黑树等方法来处理冲突;map:基于红黑树(一种自平衡的二叉搜索树)实现。红黑树的每个节点存储一个键值对,并且满足左子树的所有节点键值小于根节点,右子树的所有节点键值大于根节点的特性,同时通过一些规则保证树的平衡,从而保证插入、查找和删除操作的时间复杂度为对数级别。unordered_map:在平均情况下,插入、查找和删除操作的时间复杂度为 O(1),最坏情况下(哈希冲突严重),时间复杂度会退化为 O(n)。map:插入、查找和删除操作的时间复杂度始终为 O(logn),这是由红黑树的性质决定的,无论数据量大小,操作时间都相对稳定。
- 如果 mp[o] 的值变为 0,此时这个键值对仍然存在于 mp 中。size() 也会对他进行统计
- 题解绝了
76. 最小覆盖子串(第一次见)
76. 最小覆盖子串 - 困难 - 力扣; 209 长度最小的子数组作为本题目的铺垫,都是[最短型]滑动窗口。
我的代码
class Solution {
public:
// 力扣模式
// 时间复杂度:O(n)
// 空间复杂度:O(k)
// 功能:在字符串 s 中找出包含字符串 t 所有字符的最小窗口子串
string minWindow(string s, string t) {
int n = s.length(), m = t.length();
// 若 s 长度小于 t,无法包含 t 所有字符,返回空串
if (n < m) return "";
unordered_map<char, int> cnt;
int less = 0;
// 统计 t 中字符出现次数,记录不同字符种类数
for (char c : t) {
if (cnt[c] == 0) ++less;
++cnt[c];
}
int nleft = -1, nright = n;
int left = 0;
// 右指针扩大窗口
for (int right = 0; right < n; ++right) {
char c = s[right];
--cnt[c];
// 若该字符数量满足 t 需求,缺少的字符种类数减 1
if (cnt[c] == 0) --less;
// 窗口包含 t 所有字符,尝试缩小窗口
while (less == 0) {
// 更新最小窗口端点
if (right - left < nright - nleft) {
nright = right;
nleft = left;
}
char o = s[left];
++cnt[o];
// 若移除字符后不满足需求,缺少的字符种类数加 1
if (cnt[o] > 0) ++less;
++left;
}
}
// 若未找到,返回空串,否则返回最小窗口子串
return nleft == -1 ? "" : s.substr(nleft, nright - nleft + 1);
}
};
我的疑惑点
- 法一:每次使用检测个数函数。一旦 t > s 就输出 false --> 统计个数还有个目的,看是否覆盖。
- 花费 O(∣Σ∣) 的时间去判断是否涵盖,能不能优化到 O(1) 呢?用一个变量 less 维护目前子串中有 less 种字母的出现次数小于 t 中字母的出现次数。
- 题目明确说明字符串 s 和 t 由英文字母组成,代码中仍将数组大小设为 128,主要是从代码实现的简洁性、通用性和便利性等多方面来考量
- 法二:在同一个计数数组里进行操作。法三:在两条计数数组外加一个 less 随时记录两者个数是否相等的情况。
- std::string 类的 substr 函数用于从原字符串中提取一个子字符串,其一般形式为:string.substr(pos, len),其中 pos 表示子字符串在原字符串中开始的位置(索引从 0 开始),len 表示子字符串的长度。
- 可以将 std::map 替换为 std::unordered_map,因为 std::unordered_map 的平均插入和查找操作的时间复杂度为 O(1),这样可以将整体时间复杂度优化到 O(n)。
59. 螺旋矩阵 II
思考
我的胡思乱想:用题目给的例子 n = 3 去模拟一遍。第一次做,第一想法是模拟递归,方向是右->下->左->上,拐弯(这样思考模拟下一个方向边界处理每一次发生变化)。
去看算法文档:关键是在转圈的逻辑,理解并使用二分搜索中提到的区间定义。
我的代码
// 力扣模式
// 时间复杂度:O(n^2)
// 空间复杂度:O(1)
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> nums(n, vector<int>(n, 0));
int nx = 0, ny = 0; // 起点
int cc = 1; // 层数
int cnt = 0; // 计数
// 循环
int x, y;
int t = n/2;
while(t --) {
for(y = ny; y < n - cc; y ++) {
nums[nx][y] = ++ cnt;
}
for(x = nx; x < n - cc; x ++) {
nums[x][y] = ++ cnt;
}
for(; y > ny; y --) {
nums[x][y] = ++ cnt;
}
for(; x > nx; x --) {
nums[x][ny] = ++ cnt;
}
// 更新起点
nx ++;
ny ++;
cc ++;
}
if(n % 2 == 1) {
int mid = n / 2;
nums[mid][mid] = n * n;
}
return nums;
}
};
我的疑惑
- 循环不变量原则:左闭右开的规则来处理。n 为偶数,完整圈数是 n / 2; n 为奇数。完整圈数是 n / 2,还要遍历正中心坐标位置。
- 几种常见的正确初始化二维 vector 的方式:
- vector<vector> nums(n, vector()); 初始化包含一个 n 个为空的 vector 的二维 vector。输出二维 vector 的行数 nums.size(); 输出列数 nums[i].size();
- vector<vector> nums(n, vector(m, 0)); 初始化一个 n 行 m 列的二维 vector,初始值都为 0;
- 为什么使用 while(cnt < n * n) 运行时间超时。
54.螺旋矩阵
我的代码
// 力扣模式
// 时间复杂度:O(n*m)
// 空间复杂度:O(1)
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
// 几行几列
int m = matrix.size();
int n = matrix[0].size();
vector<int> result(n * m, 0);
// 上下左右
int top = 0, bottom = m - 1;
int left = 0, right = n - 1;
int cnt = 0, row, col;
while (top <= bottom && left <= right) {
// 最上面一排
for (col = left; col <= right; col++) {
result[cnt++] = matrix[top][col];
}
top++;
// 最右边一列
if (top <= bottom) {
for (row = top; row <= bottom; row++) {
result[cnt++] = matrix[row][right];
}
}
right--;
// 最下面一排
if (top <= bottom && left <= right) {
for (col = right; col >= left; col--) {
result[cnt++] = matrix[bottom][col];
}
}
bottom--;
// 最左边一列
if (top <= bottom && left <= right) {
for (row = bottom; row >= top; row--) {
result[cnt++] = matrix[row][left];
}
}
left++;
}
return result;
}
};
我的疑惑
- 记录矩阵的行数 rows 和列数 cols。定义四个边界变量 left、right、top 和 bottom,分别表示矩阵的左、右、上、下边界。每次遍历完一条边后,相应的边界向内收缩。
- 依旧 while 循环条件没有想清楚,导致超时;while (left <= right && top <= bottom) 可以。能够自动适应 n != m 的情况。
- 我的 while 循环是整除 2 的倍数,不适应 n != m 的情况。
- 当矩阵的行数或列数较少时,例如只有一行或一列,在执行从右到左和从下到上的遍历操作时,可能会重复访问之前已经访问过的元素。比如当矩阵只有一行时,在执行完从左到右的遍历(第一排)之后,top 会加 1,此时 top 就会大于 bottom,但代码仍然会执行后续的从右到左以及从下到上的遍历,从而导致重复访问。
LCR 146. 螺旋遍历二维数组
我的代码
class Solution {
public:
vector<int> spiralArray(vector<vector<int>>& array) {
if (array.empty())
return {};
int rows = array.size();
int cols = array[0].size();
vector<int> result;
int left = 0, right = cols - 1, top = 0, bottom = rows - 1;
while (left <= right && top <= bottom) {
// 从左到右遍历上边界
for (int col = left; col <= right; ++col) {
result.push_back(array[top][col]);
}
top++;
// 从上到下遍历右边界
for (int row = top; row <= bottom; ++row) {
result.push_back(array[row][right]);
}
right--;
// 当 top <= bottom 时,从右到左遍历下边界
if (top <= bottom) {
for (int col = right; col >= left; --col) {
result.push_back(array[bottom][col]);
}
}
bottom--;
// 当 left <= right 时,从下到上遍历左边界
if (left <= right) {
for (int row = bottom; row >= top; --row) {
result.push_back(array[row][left]);
}
}
left++;
}
return result;
}
};
###我的疑惑点
-
if (array.empty()) return {};
区间和(前缀和)
一年前我接触的第一个算法技巧,也是面试题目。
在处理与数组相关的查询和更新操作时。为了高效解决这查询区间问题,通常会使用“前缀和”技术
第一次接触的小伙伴可以搜索相关资料,网上讲解资料很多。
我的代码
#include <bits/stdc++.h>
using namespace std;
signed main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int n; cin >> n;
vector<int> nums(n + 1, 0);
vector<int> pre(n + 1, 0);
// 从下标 1 开始存放
for(int i = 1; i <= n; i ++) {
int num;
cin >> num;
nums[i] = num;
// 前缀和预处理
pre[i] = pre[i - 1] + nums[i];
}
// 随机输入几个区间查询
int l, r; // 实际是 l+1,r+1
while(cin >> l >> r) {
int result = pre[r + 1] - pre[l];
cout << result << '\n';
}
}
开发商购买土地
- 上一题是前缀和 本题是差分;都是最典型的常见技巧
我的代码
#include <bits/stdc++.h>
using namespace std;
// n * m nums[] 权值
// 要求横向 或者 纵向 划分为 A B 两块;找到总权值差最小
int diff(vector<int> num_d, int total) {
int min_d = INT32_MAX;
int sum = 0;
for(int i = 0; i < num_d.size() - 1; i ++) {
sum = sum + num_d[i];
int t = total - sum;
int dd_d = abs(t - sum);
min_d = min(min_d, dd_d);
}
return min_d;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int n, m; cin >> n >> m; // n 行 m 列
vector<vector<int>> nums(n, vector<int>(m, 0)); // 二维数组
int total = 0; // 总权重
for(int i = 0; i < n; i ++) {
for(int j = 0; j < m; j ++) {
int num;
cin >> num;
total += num;
nums[i][j] = num;
}
}
// rows[] = 每一行的权重
vector<int> rows(n);
for(int i = 0; i < n; i ++) {
int sum = 0;
for(int j = 0; j < m; j ++) {
sum += nums[i][j];
}
rows[i] = sum;
}
// cols[] = 每一列的权重
vector<int> cols(m);
for(int j = 0; j < m; j ++) {
int sum = 0;
for(int i = 0; i < n; i ++) {
sum += nums[i][j];
}
cols[j] = sum;
}
// rows 和 cols 分别做差分;只要记录差分的最小值;
int rows_d = diff(rows, total);
int cols_d = diff(cols, total);
cout << (rows_d < cols_d ? rows_d : cols_d) << '\n';
}
#include <bits/stdc++.h>
using namespace std;
signed main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int n, m;
cin >> n >> m;
vector<vector<int>> vec(n+1, vector<int>(m+1, 0));
for(int i = 1; i <= n; i ++) {
for(int j = 1; j <= m; j ++) {
int num;cin >> num;
vec[i][j] = num + vec[i - 1][j] + vec[i][j - 1] - vec[i - 1][j - 1]; // 推理出前缀和公式
}
}
// 算差值
for(int i = 1; i < n; i++)
{
int x = abs(s[n][m] - 2 * s[i][m]);
ans = min(ans, x);
}
// 尝试纵向划分矩阵,i 表示划分的列
for(int i = 1; i < m; i++)
{
int x = abs(s[n][m] - 2 * s[n][i]);
ans = min(ans, x);
}
cout << ans;
}
总结
本期重点在于掌握滑动窗口和循环不变量原则,感受边界处理的重要性。通过优化算法,显著提升了问题解决效率。下期将继续深入学习更多算法知识