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

目录

150. 逆波兰表达式求值

239. 滑动窗口最大值

347.前 K 个高频元素

方法一:哈希表 + 排序

方法二:哈希表 + 小顶堆(优先队列)

补充1:C++ STL中与队列相关的容器和适配器

补充2:priority_queue(优先队列)


150. 逆波兰表达式求值

题目链接:150. 逆波兰表达式求值 - 力扣(LeetCode)

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

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

解题思路:

遇到数字则入栈;遇到运算符则取出栈顶两个数字进行计算,并将结果压入栈中。

class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<int> st;
        for(string s : tokens){
            if(s == "+" || s == "-" || s == "*" || s == "/"){
                // 运算符,出栈计算,运算结果压栈
                int num1 = st.top();st.pop();
                int num2 = st.top();st.pop();

                if(s == "+"){
                    st.push(num2 + num1);
                }else if(s == "-"){
                    st.push(num2 - num1);
                }else if(s == "*"){
                    st.push(num2 * num1);
                }else if(s == "/"){
                    st.push(num2 / num1);
                }

            }else{
                // 数字入栈
                int num = stoi(s);//将字符串转换成int
                st.push(num);
            }
        }
        return st.top();

    }
};

 

239. 滑动窗口最大值

题目链接:239. 滑动窗口最大值 - 力扣(LeetCode)

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

返回 滑动窗口中的最大值 

解题思路:

用队列模拟滑动窗口。每次窗口移动的时候,调用que.pop(滑动窗口中移除元素的数值),que.push(滑动窗口添加元素的数值),然后que.front()就返回我们要的最大值。

队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队列里的元素数值是由大到小的。

 

设计单调队列的时候,pop,和push操作要保持如下规则:

  1. pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作
  2. push(value):如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止

保持如上规则,每次窗口移动的时候,只要问que.front()就可以返回当前窗口的最大值。

class MyQueue { //单调队列(从大到小)
public:
    deque<int> que; // 使用deque来实现单调队列
    // 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。
    // 同时pop之前判断队列当前是否为空。
    void pop(int value) {
        if (!que.empty() && value == que.front()) {
            que.pop_front();
        }
    }
    // 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。
    // 这样就保持了队列里的数值是单调从大到小的了。
    void push(int value) {
        while (!que.empty() && value > que.back()) {
            que.pop_back();
        }
        que.push_back(value);

    }
    // 查询当前队列里的最大值 直接返回队列前端也就是front就可以了。
    int front() {
        return que.front();
    }
};

这样我们就用deque实现了一个单调队列,接下来解决滑动窗口最大值的问题就很简单了,直接看代码吧。

class Solution {
private:
    class MyQueue { //单调队列(从大到小)
    public:
        deque<int> que; // 使用deque来实现单调队列
        // 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。
        // 同时pop之前判断队列当前是否为空。
        void pop(int value) {
            if (!que.empty() && value == que.front()) {
                que.pop_front();
            }
        }
        // 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。
        // 这样就保持了队列里的数值是单调从大到小的了。
        void push(int value) {
            while (!que.empty() && value > que.back()) {
                que.pop_back();
            }
            que.push_back(value);

        }
        // 查询当前队列里的最大值 直接返回队列前端也就是front就可以了。
        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 个高频元素 - 力扣(LeetCode)

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

解题思路:

方法一:哈希表 + 排序

  1. 使用哈希表(或字典)来记录每个元素在数组中出现的次数。
  2. 将哈希表中的键值对(元素,频率)转换为列表或元组列表。
  3. 根据频率对列表进行降序排序。
  4. 返回排序后列表的前k个元素。
class Solution {
public:
    vector<int> topKFrequent(vector<int>& nums, int k) {
        map<int,int> freqMap;
        // 统计每个元素的出现频率
        for(int num: nums){
            freqMap[num]++;
        }

        // 将map中的元素转移到vector中,每个元素是一个pair<int, int>,表示值和频率 
        vector<pair<int,int>> freqVec(freqMap.begin(),freqMap.end());

        // 根据频率对vector进行排序,如果频率相同,则按值排序
        sort(freqVec.begin(),freqVec.end(),[](const pair<int,int>& a,const pair<int,int>& b){
            if (a.second != b.second) {  
                return a.second > b.second; // 按频率降序  
            }  
            return a.first < b.first; // 如果频率相同,则按值升序
        });

        // 提取前k个元素的值
        vector<int> result;
        for(int i = 0; i < k ; i++){
            result.push_back(freqVec[i].first);
        }
        return result;

    }
};

时间复杂度: O(nlogn)

优化代码:

不需要对整个数组进行排序,只需要维护k个有序的序列,所以使用优先级队列是最优的。

方法二:哈希表 + 小顶堆(优先队列)

