50. 逆波兰表达式求值
给你一个字符串数组 tokens
,表示一个根据 逆波兰表示法 表示的算术表达式。
请你计算该表达式。返回一个表示表达式值的整数。
题目链接/文章讲解/视频讲解:代码随想录
逆波兰表达式(后缀表达式)
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<long long>st;
for(const string& s:tokens)
{
if(isdigit(s[0])||(s.size()>1&&s[0]=='-'))
{st.push(stoll(s));}//表示遇见运算符直接压入栈
else //表示遇见操作符
{
long long a=st.top();st.pop();
long long b=st.top();st.pop();
long long all=0;
if(s=="+") all=a+b;
else if(s=="-") all=b-a;
else if(s=="*") all=a*b;
else if(s=="/") all=b/a;
st.push(all);
}
}
return st.top();
}
};
将逆波兰式转化为中缀表达式过程:遇到数字进栈,遇到运算符使栈顶的前两个数字出栈,第一个出站的数字位于右运算位,后出栈的存在于左运算位,然后计算运算结果在压入栈。
需要完善的相关基础语法知识:
1.单引号只能表示单个字符,单引号只能用于表示单个字符,即 char
类型。
char ch = 'AB'; // ❌ 错误!单引号只能包含一个字符
双引号用于表示字符串,即 string
或 const char*
char ch = "A"; // ❌ 错误!"A" 是字符串,不能赋值给 char
2.
for (string s : tokens) { ... }
这样 s
不是引用,而是 tokens
里元素的拷贝,会导致额外的性能损耗。
for (const string& s : tokens) { ... }
const避免修改s,使更高效
3.stoll的用法,使字符串转化 long long类型的整数
#include <iostream>
#include <string>
using namespace std;
int main() {
string str1 = "12345";
string str2 = "-67890";
long long num1 = stoll(str1);
long long num2 = stoll(str2);
cout << num1 << endl; // 输出 12345
cout << num2 << endl; // 输出 -67890
return 0;
}
239. 滑动窗口最大值
单调队列的应用
题目链接/文章讲解/视频讲解:代码随想录、
队列和双端队列的区别
普通队列(Queue)
- 类似于排队买票:
- 只能从 后面(队尾) 加人(
push
)。 - 只能从 前面(队头) 取票(
pop
)。
- 只能从 后面(队尾) 加人(
queue<int> q;
q.push(1); // [1]
q.push(2); // [1, 2]
q.push(3); // [1, 2, 3]
q.pop(); // [2, 3] (只能从前面移除)
双端队列(Deque)
- 类似于 双向进出的队伍:
- 既可以从 前面 进、也可以从 后面 进。
- 既可以从 前面 出、也可以从 后面 出。
deque<int> dq; dq.push_back(1); // [1] dq.push_back(2); // [1, 2] dq.push_front(0); // [0, 1, 2] dq.pop_back(); // [0, 1] (从后面移除) dq.pop_front(); // [1] (从前面移除)
暴力解法的缺点
如果你每次滑动窗口后,都要重新找一遍最大值,那就很慢了:
- 每次都要遍历
k
个数,整个数组长度为n
,时间复杂度是O(n*k)
,会超时! - 我们需要更聪明的方法!
-
我们的聪明策略 —— 用“队伍管理”技巧!
我们可以用一个 双端队列(deque) 来维护 窗口内可能成为最大值的元素索引,确保:
- 队列里的元素单调递减(队头存最大值)。
- 只存有可能成为最大值的索引,没用的直接踢掉!
-
形象比喻
想象你是一名篮球教练,你要在一场 3V3 的比赛中,每次从一排球员里挑选最强的球员上场:
- 球员(数字)按照体能(值)排队。
- 每次只能选择连续的
k
个球员。 - 你要管理好球队阵容,保证最强球员始终站在队伍前列(deque 头部)!
- 如果一个新来的球员比你队伍里最弱的球员强,那就把弱的踢掉,保证队伍单调递减!
-
class Solution { public: vector<int> maxSlidingWindow(vector<int>& nums, int k) { deque<int>q;//用双端队列来存储滑动窗口有可能最大的下标,保证头部元素下标代表的数值最大 vector<int>res;//用数组来存储最后结果,为一系列数字 for(int i=0;i<nums.size();i++) { if(!q.empty()&&q.front()<i-k+1)//i-k+1代表窗口的最左端 { q.pop_front();//删除已经越界的数组下标 } while(!q.empty()&&nums[i]>nums[q.back()]) { q.pop_back(); }//替换掉队伍中最弱的元素,可以不停的替换删除 q.push_back(i); if(i>=k-1)//代表窗口已经形成 { res.push_back(nums[q.front()]);//记录单调队列的最大值 } } return res;//经历完所有循环,返回最终结果 } };
为什么是
O(n)
复杂度?每个元素:
- 最多入队一次
- 最多出队一次
-
整个
nums
只遍历了一遍,每个元素只操作O(1)
次,因此时间复杂度是O(n)
347.前 K 个高频元素 (有点难度,可能代码写不出来,一刷至少需要理解思路)
给你一个整数数组 nums
和一个整数 k
,请你返回其中出现频率前 k
高的元素。你可以按 任意顺序 返回答案。
题目链接/文章讲解/视频讲解:https://programmercarl.com/0347.%E5%89%8DK%E4%B8%AA%E9%AB%98%E9%A2%91%E5%85%83%E7%B4%A0.html
我们需要找到数组 nums
中出现频率最高的 k
个元素,可以用哈希表 + 小顶堆来高效解决。
在 C++ 中,默认的 priority_queue
是最大堆,但我们可以使用 greater<>
或者 vector
+ make_heap()
等方式 实现最小堆。
C++ STL 提供的 priority_queue
默认是大顶堆,即 堆顶元素最大:
#include <iostream>
#include <queue>
using namespace std;
int main() {
priority_queue<int> maxHeap; // 默认是大顶堆(最大堆)
maxHeap.push(3);
maxHeap.push(1);
maxHeap.push(5);
cout << "最大堆的堆顶:" << maxHeap.top() << endl; // 输出 5
return 0;
}
2. 使用 greater<>
创建最小堆
方式 1:priority_queue<T, vector<T>, greater<T>>
#include <iostream>
#include <queue>
using namespace std;
int main() {
priority_queue<int, vector<int>, greater<int>> minHeap; // 最小堆
minHeap.push(3);
minHeap.push(1);
minHeap.push(5);
cout << "最小堆的堆顶:" << minHeap.top() << endl; // 输出 1
return 0;
}
- 第一参数
int
: 表示存储的类型 - 第二参数
vector<int>
: 指定底层容器(默认就是vector
) - 第三参数
greater<int>
: 让priority_queue
变成 最小堆(堆顶最小) -
pair
作为最小堆,在 哈希表统计频率 + 最小堆 方案中,我们会用pair<int, int>
存储 (频率, 元素),并按 频率排序: -
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> minHeap;
#include <iostream> #include <queue> using namespace std; int main() { priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> minHeap; minHeap.emplace(3, 100); // 频率 3, 数字 100 minHeap.emplace(1, 200); // 频率 1, 数字 200 minHeap.emplace(2, 300); // 频率 2, 数字 300 cout << "最小堆的堆顶:" << minHeap.top().second << endl; // 输出 200(因为频率最小是1) return 0; }
为什么使用
emplace()
而不是push()
?如果使用
push()
,需要先手动创建一个pair<int, int>
,然后再插入: -
minHeap.push(make_pair(3, 100));
为什么用
vector
? unordered_map<int, int>
不能直接排序,所以需要转换成vector
便于排序或建堆。
解决方案
- 统计频率:使用
unordered_map
统计每个数字出现的次数。 - 使用小顶堆(优先队列):维护
k
个高频元素。- 当堆的大小超过
k
时,弹出最小的元素,确保堆内始终是前k
大的频率元素。
- 当堆的大小超过
- 提取结果:取出堆中的元素,返回即可。
class Solution { public: vector<int> topKFrequent(vector<int>& nums, int k) { unordered_map<int,int>freqmap;//unordered_map不能直接排序 for(int i=0;i<nums.size();i++) { freqmap[nums[i]]++;///形成pair<int,int>(数字,频率)的形式 } priority_queue<pair<int,int>,vector<pair<int,int>>,greater<>>minHeap;//最小堆 //为了便于排序,需要转化为(频率,数字)的形式 for(const auto&[key,value]:freqmap) { minHeap.emplace(value,key); if(minHeap.size()>k) { minHeap.pop();//维护堆的大小不超过k } } vector<int>res;//双端数组存储最终结果push_back,pop_back while(!minHeap.empty()) {res.push_back(minHeap.top().second); minHeap.pop(); } return res; } };
采用最小堆的原因:当堆中元素超过k时,弹出顶部元素,即最小元素,维护的堆中的频率前k大的元素,最终再将堆中存储的大频率元素一个个弹出。
-
补充知识