代码随想录算法训练营|第五章栈与队列02|力扣150逆波兰表达式 力扣239滑动窗口最大值

力扣150 逆波兰表达式求值

题目链接/文章讲解/视频讲解:​​​​​​代码随想录

思路:遍历字符串,遇到数字就入栈,遇到运算符号取出栈中两个数字计算并将结果入栈。这道题目主要理解逆波兰表达式的特点就很好解题。

逆波兰表达式(Reverse Polish Notation, RPN)是一种将运算符置于操作数之后的数学和计算机科学表达方式。它也被称为后缀表达式,与传统的中缀表达式(即运算符在操作数之间)相对应。

在逆波兰表达式中,每个运算符跟随其相关的操作数。这种表示法消除了括号的需求,并且使得表达式的计算顺序更加明确。例如,将中缀表达式 "3 + 4 * 5" 转换为逆波兰表达式后变为 "3 4 5 * +"。在这个逆波兰表达式中:

  • 数字 "3" 和 "4" 直接被输出。
  • "5" 跟随在操作符 "*" 后面,因为它是操作符 "*" 的操作数。
  • 最后,操作符 "+" 跟随在结果 "4 * 5" 的后面,因为它是操作数 "3" 和 "4 * 5" 的操作符。

逆波兰表达式的主要优点包括:

  1. 无需括号:由于操作符在操作数后面,因此不需要使用括号来改变计算顺序。
  2. 简单的计算顺序:计算顺序从左到右进行,每次操作符遇到足够的操作数时立即计算。

逆波兰表达式通常用于栈数据结构的计算过程中,其中操作符遇到操作数时直接进行计算,并将结果推入栈中,直到整个表达式被处理完毕。

例如,对于逆波兰表达式 "3 4 + 5 *" 的计算过程如下:

  1. 将 "3" 和 "4" 推入栈中。
  2. 遇到 "+" 操作符,弹出栈顶的两个元素(4 和 3),计算 3 + 4,结果 7 推入栈中。
  3. 将 "5" 推入栈中。
  4. 遇到 "*" 操作符,弹出栈顶的两个元素(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定义为Solutionprivate成员类,你限定了MyQueue的作用域仅限于Solution类内部。这种做法确保了MyQueue只能用于Solution类内部的逻辑,而不能被其他类或函数直接访问。这有助于保持类之间的独立性和一致性。

3. 组织和结构

MyQueue嵌套在Solution类中,还可以更好地组织代码,使其结构更清晰。MyQueueSolution的一个内部实现细节,因此将其嵌套在Solution类中能够将相关功能聚合在一起。这种方式有助于维护代码的逻辑结构,并使得MyQueueSolution的紧密关系更加明确。

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

迭代器和解引用

  1. 迭代器(it

    • it 是一个迭代器,用于指向容器中的某个元素。
    • 迭代器类似于指针,允许访问和遍历容器中的元素。
  2. 解引用(*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(),表示容器的结束位置。
  • 17
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值