  1. 使用哈希表来记录每个元素的出现频率。
  2. 创建一个大小为k的小顶堆,堆中存储的是(频率,元素)的元组,其中堆是根据频率进行排序的。
  3. 遍历哈希表中的每个元素,如果堆未满,则将当前元素及其频率添加到堆中;如果堆已满,则弹出堆顶元素,将当前元素加入堆中。
  4. 遍历结束后,堆中剩下的就是频率最高的前k个元素(根据元素值)。

问题:为什么采用小顶堆?而不是大顶堆?

我们维护的是一个大小为k的堆,每次添加元素的时候(超出k个),都会将堆顶元素弹出。

小顶堆每次将最小的元素弹出,最后小顶堆里积累的是k个最大元素。

大顶堆每次将最大的元素弹出,最后大顶堆里积累的是k个最小元素。

class Solution {
public:
    // 自定义的比较器
    class mycompare {
    public:
        bool operator()(const pair<int, int>& a, const pair<int, int>& b) {
            return b.second < a.second;
        }
    };

    vector<int> topKFrequent(vector<int>& nums, int k) {
        map<int, int> freqMap;
        // 统计元素出现频率
        for (int num : nums) {
            freqMap[num]++;
        }

        // 定义一个小顶堆,大小为k
        priority_queue<pair<int, int>, vector<pair<int, int>>, mycompare> pri_que;

        // 填充堆,但保持其大小不超过k
        for (pair<int, int> p : freqMap) {
            pri_que.push(p);

            if (pri_que.size() > k) {
                pri_que.pop();// 如果堆的大小超过了k,就移除堆顶元素(频率最小的元素)
            }
        }

        // 提取堆中的前k个元素
        vector<int> result;
        for (int i = 0; i < k; i++) {
            result.push_back(pri_que.top().first);
            pri_que.pop();
        }
        return result;
    }
};

时间复杂度: O(nlogk)

补充1:C++ STL中与队列相关的容器和适配器

  1. std::queue
    这是最典型的队列实现。它是一个模板类,通常内部使用std::deque作为底层容器,std::queue提供了基本的队列操作,如push()(在队尾添加元素)、pop()(移除队首元素)、front(访问队首元素)和back(访问队尾元素,但注意,队列通常只从一端添加元素,从另一端移除元素)。

  2. std::priority_queue(优先队列):
    std::priority_queue是一个最大堆,默认情况下,它会根据元素的“大于”运算符来排序元素(即,顶部元素是队列中最大的元素)。你可以通过提供自定义的比较函数或类来改变这个行为,以支持最小堆或其他排序准则。

  3. std::deque(双端队列):
    提供了在两端快速插入和删除元素的能力

补充2:priority_queue(优先队列)

  • 什么是优先级队列呢?

其实就是一个披着队列外衣的堆,因为优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。

  •  优先级队列内部元素是如何有序排列的呢?

默认利用max-heap(大顶堆)完成对元素的排序,这个大顶堆是以vector为表现形式的complete binary tree(完全二叉树)。

堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。

当你定义 priority_queue 时,它的模板参数通常如下所示:

template <  
    class T,  
    class Container = std::vector<T>,  
    class Compare = std::less<T>  
> class priority_queue;
  • T 是存储元素的类型。
  • Container 是用于存储元素的底层容器类型。默认情况下,它是 std::vector<T>
  • Compare 是一个用于比较元素的函数对象类型。默认情况下,它是 std::less<T>,这会导致 priority_queue 成为一个最大堆。然而,你可以通过提供一个自定义的比较类(如 mycomparison)来改变这一行为。

如何根据 Compare 对堆进行维护

  1. 插入操作
    • 当你向 std::priority_queue 插入一个新元素时,该元素首先被放置在堆的末尾(即底层容器的末尾)。
    • 然后,堆调整算法(通常是上浮调整,也称为堆化或筛选)被触发,以确保新元素根据比较函数的定义被正确地放置在堆中的位置。如果比较函数定义了最大堆(即父节点值大于子节点值),则新元素会沿着堆向上移动,直到它位于一个位置,使得它与父节点的比较关系不违反堆的性质。
  2. 删除操作(特别是删除堆顶元素):
    • 当你从 std::priority_queue 中删除堆顶元素时,堆顶元素(即最大或最小元素,取决于堆的类型和比较函数的定义)被移除。
    • 接着,堆的最后一个元素被移动到堆顶的位置。
    • 然后,堆调整算法(通常是下沉调整)被触发,以确保新的堆顶元素根据比较函数的定义被正确地放置在堆中的位置。如果比较函数定义了最大堆,则新堆顶元素会沿着堆向下移动,直到它位于一个位置,使得它与子节点的比较关系不违反堆的性质。

Compare 的作用

  • 比较函数或对象Compare 是一个定义了元素之间比较逻辑的函数对象。它通常有两个参数,表示要比较的两个元素,并返回一个布尔值来指示第一个元素是否应该在堆中排在第二个元素之前。
  • 当你需要快速访问最大元素时,使用大顶堆(< 比较器)。
  • 当你需要快速访问最小元素时,使用小顶堆(> 比较器)。

我的记忆tip:

compare(a,b) a记作第一个元素,b记作第二个元素。当返回值为true时,会进行上浮调整。

把b看作插入元素,a看作堆中元素。

对于大顶堆,最大值在上面,若堆中值(a)<  插入值(b)时,我们就要把b上浮。所以,“ < ”

对于小顶堆,最小值在上面,若堆中值(a)>  插入值(b)时,我们就要把b上浮。所以,“ > ”

不一定正确,是为了方便记忆。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值