Day11 栈与队列:150.逆波兰表达式求值 239.滑动窗口最大值 347.前k个高频元素

91 阅读6分钟

150.逆波兰表达式求值

难度指数:😀🙂

这不仅仅是一道好题,也展现出计算机的思考方式

用栈来解决逆波兰表达式的计算过程。

题目链接:150.逆波兰表达式求值

逆波兰表达式就是一种后缀表达式

计算机在做运算的时候,用后缀表达式更加方便它的运算,因为计算机直接按照顺序去处理,不需要担心括号的优先级。

用栈来模拟后缀表达式的计算过程:

遇见数字就加入到栈里,遇见操作符就从栈里取出元素,进行计算后放回到栈里。

动画:

11.01.gif

灵魂发问:为什么最后都计算出结果了,还给它加入到栈里?直接返回结果不香吗?

答:从代码整体上的简洁性来考虑,加入到栈里是可以的,省去最后做特殊处理的麻烦。

这道题,两个数字,遇见一个操作符就进行消除的操作,(和前两道题的思路大同小异)

栈这种数据结构非常适合做相邻字符的消除操作

代码思路:

遇见操作符,就从栈里取元素进行运算的操作。

 stack<int> st;
 ​
 for (i = 0; i < s.size(); i++) {
     if (s[i] == '+' || s[i] == '-' || s[i] == '*' || s[i] == '/') {
         nums1 = st.top();  //获取到栈顶元素
         st.pop();  //将栈顶元素弹出
         
         nums2 = st.top();
         st.pop();
         if (s[i] == '+') st.push(nums2 + nums1);
         if (s[i] == '-') st.push(nums2 - nums1);
         if (s[i] == '*') st.push(nums2 * nums1);
         if (s[i] == '/') st.push(nums2 / nums1);
     }
     else st.push(s[i]);
     
     int result = st.top();
     st.pop();
     return result;
 }

说一嘴:题目给我们的逆波兰表达式一定是合法的表达式,我们这里没有对异常的情况做处理。

AC代码: (核心代码模式)

 class Solution {
 public:
     int evalRPN(vector<string>& tokens) {
         stack<int> st;
         for (int i = 0; i < tokens.size(); i++) {
             if (tokens[i] == "+" || tokens[i] == "-" || tokens[i] == "*" || tokens[i] == "/") {
                 int nums1 = st.top();  //获取到栈顶元素
                 st.pop();  //将栈顶元素弹出
 ​
                 int nums2 = st.top();
                 st.pop();
                 if (tokens[i] == "+") st.push(nums2 + nums1);
                 else if (tokens[i] == "-") st.push(nums2 - nums1);
                 else if (tokens[i] == "*") st.push((long)nums2 * nums1);
                 else if (tokens[i] == "/") st.push(nums2 / nums1);
             }
             else st.push(stoi(tokens[i]));  //若遍历的是数字,push到栈里面
         }
         int result = st.top();
         st.pop();
         return result;
     }
 };

题目没啥难度,关键是理解逆波兰表达式是从哪里来的,就是从二叉树的后续表达式(后序遍历)来的。

239.滑动窗口最大值

难度指数:😀😕🙁😩

题目链接:239.滑动窗口最大值

题意:

给定一个数组,给出滑动窗口的范围,比如:k = 3,用这个滑动窗口去遍历这个数组,每次返回滑动窗口内的最大值。

很多人会有暴力出奇迹的想法,O(n × k)


更优的解法:

观察滑动窗口的移动过程,你会发现很像一个队列:

队列里面始终维护着这个窗口,每移动一个位置,就对应 pop 掉一个元素,同时 push 进来一个元素

pop();

push();

getMaxValue(); 每次调用这个函数,就询问这个队列里面当前的最大值是多少。

倘若队列里面能支持上面的操作,就很nice,但目前编程语言里还没有支持这个数据结构。

有同学会想:用优先级队列来解决这个问题

其实C++里面优先级队列这种数据结构就是大顶堆或者小顶堆,它只不过是单调递增或者单调递减。

……05:00~05:30

例如这个优先级队列,你就是要让它单调递减,那么内部实现就是一个大顶堆,(大顶堆就是一个将里面所有元素进行排序的二叉树,大顶堆的最大元素在根结点)

……

