力扣150 逆波兰表达式求值
题目链接/文章讲解/视频讲解:代码随想录
思路:遍历字符串,遇到数字就入栈,遇到运算符号取出栈中两个数字计算并将结果入栈。这道题目主要理解逆波兰表达式的特点就很好解题。
逆波兰表达式(Reverse Polish Notation, RPN)是一种将运算符置于操作数之后的数学和计算机科学表达方式。它也被称为后缀表达式,与传统的中缀表达式(即运算符在操作数之间)相对应。
在逆波兰表达式中,每个运算符跟随其相关的操作数。这种表示法消除了括号的需求,并且使得表达式的计算顺序更加明确。例如,将中缀表达式
"3 + 4 * 5"
转换为逆波兰表达式后变为"3 4 5 * +"
。在这个逆波兰表达式中:
- 数字
"3"
和"4"
直接被输出。"5"
跟随在操作符"*"
后面,因为它是操作符"*"
的操作数。- 最后,操作符
"+"
跟随在结果"4 * 5"
的后面,因为它是操作数"3"
和"4 * 5"
的操作符。逆波兰表达式的主要优点包括:
- 无需括号:由于操作符在操作数后面,因此不需要使用括号来改变计算顺序。
- 简单的计算顺序:计算顺序从左到右进行,每次操作符遇到足够的操作数时立即计算。
逆波兰表达式通常用于栈数据结构的计算过程中,其中操作符遇到操作数时直接进行计算,并将结果推入栈中,直到整个表达式被处理完毕。
例如,对于逆波兰表达式
"3 4 + 5 *"
的计算过程如下:
- 将
"3"
和"4"
推入栈中。- 遇到
"+"
操作符,弹出栈顶的两个元素(4
和3
),计算3 + 4
,结果7
推入栈中。- 将
"5"
推入栈中。- 遇到
"*"
操作符,弹出栈顶的两个元素(5
和7
),计算7 * 5
,结果35
推入栈中。最终,栈中仅剩下一个元素
35
,即整个表达式的计算结果。逆波兰表达式因其简单的计算规则和无需括号的特性,在计算器程序、编译器和解析器等领域中得到广泛应用。
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<long long> st;
for(int i = 0; i < tokens.size(); i++){
if(tokens[i] == "+" || tokens[i] == "-" || tokens[i] == "*" || tokens[i] == "/"){
long long num1 = st.top();
st.pop();
long long num2 = st.top();
st.pop();
if(tokens[i] == "+") st.push(num2 + num1);
if(tokens[i] == "-") st.push(num2 - num1);
if(tokens[i] == "*") st.push(num2 * num1);
if(tokens[i] == "/") st.push(num2 / num1);
}
else{
st.push(stoll(tokens[i]));
}
}
int res = st.top();
st.pop();
return res;
}
};
代码要注意类型匹配的问题,在else分支中,tokens[i]
是 string
类型,而栈 st
期望 long long
类型 。
在C++中,
stoll()
是一个函数,用于将字符串转换为长整型(long long
)。其原型定义在<string>
头文件中,函数签名如下:long long stoll (const string& str, size_t* idx = 0, int base = 10);
这个函数的作用是将字符串
str
转换为长整型数值long long
。参数说明如下:
str
:要转换的字符串。idx
:可选参数,用于存储第一个无效字符的索引(即转换停止的位置)。如果不需要此信息,可以将其设置为nullptr
。base
:可选参数,指定进制,默认为十进制。可以设置为 0 或者介于 2 到 36 之间的值。如果base
是 0,则自动根据字符串内容判断其进制(比如 “0x” 开头的字符串会被识别为十六进制)。函数返回转换后的长整型数值。如果无法进行有效转换(例如字符串格式不符合预期或超出
long long
的范围),将会引发std::out_of_range
异常或返回0。
力扣239 滑动窗口最大值
题目链接/文章讲解/视频讲解:代码随想录
思路:在这里构建了一个单调队列,在队列中只维护可能的最大值,同时保证队列的单调性。这里需要明白一个逻辑,在滑动窗口移动的过程中,如果次大值在最大值之前,那么次大值不再生效。如果次大值在最大值之后,那么次大值的有可能生效。所以维护队列是单调递减的且最大值位于队列出口即可,可以放心舍弃不符合条件的元素。至于如何保证队列的单调性,这里的思路是利用push操作。如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止。而如何判断当前最大值是否还有效,则利用pop操作。如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作。
class Solution {
private:
class MyQueue{
public:
deque<int> que;
void pop(int value){
if(!que.empty() && value == que.front()){
que.pop_front();
}
}
void push(int value){
while(!que.empty() && value > que.back()){
que.pop_back();
}
que.push_back(value);
}
int front(){
return que.front();
}
};
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
MyQueue que;
vector<int> result;
for(int i = 0; i < k; i++){
que.push(nums[i]);
}
result.push_back(que.front());
for(int i = k; i < nums.size(); i++){
que.pop(nums[i-k]);
que.push(nums[i]);
result.push_back(que.front());
}
return result;
}
};
我对代码结构有一些疑惑,查阅资料:
MyQueue
类被定义在了Solution
类的private
访问控制部分,而MyQueue
类的接口(即其公共成员函数)则被标记为public
。这种做法有其特定的目的和优点,下面是详细的解释:1. 封装性
将
MyQueue
定义为Solution
类的private
成员类,意味着它只能被Solution
类的成员函数访问。这是一种封装的技术,可以隐藏MyQueue
的实现细节,只暴露必要的接口。这使得Solution
类内部的设计更为清晰,并且外部代码不能直接依赖或修改MyQueue
的实现,从而减少了潜在的错误和维护成本。2. 作用域限定
通过将
MyQueue
定义为Solution
的private
成员类,你限定了MyQueue
的作用域仅限于Solution
类内部。这种做法确保了MyQueue
只能用于Solution
类内部的逻辑,而不能被其他类或函数直接访问。这有助于保持类之间的独立性和一致性。3. 组织和结构
将
MyQueue
嵌套在Solution
类中,还可以更好地组织代码,使其结构更清晰。MyQueue
是Solution
的一个内部实现细节,因此将其嵌套在Solution
类中能够将相关功能聚合在一起。这种方式有助于维护代码的逻辑结构,并使得MyQueue
与Solution
的紧密关系更加明确。4. 访问控制
虽然
MyQueue
类的成员函数被声明为public
,以便其他类(在本例中是Solution
的成员函数)可以使用它,但其实现细节(成员变量和具体实现)仍然是私有的。这种方式允许MyQueue
类提供公共接口,同时保护其内部状态不被直接访问或修改
力扣347 前 K 个高频元素
题目链接/文章讲解/视频讲解:代码随想录
思路:看到这道题目最先想到的就是哈希表,但是对set和map区分有些模糊,先复习一下:
1. 数组(Array)
特点:
- 索引访问:可以通过索引快速访问元素。
- 固定大小:在创建时大小固定(某些语言和库允许动态扩展)。
- 简单的存储:适合存储顺序排列的数据。
适用场景:
- 已知大小:当数据量固定或者已知最大大小时使用。
- 频繁索引访问:需要通过索引快速访问和更新数据时使用。
- 简单的数据结构:当不需要复杂的查找操作,只需要顺序存储时使用。
示例:
- 存储学生的分数:
scores = [90, 80, 70]
2. 集合(Set)
特点:
- 唯一性:不允许重复元素。
- 无序:元素没有特定的顺序。
- 高效的成员检测:支持 O(1) 平均时间复杂度的元素存在性检查。
适用场景:
- 去重:当需要存储唯一元素,并且检查元素是否存在时使用。
- 不关心顺序:当元素的顺序不重要时使用。
- 快速查找:需要快速判断某个元素是否存在于集合中时使用。
示例:
- 存储唯一的用户ID:
user_ids = {101, 102, 103}
3. 映射(Map)
特点:
- 键值对存储:每个元素都是一个键值对。
- 高效的查找和插入:支持 O(1) 平均时间复杂度的查找、插入和删除操作。
- 无序:键值对没有特定的顺序(某些实现如
TreeMap
是有序的)。适用场景:
- 关联存储:需要根据键快速查找、更新或删除对应的值时使用。
- 频率统计:记录和查找每个元素的频率时使用。
- 映射关系:需要维护一对一的映射关系,如用户ID到用户名的映射。
示例:
- 存储单词及其出现频率:
word_count = {'apple': 3, 'banana': 2, 'cherry': 1}
总结
- 数组:适用于固定大小的数据存储和频繁的索引访问。
- 集合:适用于需要唯一性和快速存在性检查的场景。
- 映射:适用于需要关联存储和快速键值对查找的场景。
统计好频率后,只需在优先级队列中维护k个频率最高的元素。而大顶堆会弹出最大的元素,不利于维护k个频率最高的元素。所以用小顶堆,当堆的大小超过 K 时,弹出堆顶元素,这样堆中始终保存的是频率最高的 K 个元素。
class Solution {
public:
class mycomparison{
public:
bool operator()(const pair<int ,int>& lhs, const pair<int, int>& rhs){
return lhs.second > rhs.second;
}
};
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int, int> map;
for(int i = 0; i < nums.size(); i++){
map[nums[i]]++;
}
priority_queue<pair<int ,int>, vector<pair<int, int>>, mycomparison> pri_que;
for(unordered_map<int, int>::iterator it = map.begin(); it != map.end(); it++){
pri_que.push(*it);
if(pri_que.size() > k){
pri_que.pop();
}
}
vector<int> result(k);
for(int i = k - 1; i >= 0; i--){
result[i] = pri_que.top().first;
pri_que.pop();
}
return result;
}
};
疑问:pri_que.push(*it);为什么是*it不是it
在 C++ 中,*it
和 it
在不同的上下文中具有不同的含义。让我们详细解释一下为什么在 pri_que.push(*it)
中需要使用 *it
而不是 it
。
迭代器和解引用
-
迭代器(
it
):it
是一个迭代器,用于指向容器中的某个元素。- 迭代器类似于指针,允许访问和遍历容器中的元素。
-
解引用(
*it
):*it
是对迭代器的解引用操作,它返回迭代器指向的实际元素(即unordered_map
中的一个pair<int, int>
)。- 解引用操作
*it
实际上获取了迭代器指向的值,这个值可以被直接使用或操作。
pri_que.push(*it)
和 pri_que.push(it)
的区别
-
pri_que.push(*it)
:*it
是解引用操作,表示获取迭代器it
指向的元素。- 如果
it
是一个unordered_map<int, int>::iterator
,那么*it
就是unordered_map
中的一个pair<int, int>
。 pri_que
是一个优先队列,它期望接受一个pair<int, int>
类型的参数。所以我们需要将*it
(即pair<int, int>
)传递给push
函数。
-
pri_que.push(it)
:it
是一个迭代器,类型是unordered_map<int, int>::iterator
。push
函数的参数类型需要是pair<int, int>
,而不是迭代器。因此,将迭代器it
直接传递给push
函数会导致编译错误,因为push
函数不接受迭代器类型的参数。
代码详解
unordered_map<int, int>::iterator it = map.begin();
1. unordered_map<int, int>
unordered_map
是 C++ 标准库中的一个容器,存储键值对(key-value
),其中key
和value
的类型可以自定义。在这个例子中,unordered_map
的键和值都是int
类型。unordered_map<int, int>
表示一个键和值都是整数的哈希表。它是基于哈希表实现的,因此插入、查找和删除操作的平均时间复杂度是 O(1)。
2. ::iterator
iterator
是unordered_map
的一个内部类型,表示容器中元素的位置。它用于遍历容器中的元素。- 通过
unordered_map<int, int>::iterator
,我们可以访问unordered_map
中存储的键值对。
3. it
it
是一个迭代器变量,用于遍历unordered_map
中的元素。- 在这段代码中,
it
被初始化为map.begin()
,即unordered_map
中的第一个元素的迭代器。
4. map.begin()
map
是一个unordered_map<int, int>
对象。map.begin()
返回一个指向unordered_map
中第一个元素的迭代器。如果容器为空,则返回的迭代器等于map.end()
,表示容器的结束位置。