第五章 栈与队列part02

逆波兰表达式求值

题目描述

根据 逆波兰表示法,求表达式的值。

有效的运算符包括 + , - , * , / 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。

说明:

整数除法只保留整数部分。 给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。

示例 1:

  • 输入: [“2”, “1”, “+”, “3”, " * "]
  • 输出: 9
  • 解释: 该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9

示例 2:

  • 输入: [“4”, “13”, “5”, “/”, “+”]
  • 输出: 6
  • 解释: 该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6

示例 3:

  • 输入: [“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  
    

逆波兰表达式:是一种后缀表达式,所谓后缀就是指运算符写在后面。

平常使用的算式则是一种中缀表达式,如 ( 1 + 2 ) * ( 3 + 4 ) 。

该算式的逆波兰表达式写法为 ( ( 1 2 + ) ( 3 4 + ) * ) 。

逆波兰表达式主要有以下两个优点:

  • 去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。
  • 适合用栈操作运算:遇到数字则入栈;遇到运算符则取出栈顶两个数字进行计算,并将结果压入栈中

解题思路

总体思路

遍历token字符串数组中的所有字符串,如果是数字就将其转换为long long 类型压入栈中,如果是运算符,则取出栈顶的前两个元素进行对应算术运算后,再将结果重新压入到栈中,最后返回栈顶元素即可

时空分析

  • 时间复杂度: O(n)
  • 空间复杂度: O(n)

代码实现

测试地址:

class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        // 使用栈来存放整数
        stack<long long> st;
  
        // 遍历每个token,token可以是数字或者是运算符
        for (string t : tokens) {
            // 检查当前token是否是运算符
            if (t == "+" || t == "-" || t == "*" || t == "/") {
                // 从栈顶取出两个数字进行运算
                long long num1 = st.top(); // 第一个数字
                st.pop(); // 弹出第一个数字
                long long num2 = st.top(); // 第二个数字
                st.pop(); // 弹出第二个数字
          
                // 根据运算符执行计算,并将结果压回栈中
                if (t == "+") {
                    st.push(num2 + num1);
                } else if (t == "-") {
                    st.push(num2 - num1);
                } else if (t == "*") {
                    st.push(num2 * num1);
                } else if (t == "/") {
                    st.push(num2 / num1);
                }
            } else {
                // 如果是数字,则将其转换为长整型后压入栈中
                st.push(stoll(t));
            }
        }
        // 最后栈顶的元素即为整个表达式的结果
        int res = st.top(); // 获取结果
        st.pop(); // 弹出结果
        return res; // 返回结果
    }
};

滑动窗口最大值

题目描述

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

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

进阶:

你能在线性时间复杂度内解决此题吗?

提示:

  • 1 <= nums.length <= 10^5
  • -10^4 <= nums[i] <= 10^4
  • 1 <= k <= nums.length

解题思路

总体思路

可以使用一个双端队列(deque)来实现高效地找到滑动窗口中的最大值。这个双端队列被设计为一个特殊的队列,即MyQueue,它在队列的前端存储当前窗口的最大值,并且在队列的后端存储可能成为下一个最大值的元素。

具体步骤如下:

  1. 初始化窗口:首先,将前k个元素加入到MyQueue中。在加入每个元素时,会从队列的后端开始比较,如果新加入的元素比队列后端的元素大,则将队列后端的元素弹出,直到队列为空或者队列后端的元素比新元素大。这样,队列的前端始终保持当前窗口的最大值。
  2. 滑动窗口:对于剩余的元素,每次滑动窗口时,先检查队列的前端元素是否与当前窗口的第一个元素相同,如果相同,则从队列前端弹出该元素,因为该元素已经不在当前窗口中了。然后,将新的元素加入到队列中,同样遵循上述规则,确保队列前端始终是最大值。
  3. 记录最大值:在每次滑动窗口后,队列的前端元素就是当前窗口的最大值,将其加入到结果数组result中。
  4. 返回结果:遍历完所有元素后,返回存储了所有窗口最大值的结果数组。

这种方法的关键在于,MyQueue队列中始终保持了可能成为最大值的元素,并且这些元素是按照从大到小的顺序排列的。这样,每次只需要检查队列的前端元素,就可以在O(1)时间内得到当前窗口的最大值。同时,由于队列中只存储了可能成为最大值的元素,因此队列的大小通常小于或等于窗口的大小,这使得整个算法的复杂度保持在O(n),其中n是数组的长度。