你要是使用优先级队列的话,你把元素加入到队列里面(队列会对元素进行排序,大顶堆就是单调递减),会导致元素的顺序改变了, pop 出来的的元素就不一定是滑动窗口里面真正要 pop 出来的元素。

⚠️不能使用优先级队列!

需要我们自己去实现一种队列,来解决这个问题:

单调队列: (单调队列就是维护队列里面的单调递增或者单调递减)

乍一看和优先级队列一样,其实不一样。

关键在于如何维护元素里面的单调递增和单调递减,如何加元素以及如何把这些元素pop出来,这个规则由我们自己定义。

相当于我们DIY了一种队列,维持这个队列里面的元素是单调递增或单调递减。

我们自定义队列,队列里维护的元素,就没有必要把窗口里的元素都添加进去,只需要维护有可能成为最大值的元素,放在队列的"出口"。

数组元素:1 3 -1 -3 5 3 2 1

如何维护? 模拟一下流程:

在3的前面,有比3小的弹出,

这样维护出口处是最大值,每次getMaxValue()就找出口处的元素就可以了。

动画:

11.02.gif

代码思路:

AC代码: (核心代码模式)

 class Solution {
 private:
     class MyQueue {  //单调队列(单调递减)
     public:
         deque<int> que;  //使用deque来实现单调队列
 ​
         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++) {  //先将前k的元素放进队列
             que.push(nums[i]);
         }
         result.push_back(que.front());  //result记录前k的元素的最大值
         for (int i = k; i < nums.size(); i++) {
             que.pop(nums[i - k]);
             que.push(nums[i]);
             result.push_back(que.front());
         }
         return result;
     }
 };

347.前K个高频元素

难度指数:😀😕🙁

这是用堆这种数据结构来解决的一道经典题目。

题目链接:347.前K个高频元素

主要有2个难点:

  • 如何求数组中每个元素的频率?
  • 如何对这个频率进行排序?

map , value是频率;key是元素。

堆是一种二叉树的数据结构,每加入一个元素,它去调整的话就是O(logk), 我们只维护k个元素

代码思路:

 //遍历数组,用map里面的value来统计频率
 for (int i = 0; i < nums.size(); i++) {
     map[nums[i]]++;  //key这个元素所出现的频率,对应做++,   nums[i]应该是这个key, 加上map[]就是它出现的频率
 }
 //我们应该定义一个优先级队列,应该定义一个小顶堆,  同时,我们仅仅对value进行排序
 priority_queue()

同时,我们仅仅对value进行排序,我们在队列里放的元素是Pair这种数据结构,要存key和value

key是元素,value是元素出现过的频率,要对这个键值对进行排序。

不仅要对键值对进行排序,还需要控制如何进行排序:我们选择的是小顶堆

在优先级队列里面,我们要从小到大进行排序,那么就需要自己实现一个compare()

遍历map,用优先级队列把map里面所有的元素做遍历:

 //it也是一个<key, value>键值对
 for (map != it) {
     que.push(it);  //把一个键值对加入到优先级队列里
     //优先级队列对k个元素进行排序
     if (que.size() > k) {
         que.pop();  //用小顶堆,每次pop的时候才能把最小的元素给pop走,堆里留下的就是较大的那些元素
     }
 }

题目想要先输出频率最高的元素,然后是频率次高的元素……

但是在优先级队列里面,它是先弹出频率相对较小的元素,然后才是频率较高的元素。

那么我们可以直接定义一个想要返回的数组result,然后倒序遍历数组。

 vector<int> result;
 for (int i = k - 1; i >= 0; i++) {
     result[i] = que.top().first();
     que.pop();
 }

AC代码: (核心代码模式)

 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;  //map<nums[i], 对应出现的次数>
         
         //遍历数组
         for (int i = 0; i < nums.size(); i++) {
             map[nums[i]]++;
         }
 ​
         //堆频率排序
         //定义一个小顶堆,大小为k
         priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparison> pri_que;
 ​
         //用固定大小为k的小顶堆,扫描所有频率的数值
         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();
             }
         }
 ​
         // 找出前k个高频元素,因为小顶堆先弹出的是最小的,所以倒序来输出到数组
         vector<int> result(k);
         for (int i = k - 1; i >= 0; i--) {
             result[i] = pri_que.top().first;
             pri_que.pop();
         }
         return result;
     }
 };