Leecode 150. 逆波兰表达式求值

题目描述

给你一个字符串数组 `tokens· ,表示一个根据 逆波兰表示法 表示的算术表达式。

请你计算该表达式。返回一个表示表达式值的整数。

注意:

有效的算符为 '+''-''*''/'

每个操作数(运算对象)都可以是一个整数或者另一个表达式。

两个整数之间的除法总是 向零截断

表达式中不含除零运算。

输入是一个根据逆波兰表示法表示的算术表达式。

答案及所有中间计算结果可以用 32 位 整数表示。

  • 示例 1:

输入tokens = ["2","1","+","3","*"]

输出9

解释:该算式转化为常见的中缀算术表达式为:\(((2 + 1) * 3) = 9\)

  • 示例 2:

输入tokens = ["4","13","5","/","+"]

输出6

解释:该算式转化为常见的中缀算术表达式为:\((4 + (13 / 5)) = 6\)

  • 示例 3:

输入tokens = ["10","6","9","3","+","-11","*","/","*","17","+","5","+"]

输出22

解释:该算式转化为常见的中缀算术表达式为:

  • \(((10 * (6 / ((9 + 3) * -11))) + 17) + 5\)

    \(= ((10 * (6 / (12 * -11))) + 17) + 5\)

    \(= ((10 * (6 / -132)) + 17) + 5\)

    \(= ((10 * 0) + 17) + 5\)

    \(= (0 + 17) + 5\)

    \(= 17 + 5\)

    \(= 22\)

解题思路与代码展示

ni逆波兰表达式即后缀表达式,其中每个运算按照顺序“先两个运算数,再运算符”的顺序排列,而非平时所常见的形如“a + b”这样的运算符在运算数中间的中缀表达式。而处理后缀表达式的计算过程中,只需要将遇到数都用一个栈来进行存储,而遇到一个操作符则从栈中取出两个操作数进行运算,随后再重新放回栈中进行后续运算。当表达式计算完成后,最后剩下的一个数就是最终计算结果。

由此我们可以按照上面运算逻辑给出下面代码:

class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> numStk; // 定义一个存放int类型的栈
for(int i = 0; i < tokens.size(); i++){ // 遍历整个token表达式数组
if(tokens[i] == "+" || tokens[i] == "-" || tokens[i] == "*" || tokens[i] == "/" ){ // 如果当前是运算符,则需要仅运算
int num1 = numStk.top(); // 记录当前栈顶并弹出
numStk.pop();
if(tokens[i] == "+")numStk.top() += num1; // 刚弹出的数与当前栈顶进行加法运算
else if(tokens[i] == "-")numStk.top() -= num1; // 进行减法运算
else if(tokens[i] == "*")numStk.top() *= num1; // 进行乘法运算
else if(tokens[i] == "/")numStk.top() /= num1; // 进行除法运算
}
else{ // 如果当前字符串不是+-*/这四种运算符中的一个,则需要将当前字符串数字转换为int类型数字
int num = 0; // 初始化当前数字遍历
int rate = 1; // rate变量用于平衡当前字符所在数字中的阶位
if(tokens[i][0] == '-'){ // 首先分类讨论,如果这是一个负数
for(int j = tokens[i].size()-1; j > 0; j--){ // 遍历这个负数除去第0位之外的部分,从最右一位遍历到第1位
num += (tokens[i][j] - '0') * rate; // 每一位乘以当前所在位的10的倍率
rate *= 10; // 更新rate遍历用于表示下一个阶位
}
num *= -1; // 由于是负数,最后需要多乘一个负一
}
else { // 如果当前数字不是负数
for(int j = tokens[i].size()-1; j >= 0; j--){ // 则需要从字符串的最高位遍历到第0位
num += (tokens[i][j] - '0') * rate; // 每一位的数字乘以所在位的倍率再相加
rate *= 10; // 更新rate表示下一个位置德尔倍率
}
}
numStk.push(num); // 将计算结果压栈
}
}
return numStk.top(); // 最终的结果就是最后栈顶的元素
}
};

使用上面代码即可完成逆波兰式的计算,同时值得一提的是,上面代码中最后将数字类型的字符串转换为int型的过程,可以直接使用c++11中的标准库中的stoi(string to int)转换函数,从而可以将代码简化为:

class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> numStk;
for(int i = 0; i < tokens.size(); i++){
if(tokens[i] == "+" || tokens[i] == "-" || tokens[i] == "*" || tokens[i] == "/" ){ // 如果当前是运算符,则需要仅运算
int num1 = numStk.top(); // 记录当前栈顶并弹出
numStk.pop();
if(tokens[i] == "+")numStk.top() += num1; // 刚弹出的数与当前栈顶进行加法运算
else if(tokens[i] == "-")numStk.top() -= num1; // 进行减法运算
else if(tokens[i] == "*")numStk.top() *= num1; // 进行乘法运算
else if(tokens[i] == "/")numStk.top() /= num1; // 进行除法运算
}
else numStk.push(stoi(tokens[i])); // 使用标准库中的stoi直接将string类型转换为int类型
}
return numStk.top();
}
};

上面使用了字符串转换为int型的标准库函数stoi,同时还有类似的另外几个函数:

  • stoi,将string类型转换为int类型
  • stol,将string类型转换为long类型
  • stoll,将string类型转换为long long 类型
  • stof,转换为flout类型
  • stod,转换为double类型

需要熟悉上面这几个常用的string类型的数字转换函数。

239. 滑动窗口最大值

题目描述

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回 滑动窗口中的最大值 。

  • 示例 1:

输入:nums = [1,3,-1,-3,5,3,6,7], k = 3

输出:[3,3,5,5,6,7]

解释:

滑动窗口的位置 最大值

--------------- -----

[1 3 -1] -3 5 3 6 7 3

1 [3 -1 -3] 5 3 6 7 3

1 3 [-1 -3 5] 3 6 7 5

1 3 -1 [-3 5 3] 6 7 5

1 3 -1 -3 [5 3 6] 7 6

1 3 -1 -3 5 [3 6 7] 7

  • 示例 2:

输入:nums = [1], k = 1

输出:[1]

解法1 暴力法

首先对于本题能想到一个非常直接的暴力法,就是遍历每一次窗口中的数,均求一次最大值,那么可以想象时间复杂度为\(O(k*n)\)。可以给出下面代码:

class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
vector<int> result(nums.size() - k + 1); // 初始化输出的最大值数组的长度
for(int i = 0; i < nums.size() - k + 1; i++){ // 遍历每一个窗口的首位
int curMax = nums[i]; // 初始化窗口内最大值
for(int j = 0; j < k; j++){ // 遍历窗口中的数
if(nums[i+j] > curMax) curMax = nums[i+j]; // 逐个比较窗口中的数,如果比当前值大,则保留
}
result[i] = curMax; // 在result数组中存放窗口最大值
}
return result; // 输出结果
}
};

上面用暴力法对本题进行了求解,但是却因为时间复杂度过大,在Leecode上提交代码后会出现超时的结果,因此我们考虑使用时间复杂度更低的算法。

解法2 双端队列滑动窗口

上面暴力解法的时间复杂度过高,导致超时无法通过,仔细分析上面代码我们可以看出,上面暴力解法中,我们对于每一次窗口移动都重新遍历了一遍窗口中的所有值来求最大值。而正是在这个每一次都遍历整个窗口导致花费了很多不必要的时间。

一个降低时间复杂度的想法是,如果每次窗口滑动之后,判断刚才滑出窗口的数是否等于上一步的最大值,如果不等于的话说明上一步的最大值还在窗口内,此时只需要再比较一下最右侧刚进入窗口中的数是否大于上一步的最大值即可,而不必完全遍历整个窗口。

上面提到的这种思想当然可以一定程度上降低时间复杂度,但是如果滑出窗口的是上一步的最大值呢?当最大值滑出之后,为了找到新的最大值,我们又需要重新遍历整个数组,从而再一次花费很多时间在重新搜索新的最大值上面。为了解决这个问题,我们需要考虑用一种数据结构来存储每一次窗口中从最大值开始的一个单调非增序列。这个单调非增序列是什么意思?我们接下来进一步介绍其中的思想。

所谓单调非增序列,或者称之为单调队列,从字面意思就能理解,但这里的单调并不意味着我们要讲窗口中的数进行排序(这里并不需要使用任何排序算法,或者说和排序根本没有任何关系),那么问题又来了,如果不用排序算法,这里的单调性又从何而来呢?为了进一步解释这些问题,下面给出一个具体的例子:

假设我们现在要求数组[4, -1, 3, 3, 1, 2, 0]中,滑动窗口长度为5的最大值数组。

  • 首先,对于第一个窗口[4, -1, 3, 3, 1],我们需要记录其中一个单调非增序列为[4, 3, 3, 1];此时可以看到,我们的单调性来自于选择性记录满足单调性的数,其中-1由于比后续的3更小,就被我们直接抛弃掉了,然后后续再记录其他的数。而此时这个序列中最大的元素就是数组中的第一个数4
  • 接着我们滑动窗口,此时的窗口数组为[-1, 3, 3, 1, 2],再选择性地记录其中的单调非增序列,得到队列[3, 3, 2],此时窗口中的最大元素还是该序列中的第一个元素3
  • 继续移动窗口,得到窗口数组为[3, 3, 1, 2, 0],此时的单调非增序列[3, 3, 2,0],窗口中最大元素是序列中第一个数3

    由上面过程可以看出,最终输出的结果为[4, 3, 3]

看完上面这个例子之后,我们再来解释所谓的单调非增序列,其实相当于记录了当前窗口中的最大值(即队列中第一个数)、以及之后当最大值移出后可能成为最大值的数。同时维护这个序列的过程,每一步需要进行判断:

  • 窗口移动后出去的数(上一步最左侧数)是否为当前队列最大数,如果是则当前队列最大数需要pop出来
  • 窗口移动后新进入的数(最右侧的数)是否比队列最右侧的数更大,如果更大则需要将队列中右侧的数pop出来,直至遇到队列为空,或者队列最右侧的数更大,此时将新的从右侧push
  • 取出队列中的第一个数,即为此时窗口中的最大值

通过每一步移动窗口后进行上面的维护算法来维护当前的单调非增序列,即可从中拿出最大值。但又需要注意的是,上面算法中涉及到了几个操作,例如“从左侧pop”、“从右侧push”、“从右侧pop”;而这几个操作都是在传统的队列和栈中没有的操作,队列仅支持在一侧push另一侧pop,而栈仅支持在同一侧push和pop。因此我们这里需要使用另一种数据结构,也就是本节标题所提到的“双端队列”(deque)。

双端队列deque这种数据结构,就如同其中文名所言,就是可以在两端都能进行操作的队列,即双端队列提供以下的操作:

  • push_back(),在队列末尾插入元素
  • pop_back(),在队列末尾删除元素
  • back(),队列末尾元素(不删除)
  • push_front(),在队列头插入元素
  • pop_front(),在队列头删除元素
  • front(),队列头元素(不删除)

上面这些操作的时间复杂度都是\(O(1)\),而使用这些操作就可以非常方便地使用双端队列deque这种数据类型。那么此时根据上面提到的算法思想,以及双端队列的数据结构,我们可以得到下面代码实现:

class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
deque<int> dq; // 定义一个存放int类型的双端队列
vector<int> re(nums.size() - k + 1); // 定义一个用于存放结果的int类型vector
for(int i = 0; i < k; i++){ // 计算第一个窗口中的单调序列
while(!dq.empty() && dq.back() < nums[i]) dq.pop_back(); // 如果队列末尾的元素比当前数更小,则需要pop出队
dq.push_back(nums[i]); // 将当前数插入队列中
}
re[0] = dq.front(); // 输出第一个窗口中的最大值
for(int i = 1; i < nums.size() - k + 1; i++){ // 移动窗口,此时i表示窗口的第一个元素的下标
if(!dq.empty() && nums[i-1] == dq.front()) dq.pop_front(); // 如果刚出窗口的元素等于队列中的第一个数,则队列需要删除第一个数
while(!dq.empty() && nums[i+k-1] > dq.back()) dq.pop_back(); // 如果新加入窗口的数比队列右侧的数更大,则需要将队列右侧的数出队
dq.push_back(nums[i+k-1]); // 将新加入窗口的数从右侧加入队列,至此相当于完成了滑动窗口后对单调序列的一次维护
re[i] = dq.front(); // 维护后的滑动窗口的第一个数即为此时窗口中的最大值,将其输入re数组中进行保存
}
return re; // 输出结果
}
};

上面使用双端队列来实现的算法中,每个元素最多入队和出队各一次,总共\(n\)个元素,相当于时间复杂度为\(O(n)\);而每一次从队列中取出最大值(即第一个元素)的时间复杂度为\(O(1)\)。因此总共的时间复杂度为\(O(n)\)。而实现上面算法使用了一个双端队列,这个队列的长度最长为窗口的长度,因此空间复杂度为\(O(k)\)。

Leecode 347. 前k个高频词

题目描述

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

  • 示例 1:

输入: nums = [1,1,1,2,2,3], k = 2

输出: [1,2]

  • 示例 2:

输入: nums = [1], k = 1

输出: [1]

解题思路与代码展示

本题要取出数组中出现频率最高的k个元素,需要完成以下两个步骤:

  • 首先统计数组中每个数出现的频率
  • 再根据统计结果取出出现频率最高的k个元素

其中对数组中\(n\)个数频率的统计,可以考虑使用哈希函数映射,即unordered_map数据结构,能够在\(O(1)\)时间内修改、查询某个数的出现次数。随后可以将哈希映射中存储的pair<int,int>l类型对根据其中第二个数的大小进行排序,并取出前k个元素即可。所以此时我们可以而如果使用快排,此时需要的时间复杂度为\(O(n) + O(n\log n) \approx O(n\log n)\)。但此时由于我们要取出的仅仅是前\(k\)个元素,如果对整个数组都进行排序,相当于对前\(k\)个以外的数进行了不必要的排序。为此我们可以考虑使用堆(heap)数据类型来实现,或者称之为优先队列(priority_queue)。

优先队列可以理解为有一定顺序的一个数组,但是并非经过排序后有一个严格单调顺序的序列,而仅仅只有队列第一个数是最大(或最小,取决于这是大根堆还是小根堆)。其中实现方法在于每一次入队和出队时都经过了堆中的“下坠”和“上浮”操作,从而确保堆顶元素的最值性。而堆中每一次取出和放入数的时间复杂度都是\(O(\log k)\),其中\(k\)表示堆的大小,同时在本题中堆的大小即为要求的前\(k\)个元素。因此在本题中,我们只需要维护一个大小为\(k\)的小根堆,保证堆顶的都是出现频率排第\(k\)的数,如果遇到出现频率更大的数,则将堆顶出队,将新的数入队即可。

最终堆中的\(k\)个数即为排序前\(k\)的数。由此我们可以得到代码如下:

class Solution {
public:
struct Compare{ // 运算符重载,定义一种用于比较两个<int,int>类型整数对的比较运算
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> umap; // 定义哈希映射map数据结构
for(int i = 0; i < nums.size(); i++){ // 遍历nums中每个数,并统计其出现次数,没出现一次就将对应的值+1
umap[nums[i]]++;
}
priority_queue<pair<int,int>, vector<pair<int,int>>, Compare> pq; // 定义一个优先队列,其中的比较关系为上面定义的整数对之间的比较函数 for(auto cur : umap){ // 遍历哈希表中所有出现过的数
pq.push({cur.first,cur.second}); // 将当前整数对push入优先队列中
if(pq.size() > k) pq.pop(); // 保证优先队列长度为k,将其中出现频率最低的数pop出来
}
vector<int> result(k); // 定义一个用于存放结果的vector数组,长度为k
int p = 0; // 初始化数组指针,指向数组第一个元素
while(!pq.empty()){ // 将优先队列中所有元素pop出队,每次pop出来的都是当前队列中出现频率最低的数
result[p] = pq.top().first; // 堆顶的first即为对应的数,将其记录在result数组中
pq.pop(); // 将堆顶的数pop出队
p++; // 数组指针指向下一个位置继续记录
}
return result; // 输出结果
}
};

上面代码中,先统计每个数出现频率需要遍历整个数组,时间复杂度为\(O(n)\),后续将每个整数对都放入优先队列中,并不断取出堆中元素控制长度在\(k\),总共\(n\)个数每个数总共入堆出堆各一次,时间复杂度为\(O(n\log k)\)。故总的时间复杂度为\(O(n) + O(n\log k) \approx O(n\log k)\)。相较于使用快排的时间复杂度\(O(n \log n)\)也有较大提升,特别是当\(k\)和\(n\)的取值差距较大的时候。

今日总结

今天这几道题目都有一定难度,不过也确实学到了挺多东西:

  • 首先是对于第一道逆波兰表达式,学习到了几个字符串转数字类型的函数,stoi, stol, stoll, stof, stod
  • 第二道滑动窗口,学习了双端队列(deque)类型,及其可以从front和back两侧在\(O(1)\)时间内pop和push的数据特性,学习了使用双端队列实现单调序列解决滑动窗口最大值的方法
  • 第三道前k高频词,复习了priority_queue优先队列(堆)数据结构,学会了如何在其上自行定义比较函数来进行排序(需要使用一个struct结构来进行运算符重载)

代码随想录第十一天 | Leecode 150. 逆波兰表达式求值、239. 滑动窗口最大值、347. 前k个高频词的更多相关文章

  1. 代码随想录算法训练营day12 | leetcode 239. 滑动窗口最大值 347.前 K 个高频元素

    基础知识 ArrayDeque deque = new ArrayDeque(); /* offerFirst(E e) 在数组前面添加元素,并返回是否添加成功 offerLast(E e) 在数组后 ...

  2. 代码随想录第十三天 | 150. 逆波兰表达式求值、239. 滑动窗口最大值、347.前 K 个高频元素

    第一题150. 逆波兰表达式求值 根据 逆波兰表示法,求表达式的值. 有效的算符包括 +.-.*./ .每个运算对象可以是整数,也可以是另一个逆波兰表达式. 注意 两个整数之间的除法只保留整数部分. ...

  3. 代码随想录算法训练营day11 | leetcode 20. 有效的括号 1047. 删除字符串中的所有相邻重复项 150. 逆波兰表达式求值

    基础知识 String StringBuilder 操作 public class StringOperation { int startIndex; int endIndex; { //初始容量为1 ...

  4. LeetCode 150. 逆波兰表达式求值(Evaluate Reverse Polish Notation) 24

    150. 逆波兰表达式求值 150. Evaluate Reverse Polish Notation 题目描述 根据逆波兰表示法,求表达式的值. 有效的运算符包括 +, -, *, /.每个运算对象 ...

  5. Java实现 LeetCode 150 逆波兰表达式求值

    150. 逆波兰表达式求值 根据逆波兰表示法,求表达式的值. 有效的运算符包括 +, -, *, / .每个运算对象可以是整数,也可以是另一个逆波兰表达式. 说明: 整数除法只保留整数部分. 给定逆波 ...

  6. Leetcode 150.逆波兰表达式求值

    逆波兰表达式求值 根据逆波兰表示法,求表达式的值. 有效的运算符包括 +, -, *, / .每个运算对象可以是整数,也可以是另一个逆波兰表达式. 说明: 整数除法只保留整数部分. 给定逆波兰表达式总 ...

  7. LeetCode:逆波兰表达式求值【150】

    LeetCode:逆波兰表达式求值[150] 题目描述 根据逆波兰表示法,求表达式的值. 有效的运算符包括 +, -, *, / .每个运算对象可以是整数,也可以是另一个逆波兰表达式. 说明: 整数除 ...

  8. 【python】Leetcode每日一题-逆波兰表达式求值

    [python]Leetcode每日一题-逆波兰表达式求值 [题目描述] 根据 逆波兰表示法,求表达式的值. 有效的算符包括 +.-.*./ .每个运算对象可以是整数,也可以是另一个逆波兰表达式. 说 ...

  9. pintia 3-7-5 逆波兰表达式求值 (20 分)

    3-7-5 逆波兰表达式求值 (20 分) 逆波兰表示法是一种将运算符(operator)写在操作数(operand)后面 的描述程序(算式)的方法.举个例子,我们平常用中缀表示法描述的算式(1 + ...

  10. lintcode 中等题:Evaluate Reverse Polish notation逆波兰表达式求值

    题目 逆波兰表达式求值 在逆波兰表达法中,其有效的运算符号包括 +, -, *, / .每个运算对象可以是整数,也可以是另一个逆波兰计数表达. 样例 ["2", "1&q ...

随机推荐

  1. presto解析jsonArr转多行

    一.假数据解析 SELECT r1.col.dataSourceId, r1.col.database, r1.col.dataTable FROM (SELECT explode(r.json) A ...

  2. WPF程序性能优化总结

    原文链接: https://blog.csdn.net/u010265681/article/details/77571947 WPF程序性能由很多因素造成,以下是简单地总结: 元素: 1. 减少需要 ...

  3. 解决 Docker 容器镜像拉取难题:全面指南

    一.引言 在使用 Docker 容器的过程中,经常会遇到镜像拉取慢甚至无法下载的问题,这给开发和部署工作带来了不小的困扰.本文将深入探讨这一问题的原因,并提供多种有效的解决方案. 二.问题原因分析 网 ...

  4. 机器学习 | 强化学习(6) | 策略梯度方法(Policy Gradient Method)

    6-策略梯度方法(Policy Gradient Method) 策略梯度概论(Introduction) 基于策略(Policy-Based) 的强化学习 对于上一节课(价值函数拟合)中采用参数\( ...

  5. 视频笔记软件JumpVideo技术解析一:Electron案例-调用VLC播放器

    大家好,我是TheGodOfKing,是 最强考研学习神器,免费视频笔记应用JumpVideo,可以快速添加截图时间戳,支持所有笔记软件,学习效率MAX!的开发者之一,分享技术的目的是想找到更多志同道 ...

  6. git clone加速

    使用github的镜像网站进行访问,github.com.cnpmjs.org,我们将原本的网站中的github.com 进行替换.

  7. oracle数据库体系架构详解

    在学习oracle中,体系结构是重中之重,一开始从宏观上掌握它的物理组成.文件组成和各种文件组成.掌握的越深入越好.在实际工作遇到疑难问题,其实都可以归结到体系结构中来解释.体系结构是对一个系统的框架 ...

  8. Ubuntu截屏工具推荐

    Ubuntu截屏工具推荐 本篇博文推荐Ubuntu下的截屏工具Flameshot,可以作为Windows下Snipaste截图工具的平替. GitHub地址:https://github.com/fl ...

  9. 寻找可靠的长久的存储介质之旅,以及背后制作的三个网页“图片粘贴转base64”、“生成L纠错级别的QR码”、“上传文件转 base64以及粘贴 base64 转可下载文件”

    其实对于目前的形式来说,虽然像 U 盘.固态硬盘.甚至光盘这些信息储存介质(设备)的容量越来越高,但是不得不说这些设备的可靠性依然像悬着的一块石头,虽然这块石头确实牢牢的粘在天花板上,但是毕竟是粘上去 ...

  10. BUUCTF---Morse

    1.题目 -..../.----/-..../-..../-..../...--/--.../....-/-..../-..../--.../-.../...--/.----/--.../...--/ ...