时空分析

  • 时间复杂度: O(n)
  • 空间复杂度: O(k)

代码实现

测试地址:https://leetcode.cn/problems/sliding-window-maximum/

class Solution {
private:
    // 内部类MyQueue用于在O(1)时间内获取滑动窗口的最大值
    class MyQueue {
    public:
        deque<int> dq; // 双端队列,用于存储当前窗口的最大值及其候选值

        // 当窗口滑动时,需要从队列front弹出元素,但仅当该元素等于窗口的前端元素时才弹出
        void pop(int value) {
            if (!dq.empty() && value == dq.front()) {
                dq.pop_front();
            }
        }

        // 向队列中添加元素,若添加的值大于队列尾部元素,则弹出队列尾部元素,以保证队列头部总是最大值
        void push(int value) {
            while (!dq.empty() && value > dq.back()) {
                dq.pop_back();
            }
            dq.push_back(value);
        }

        // 获取队列中的头部元素,即为当前窗口的最大值
        int front() { return dq.front(); }
    };

public:
    // 主方法用于计算并返回每个滑动窗口的最大值
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        MyQueue dq; // 实例化内部MyQueue类
        vector<int> result; // 用于存储最终结果的数组

        // 首先处理前k个元素,初始化滑动窗口
        for (int i = 0; i < k; i++) {
            dq.push(nums[i]);
        }
        result.push_back(dq.front()); // 将当前窗口的最大值加入结果

        // 遍历数组,继续处理剩余元素
        for (int i = k; i < nums.size(); i++) {
            dq.pop(nums[i - k]); // 移除滑动窗口最左侧的元素
            dq.push(nums[i]); // 添加当前元素到滑动窗口
            result.push_back(dq.front()); // 将当前窗口的最大值加入结果
        }
        return result; // 返回结果数组
    }
};

前 K 个高频元素

题目描述

给定一个非空的整数数组,返回其中出现频率前 k 高的元素。

示例 1:

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

示例 2:

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

提示:

  • 你可以假设给定的 k 总是合理的,且 1 ≤ k ≤ 数组中不相同的元素的个数。
  • 你的算法的时间复杂度必须优于 O ( n log ⁡ n ) O(n \log n) O(nlogn) , n 是数组的大小。
  • 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的。
  • 你可以按任意顺序返回答案。

解题思路

总体思路

  • 通过分析题意可以得知,我们需要计算某个数值出现的次数频率,因为涉及到两个数值的存储,所以可以定义哈希表来存储元素以及该元素所出现的频率
  • 当得到一张含有数组所有元素以及对应次数的表以后,因为我们需要取出次数出现最多的元素,因此可以考虑采用快速排序的方式,时间复杂度为nlogn,那么有没有效率更高的方法呢?
  • 我们只需要取出前k个高频元素,所以可以采用小根堆来维护前k个元素,当小跟堆中的元素大于k的时候,将最小的元素弹出
  • 倒序返回小根堆中的元素即可

时空分析

  • 时间复杂度: O(nlogk)
  • 空间复杂度: O(n)

代码实现

测试地址:https://leetcode.cn/problems/top-k-frequent-elements/description/

class Solution {
public:
    // 定义一个比较器,用于创建最小堆
    class mycomparison {
    public:
        // 比较两个pair,根据second值决定优先级
        // 如果lhs的second大于rhs的second,则lhs的优先级低
        bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) {
            return lhs.second > rhs.second;
        }
    };

    // 主函数,用于找出数组中出现频率最高的前k个元素
    vector<int> topKFrequent(vector<int>& nums, int k) {
        // 使用哈希表统计每个元素的出现频率
        unordered_map<int, int> map;
        for (int i = 0; i < nums.size(); i++) {
            map[nums[i]]++;
        }

        // 创建一个最小堆,使用自定义的比较器
		// pair<int, int> 元素类型
		// vector<pair<int, int> 存储元素的容器
		// mycomparison 自定义比较器
        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,移除堆顶元素
            }
        }

        // 创建结果数组,用于存储前k个高频元素
        vector<int> result(k);

        // 从最小堆中提取元素,由于是最小堆,需要反向填充结果数组
        for (int i = k - 1; i >= 0; i--) {
            result[i] = pri_que.top().first; // 获取元素值
            pri_que.pop(); // 从堆中移除元素
        }

        // 返回结果数组
        return result;
    }
};

  • 27
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值