150. 逆波兰表达式求值
给你一个字符串数组
tokens,表示一个根据 逆波兰表示法 表示的算术表达式。请你计算该表达式。返回一个表示表达式值的整数。
注意:
- 有效的算符为
'+'、'-'、'*'和'/'。- 每个操作数(运算对象)都可以是一个整数或者另一个表达式。
- 两个整数之间的除法总是 向零截断 。
- 表达式中不含除零运算。
- 输入是一个根据逆波兰表示法表示的算术表达式。
- 答案及所有中间计算结果可以用 32 位 整数表示。
cpp
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int>st;
// 遍历每个 token(数字/运算符)
for(string token : tokens){
// isdigit(token[0]):正数;
// token.size()>1:负数(如"-11")
if(isdigit(token[0]) || token.size() > 1){
int num = stoi(token); // 字符串转整数
st.push(num); // 数字直接入栈
}
// 遇到运算符,弹出两个数字进行计算
else{
// 注意顺序:先弹出的是右操作数n1,后弹出的是左操作数n2
int n1 = st.top(); st.pop();
int n2 = st.top(); st.pop();
// 根据运算符计算,结果入栈
if(token == "+") st.push(n2 + n1);
else if(token == "-") st.push(n2 - n1);
else if(token == "*") st.push(n2 * n1);
else if(token == "/") st.push(n2 / n1);
}
}
// 栈中最后剩余的数字就是最终结果
return st.top();
}
};
总结
1. 设计思想
- 遍历表达式:遇到数字直接入栈;
- 遇到运算符:从栈中弹出 2 个数字,先弹的是右操作数,后弹的是左操作数;
- 运算后入栈:将计算结果重新压入栈;
- 结束:遍历完成后,栈顶元素就是表达式结果。
2. 重点
- 运算顺序减法 / 除法不满足交换律,必须用后弹出的 n2 去运算先弹出的 n1,正确:
n2-n1; - 负数判断负数首字符是
-,isdigit()会判定为非数字,必须增加长度>1的判断条件;
3. 复杂度分析
- 时间复杂度:O(n) 仅遍历一次表达式数组,每个数字入栈 / 出栈各一次;
- 空间复杂度:O(n) 栈存储表达式中的所有数字,最坏情况全为数字。
239. 滑动窗口最大值
给你一个整数数组
nums,有一个大小为k的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的k个数字。滑动窗口每次只向右移动一位。返回 滑动窗口中的最大值。
cpp
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
deque<int> que; // 双端队列:存储元素下标,维护队列对应数值【单调递减】
vector<int> ans; // 存储每个窗口的最大值
for(int i = 0; i < nums.size(); i++){
// 1. 维护单调递减:当前元素 > 队尾元素,弹出队尾(保证队头永远是最大值)
while(!que.empty() && nums[i] > nums[que.back()]) {
que.pop_back();
}
que.push_back(i); // 当前元素下标入队
// 2. 移除窗口外的元素:队头下标超出窗口左边界,弹出队头
if(i - que.front() + 1 > k) {
que.pop_front();
}
// 3. 窗口形成(i >= k-1),记录队头(窗口最大值)
if(i >= k-1) {
ans.push_back(nums[que.front()]);
}
}
return ans;
}
};
总结
1. 设计思想
- 队列存储下标:方便判断元素是否在窗口内;
- 单调递减维护:队列中元素对应的值从队头到队尾严格递减,队头永远是当前窗口最大值;
- 窗口边界控制:实时移除超出窗口范围的队头元素;
- 窗口成型后:收集队头元素作为当前窗口最大值。
2. 重点
- 单调队列维护必须保证队列单调递减,才能让队头始终是最大值;
- 窗口边界
i - que.front() + 1 > k精准移除窗口外的过期元素; - 结果收集时机只有当
i >= k-1时,窗口才第一次成型,开始记录最大值。
3. 复杂度分析
- 时间复杂度:O(n) 每个元素仅入队、出队一次,线性遍历,完美解决大数据超时;
- 空间复杂度:O(k) 双端队列最多存储窗口内的
k个元素。
347. 前 K 个高频元素
给你一个整数数组
nums和一个整数k,请你返回其中出现频率前k高的元素。你可以按 任意顺序 返回答案。
cpp
class Solution {
public:
// 自定义比较器:实现小顶堆(升序排列,堆顶为频率最小的元素)
class cmp{
public:
bool operator() (const pair<int,int>& a, const pair<int,int>& b){
return a.second > b.second;
}
};
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int,int> mp; // 哈希表:key=数字,value=出现频率
vector<int> ans;
// 1. 统计每个数字出现的频率
for(int i=0;i<nums.size();i++){
mp[nums[i]]++;
}
// 2. 定义小顶堆:存储<数字,频率>,按频率升序
priority_queue<pair<int,int>,vector<pair<int,int>>,cmp> que;
// 3. 遍历哈希表,维护大小为k的小顶堆
for(auto i : mp){
que.push(i);
// 堆大小超过k,弹出堆顶(频率最小的元素)
if(que.size() > k) que.pop();
}
// 4. 堆中剩余的k个元素就是TopK高频元素,收集结果
while(!que.empty()){
ans.push_back(que.top().first);
que.pop();
}
return ans;
}
};
总结
1. 设计思想
- 哈希表统计:快速记录每个数字的出现次数;
- 小顶堆筛选:
- 堆的大小固定为
k; - 遍历所有频率,若堆超容则弹出频率最小的元素;
- 最终堆中保留的就是频率最高的 k 个元素;
- 堆的大小固定为
- 结果收集:将堆中元素取出即为答案。
2. 重点
- 小顶堆比较器
return a.second > b.second是小顶堆的关键,大于号决定升序排列; - 堆的大小控制始终保持堆大小为
k,保证最优时空效率; - 存储结构用
pair<数字, 频率>存储,方便同时关联数值与频率。
3. 复杂度分析
- 时间复杂度:O(nlogk) n 为数组长度,logk 为堆的调整开销,远优于排序的 O(nlogn);
- 空间复杂度:O(n) 哈希表存储所有元素,堆仅占用 O(k) 空间。
栈与队列章节总结
一、题型
1. 基础互模拟题(栈 ↔ 队列)
- 用栈实现队列:双栈(输入栈 + 输出栈),利用栈反转元素顺序,实现 FIFO
- 用队列实现栈:单队列循环移动元素,实现 LIFO
2. 栈的经典应用(核心:先进后出)
- 有效的括号:栈匹配嵌套括号,左括号压入对应右括号,一键匹配
- 删除相邻重复项:栈 / 字符串模拟栈,消除相邻重复元素
- 逆波兰表达式求值:栈存数字,遇运算符弹出计算,固定运算顺序
3. 高阶数据结构
- 滑动窗口最大值:单调递减双端队列,线性时间维护窗口最值
- 前 K 个高频元素:哈希表统计频率 + 小顶堆筛选 TopK
二、核心
- 栈:处理相邻匹配、嵌套、后缀表达式的首选结构
- 字符串模拟栈:替代栈容器,代码更简洁(删除重复项)
- 单调队列:滑动窗口求最值,时间复杂度压至 O(n)
- 小顶堆:TopK 问题最优解,空间仅保留 k 个元素
三、重难点
- 核心难点:单调队列(滑动窗口)、堆的自定义排序(TopK)
- 易错点:逆波兰表达式运算顺序、栈 / 队列模拟的边界判断
- 最优原则:能用模拟就不用冗余空间,能用线性复杂度就不用暴力解法