鸽了很多天(跪),今天也算是跌跌撞撞重新开始了,一共两道题,难度都是★★★。
在参加了传智杯省赛之后,发现自己以前对于题目的记录方式有误。如果只专注于最后通过所有测试用例的代码,后期回顾的时候就只能走马观花,而对于为什么没做出一道题以及怎么解决的不会有一点印象,所以之后会记录下整个做题过程,以便后续回顾总结内化。
一.棒球比赛 ★★★☆☆
题目
你现在是一场采用特殊赛制棒球比赛的记录员。这场比赛由若干回合组成,过去几回合的得分可能会影响以后几回合的得分。
比赛开始时,记录是空白的。你会得到一个记录操作的字符串列表 ops,其中 ops[i] 是你需要记录的第 i 项操作,ops 遵循下述规则:
- 整数
x- 表示本回合新获得分数x "+"- 表示本回合新获得的得分是前两次得分的总和。题目数据保证记录此操作时前面总是存在两个有效的分数。"D"- 表示本回合新获得的得分是前一次得分的两倍。题目数据保证记录此操作时前面总是存在一个有效的分数。"C"- 表示前一次得分无效,将其从记录中移除。题目数据保证记录此操作时前面总是存在一个有效的分数。
请你返回记录中所有得分的总和
思路
1.++利用两个变量++pre1、pre2分别表示前一回合得分和前前一回合得分;res记录得分总和
2.遍历字符串数组中的字符串,根据题意针对不同情况对res做不同处理
tip:用++stoi函数++将字符串直接变为整数
代码1
cpp
class Solution {
public:
int calPoints(vector<string>& operations) {
//用两个变量分别表示前一个数字和前一个数字的前一个数字
int pre1=0,pre2=0,res=0;
for(string str:operations){
if(str=="+"){
res+=(pre1+pre2);
}else if(str=="D"){
res+=(pre1*2);
}else if(str=="C"){
res-=pre1;
}else{
//将字符串变为整数
int cur=stoi(str);
res+=cur;
pre2=pre1;
pre1=cur;
}
}
return res;
}
};
错误
没有在字符串为+、D、C时及时修改pre1和pre2
代码2
在字符串为+、D、C时及时修改pre1和pre2;同时,增加了一个变量pre3记录前前前一回合得分,因为在字符串为C时,要移除前一次得分,pre1和pre2也要对应改变,此时可以使得pre2=pre3,实现变化
cpp
class Solution {
public:
int calPoints(vector<string>& operations) {
// 用两个变量分别表示前一个数字和前一个数字的前一个数字
int pre1 = 0, pre2 = 0, pre3 = 0, res = 0;
for (string str : operations) {
int cur;
if (str == "+") {
cur = (pre1 + pre2);
res += cur;
pre3 = pre2;
pre2 = pre1;
pre1 = cur;
} else if (str == "D") {
cur = pre1 * 2;
res += cur;
pre3 = pre2;
pre2 = pre1;
pre1 = cur;
} else if (str == "C") {
cur = pre1;
res -= cur;
pre1 = pre2;
pre2 = pre3;
} else {
// 将字符串变为整数
cur = stoi(str);
res += cur;
pre3 = pre2;
pre2 = pre1;
pre1 = cur;
}
}
return res;
}
};
错误
C 操作中 pre3 未同步回滚,仅能处理单轮 C,无法应对连续 C 或复杂场景
++如果只靠一般变量,无法应对复杂场景,所以需要换方法。++
代码3
改用**++栈++**的方式记录之前所有的操作数字
根据ops 的值进行不同的操作:
"+" :res加上栈顶的前两个元素之和,并且把两个元素之和加入栈。
"D" :res加上栈顶元素的两倍,并将栈顶元素的两倍加入栈。
"C" :res减去栈顶元素,同时将栈顶元素移出栈。
整数 x:res直接加上x,并且将x入栈,作为下一个数的前一次得分
PS:因为题目数据确保分数有效,所以无需考虑栈中元素是否合法;操作符是整数x比较难以确定,所以可以放到if语句最后的else语句中。
cpp
class Solution {
public:
int calPoints(vector<string>& operations) {
int res = 0;
stack<string> s;
for (auto op : operations) {
if (op == "+") {
string a = s.top();
s.pop();
int cur = stoi(a) + stoi(s.top());
res += cur;
s.push(a);
s.push(to_string(cur));
} else if (op == "D") {
int cur = 2 * (stoi(s.top()));
res += cur;
s.push(to_string(cur));
} else if (op == "C") {
res -= stoi(s.top());
s.pop();
} else {
res += stoi(op);
s.push(op);
}
}
return res;
}
};
复杂度
N为数组operation的长度
时间复杂度:O(N)。遍历数组,循环N次。
空间复杂度:O(N)。栈的空间开销O(N)+常数常数级临时变量空间O(1),所以整体的空间复杂度为O(N)。
栈的复杂度
++栈的核心操作(push()、pop()、top())都是 常数时间操作 O (1)++ ,无论栈中有多少元素,这些操作的执行时间固定;最坏情况下,operations 数组中无 "C" 操作,此时每个操作都会向栈中插入一个元素,栈的元素个数最多为 N 个,占用 O (N) 空间。
官方题解
官方利用动态数组代替了栈的操作,但目的是一样的:保存数值用于后续操作,但是代码可见得更加简洁了,少了stoi和to_string的重复操作。
其他差异和优点:
1.使用switch(op[0])-case代替if:代码更加清晰简洁
2.循环采用 ++auto &op引用传递++:避免字符串拷贝,尤其在操作数较多时,效率优势明显
代码
cpp
class Solution {
public:
int calPoints(vector<string>& operations) {
int res = 0;
vector<int> points;
for (auto& op : operations) {
int n = points.size();
switch (op[0]) {
case '+':
res += points[n - 1] + points[n - 2];
points.push_back(points[n - 1] + points[n - 2]);
break;
case 'D':
res += 2 * points[n - 1];
points.push_back(2 * points[n - 1]);
break;
case 'C':
res -= points[n - 1];
points.pop_back();
break;
default:
res += stoi(op);
points.push_back(stoi(op));
break;
}
}
return res;
}
};
复杂度
N为数组operation的长度
时间复杂度:O(N)。遍历数组,循环N次。
空间复杂度:O(N)。动态数组最多保存N个元素。
二.数组的度 ★★★☆☆
题目
697. 数组的度 给定一个非空且只包含非负数的整数数组 nums,数组的 度 的定义是指数组里任一元素出现频数的最大值。
你的任务是在 nums 中找到与 nums 拥有相同大小的度的最短连续子数组,返回其长度。
思路
1.求得原数组nums的度degree:遍历数组,用哈希表记录不同数字出现次数,degree始终记录最大的次数,遍历结束后,degree的值就是原数组nums的度
2.寻找度为degree的最短连续子数组:
2.1 子数组的长度从curLen到nums.size():因为度为degree的子数组,长度最短为degree,所以令curLen=degree
2.2 用两个索引start和end表示子数组的头尾,遍历长度为curLen的子数组,用哈希表map2记录各个元素的次数,只要出现一个元素的出现次数为degree,直接返回当前子数组长度curLen;若没有,将start和end向后移动继续遍历后续的子数组
2.3 改变子数组的长度,即使curLen++,直至找到最终的子数组。
代码1
cpp
class Solution {
public:
int findShortestSubArray(vector<int>& nums) {
unordered_map<int, int> map;
int degree = 0; // 数组的度
for (auto& num : nums) {
map[num]++;
degree = max(degree, map[num]);
}
int curLen = degree;
while (curLen <= nums.size()) {
int start = 0;
int end = curLen - 1;
unordered_map<int, int> map2;
while (end < nums.size() && start <= end) {
for (int i = start; i <= end; i++) {
map2[nums[i]]++;
if (map2[nums[i]] == degree) {
return curLen;
}
}
start++;
end++;
}
curLen++;
}
return curLen;
}
};
我的疑问
遇到map2[nums[i]] == degree时,curLen会不会并不是当前满足的最短连续子数组的长度?
答案是:不会。因为当map2[nums[i]] == degree,curLen最短就等于degree,即子数组的元素都是nums[i],这时已经会直接返回curLen了,不会再去遍历包含当前数组的其他更长的子数组。
错误原因
++哈希表map2没有在每一个子数组置零++,上一个子数组的数据存留到当前子数组中,所以map[nums[i]]==degree时的子数组长度并不一定是符合要求的子数组长度。所以需要将哈希表map2的定义移动到for循环中,保证每个子数组的map2都是新的哈希表。
代码2
修改后的代码
cpp
class Solution {
public:
int findShortestSubArray(vector<int>& nums) {
unordered_map<int, int> map;
int degree = 0; // 数组的度
for (auto& num : nums) {
map[num]++;
degree = max(degree, map[num]);
}
int curLen = degree;
while (curLen <= nums.size()) {
int start = 0;
int end = curLen - 1;
while (end < nums.size() && start <= end) {
unordered_map<int, int> map2;
for (int i = start; i <= end; i++) {
map2[nums[i]]++;
if (map2[nums[i]] == degree) {
return curLen;
}
}
start++;
end++;
}
curLen++;
}
return curLen;
}
};
错误原因
超出时间限制,因为有三层循环,第三层找子数组存在大量的重复遍历,所以可以++使用滑动窗口减少遍历次数++。
代码3
对于长度为curLen的子数组只创建一次哈希表map2,先将从0索引开始的子数组的元素及其出现次数装入哈希表map2,然后利用start和end对子数组进行移动,同时改变对应元素的出现次数
cpp
class Solution {
public:
int findShortestSubArray(vector<int>& nums) {
unordered_map<int, int> map;
int degree = 0; // 数组的度
for (auto& num : nums) {
map[num]++;
degree = max(degree, map[num]);
}
int curLen = degree;
while (curLen <= nums.size()) {
int start = 0;
int end = curLen - 1;
// 从0索引开始的子数组
unordered_map<int, int> map2;
for (int i = start; i <= end; i++) {
map2[nums[i]]++;
//判断是否度为degree
if (map2[nums[i]] == degree) {
return curLen;
}
}
//下一个子数组
start++;
end++;
while (end < nums.size() - 1) {
map2[nums[start]]--; // 减去前一个子数组的第一个元素
map2[nums[end + 1]]++; // 加上当前的元素
if (map2[nums[end + 1]] == degree) {
return curLen;
}
start++;
end++;
}
curLen++;
}
return curLen;
}
};
错误原因
start索引错误,导致++删除的不是前一个窗口的左界++:第一个窗口的start=0,进行start++后,start=1,删除的不是nums[0]了。end++也导致添加的不是对应窗口的右界。
代码4
将中间的start++和end++删去即可
cpp
class Solution {
public:
int findShortestSubArray(vector<int>& nums) {
unordered_map<int, int> map;
int degree = 0; // 数组的度
for (auto& num : nums) {
map[num]++;
degree = max(degree, map[num]);
}
int curLen = degree;
while (curLen <= nums.size()) {
int start = 0;
int end = curLen - 1;
// 从0索引开始的子数组
unordered_map<int, int> map2;
for (int i = start; i <= end; i++) {
map2[nums[i]]++;
//判断是否度为degree
if (map2[nums[i]] == degree) {
return curLen;
}
}
while (end < nums.size() - 1) {
map2[nums[start]]--; // 减去前一个子数组的第一个元素
map2[nums[end + 1]]++; // 加上当前的元素
if (map2[nums[end + 1]] == degree) {
return curLen;
}
start++;
end++;
}
curLen++;
}
return curLen;
}
};
错误原因
1.滑动窗口增量更新时,删除 / 添加元素的目标错误(计数失真)
2.后续窗口循环条件错误 end < nums.size() - 1(漏判最后一个窗口)
代码5
cpp
class Solution {
public:
int findShortestSubArray(vector<int>& nums) {
int n=nums.size();
unordered_map<int, int> map;
int degree = 0; // 数组的度
for (auto& num : nums) {
map[num]++;
degree = max(degree, map[num]);
}
int curLen = degree;
while (curLen <= n) {
int start = 0;
int end = curLen - 1;
// 从0索引开始的子数组
unordered_map<int, int> map2;
for (int i = start; i <= end; i++) {
map2[nums[i]]++;
// 判断是否度为degree
if (map2[nums[i]] == degree) {
return curLen;
}
}
while (end < n) {
start++;
end++;
// 若滑动后右边界超出数组,终止循环
if (end >= n) {
break;
}
// 删去前一个窗口的左边界(start-1 是前一个窗口左边界)
int leftVal = nums[start - 1];
map2[leftVal]--;
// 计数为0时删除键(可选,优化空间,不影响逻辑)
if (map2[leftVal] == 0) {
map2.erase(leftVal);
}
// 添加当前窗口的右边界(end 是当前窗口右边界)
int rightVal = nums[end];
map2[rightVal]++;
if(map2[rightVal]==degree){
return curLen;
}
}
curLen++;
}
return curLen;
}
};
复杂度
时间复杂度:O (n×k)(最坏 O (n²),k 为枚举的长度数)
空间复杂度:O (m)(两个哈希表,空间略多),m 为数组中不同元素的个数
官方题解
使用哈希表实现功能,每一个数映射到一个长度为 3 的数组,数组中的三个元素分别代表这个数出现的次数、这个数在原数组中第一次出现的位置和这个数在原数组中最后一次出现的位置。当我们记录完所有信息后,我们需要遍历该哈希表,找到元素出现次数最多,且前后位置差最小的数。
cpp
class Solution {
public:
int findShortestSubArray(vector<int>& nums) {
unordered_map<int, vector<int>> mp;
int n = nums.size();
for (int i = 0; i < n; i++) {
if (mp.count(nums[i])) {
mp[nums[i]][0]++;
mp[nums[i]][2] = i;
} else {
mp[nums[i]] = {1, i, i};
}
}
int maxNum = 0, minLen = 0;
for (auto& [_, vec] : mp) {
if (maxNum < vec[0]) {
maxNum = vec[0];
minLen = vec[2] - vec[1] + 1;
} else if (maxNum == vec[0]) {
if (minLen > vec[2] - vec[1] + 1) {
minLen = vec[2] - vec[1] + 1;
}
}
}
return minLen;
}
};
复杂度
时间复杂度:O(n),其中 n 是原数组的长度,我们需要遍历原数组和哈希表各一次,它们的大小均为 O(n)。
空间复杂度:O(n),其中 n 是原数组的长度,最坏情况下,哈希表和原数组等大。