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

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

有效的运算符包括 + , - , * , / 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。
说明:
整数除法只保留整数部分。 给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。
输入: [“2”, “1”, “+”, “3”, " * "]
输出: 9
解释: 该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9

什么是逆波兰表达式

逆波兰表达式:是一种后缀表达式,所谓后缀就是指运算符写在后面。
平常使用的算式则是一种中缀表达式,如 ( 1 + 2 ) * ( 3 + 4 ) 。
该算式的逆波兰表达式写法为 ( ( 1 2 + ) ( 3 4 + ) * ) 。
逆波兰表达式主要有以下两个优点:
去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。
适合用栈操作运算:遇到数字则入栈;遇到运算符则取出栈顶两个数字进行计算,并将结果压入栈中。
在这里插入图片描述

代码实现

代码思路

  1. 新建一个栈
  2. 进入for循环,进入if判断,如果为符号,那么就从栈中取出两个头元素,做对应的运算,并将运算结果加入中栈中
  3. 否则就是另一种情况遇到数字,那就将对应的数字加入栈中
  4. 最后跳出循环,将栈中最后一个数字返回
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]));
            }

        }

        long long  result = st.top();
        st.pop();

        return result;

    }
};

注意点

stoll 是 C++ 标准库中的一个函数,用于将字符串转换为长整型(long long)整数。stoll 是 std::string 类的一部分,定义在 头文件中。
注意题目要求,在字符串判断的时候只能用" ",长度为1的字符串也是字符串,而单引号’ ’ 用于表示单个字符。

239. 滑动窗口最大值

题干

给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。返回滑动窗口中的最大值。
进阶:你能在线性时间复杂度内解决此题吗?
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]

题解思路

如果使用暴力方法,遍历一遍的过程中每次从窗口中再找到最大的数值,这样很明显是O(n × k)的算法。
另一种思路就是用一个队列,在遍历数组的过程中新加入一个元素,我们就把最先放进来的元素移除,始终保持队列中元素个数为k,每循环一次我们求一下此时队列中的最大值,放入新建的数组中
在这里插入图片描述
这里需要使用一个单调队列来实现上述的功能:
我们需要在一个deque双端队列的基础上完成三个功能:

  1. pop功能(此函数需要有一个参数,即要pop的元素)
  • 首先要判断这个队列不为空,不能对空队列操作,并且我们要pop的这个元素如果等于这个队列最前面的元素(即队列中最大的元素),那我们就将最前面的一个元素pop,其他的情况在push函数中实现,
  1. push功能,从队尾push,确保队列的最大元素在队头
  • 判断此队列不为空,并且push进去的元素如果大于队尾的元素,那么就将队尾的元素pop,直到队头的元素最大
  1. Get_Maxval功能
  • 获取队头元素,这就是队列中的最大元素

在主函数中基于此队列,调用相应的功能完成任务
4. 新建单调队列
5. 新建一个数组用于存放结果
6. 一个for循环将前k个元素放入队列,并记录前k个元素的最大值
7. 在进入一个for循环,此时循环索引从k开始,移除窗口最前面的元素,加入窗口后面的元素,将最大值加入结果数组中

实现代码

class Solution {
    private:
    class Myque
    {
        public:
            deque<int>que;
            void pop(int val)
            {
                if(!que.empty() && val == que.front())
                {
                    que.pop_front();
                }
            }
            void push(int val)
            {
                while(!que.empty() && que.back()<val)
                {
                    
                    que.pop_back();
                
                }
		//此处不要再画蛇添足加一个if判断,if(que.back() >= val),再插入元素,因为上一步有可能将队列中的所有元素都弹出,
		//此时再对队列中的末尾元素判断就会报错
                que.push_back(val);
 
            }

            int GetMaxval(void)
            {
                return que.front();
            }
    };

public:

    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        Myque que;
        vector<int>result;
        for(int i=0;i<k;i++)
        {
            que.push(nums[i]);
        }
        result.push_back(que.GetMaxval());
        for(int i=k;i<nums.size();i++)
        {
            que.pop(nums[i-k]);
            que.push(nums[i]);
            result.push_back(que.GetMaxval());
        }
        return result;
    }
};

347.前 K 个高频元素

题干

给定一个非空的整数数组,返回其中出现频率前 k 高的元素。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]

题解思路

  1. 要统计元素出现频率
  2. 对频率排序
  3. 找出前K个高频元素

首先统计元素出现的频率,这一类的问题可以使用map来进行统计。
然后是对频率进行排序,这可以使用一种容器适配器就是优先级队列。

在C++中,优先级队列(Priority Queue)是一种数据结构,它类似于常规队列,但每个元素都有一个与之相关的“优先级”,具有较高优先级的元素会先被处理。优先级队列通常使用堆(heap)来实现。

#include <iostream>
#include <queue>
int main() {
    // 定义一个整数类型的优先级队列(默认是大顶堆)
    std::priority_queue<int> pq;
    // 向队列中插入元素
    pq.push(5);
    pq.push(10);
    pq.push(3);

    // 输出并移除队列中的最大元素
    std::cout << "最高优先级元素: " << pq.top() << std::endl;
    pq.pop();

    // 再次输出最高优先级元素
    std::cout << "下一个最高优先级元素: " << pq.top() << std::endl;
    pq.pop();

    return 0;
    }

常用操作

  1. push( value): 将元素value插入到优先级队列中。
  2. pop(): 移除优先级队列中的最高优先级元素。
  3. top(): 获取优先级队列中的最高优先级元素(不移除)。
  4. empty(): 检查优先级队列是否为空。
  5. size(): 返回优先级队列中的元素数量。

优先级队列其实就是一个披着队列外衣的堆,因为优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。
而且优先级队列内部元素是自动依照元素的权值排列。那么它是如何有序排列的呢?
缺省情况下priority_queue利用max-heap(大顶堆)完成对元素的排序,这个大顶堆是以vector为表现形式的complete binary tree(完全二叉树)。

堆(Heap)的基础概念

  1. 动态内存分配:
  • 堆是一块被操作系统管理的内存区域,专门用于在运行时动态分配内存。程序可以在运行时请求(allocate)和释放(free)堆内存。在 C 语言中,堆内存通常通过库函数 malloc()、calloc()、realloc() 来分配,通过 free() 来释放。
  1. 内存管理:
  • 堆内存由程序员手动管理。程序员需要明确什么时候分配和释放内存,以避免内存泄漏(忘记释放内存)或野指针错误(释放后继续使用该内存)。
    与自动管理的栈不同,堆上的内存不会在函数退出时自动释放,必须显式调用 free()。
  1. 灵活性
  • 堆上的内存分配是非常灵活的。可以分配任意大小的内存块,而且内存的生存期可以跨越函数调用,直到显式释放。
  1. 性能与碎片化:
  • 堆内存的分配和释放相对较慢,因为操作系统需要进行复杂的内存管理,确保已分配和未分配内存块之间的有效管理。
  • 长时间运行的程序可能会导致堆内存碎片化,即大量的小块空闲内存分散在堆中,使得即使有足够的空闲内存,仍然无法分配一个大块的连续内存。
    在这里插入图片描述

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

所以大顶堆(堆头是最大元素),小顶堆(堆头是最小元素),从小到大排就是小顶堆,从大到小排就是大顶堆。
本题我们就要使用优先级队列来对部分频率进行排序。

为什么不用快排呢,使用快排要将map转换为vector的结构,然后对整个数组进行排序, 而这种场景下,我们其实只需要维护k个有序的序列就可以了,所以使用优先级队列是最优的。

那么到底是使用大顶堆还是小顶堆?
如果使用大顶堆的话,在每次移动更新大顶堆的时候,每次弹出都把最大的元素弹出去了,那么怎么保留下来前K个高频元素呢。
而且使用大顶堆就要把所有元素都进行排序,那能不能只排序k个元素呢?

所以要用小顶堆,因为要统计最大前k个元素,只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素。

寻找前k个最大元素流程如图所示:(图中的频率只有三个,所以正好构成一个大小为3的小顶堆,如果频率更多一些,则用这个小顶堆进行扫描)

在这里插入图片描述

C++中pair的用法

#include <utility> // 包含pair定义
pair 是一个模板类,用于将两个数据组合在一起形成一个简单的二元组。它可以包含两个不同类型的对象,并且在许多情况下被用来处理成对的数据,如关联数组中的键值对、返回多个值、存储坐标等。
使用方法

  1. 创建pair对象:
直接初始化
pair<int, string> p1(1, "one");

使用make_pair
pair<int, string> p2 = make_pair(2, "two");
  1. 访问pair的成员
    通过 first 和 second 成员来访问 pair 的元素。
cout << "First: " << p1.first << ", Second: " << p1.second << endl;
  1. 使用pair在容器中:
    pair 经常与STL容器如 vector、map 等结合使用。
    例如在 map 中,每个键值对实际上是一个 pair。
map<int, string> m;
m.insert(pair<int, string>(1, "one"));
m.insert(make_pair(2, "two"));

for (const auto& entry : m) {
    cout << entry.first << ": " << entry.second << endl;
}

  1. 比较pair:
    pair 支持字典序比较,先比较 first 元素,如果 first 相等,再比较 second 元素。
pair<int, int> p1(1, 5);
pair<int, int> p2(1, 6);

if (p1 < p2) {
    cout << "p1 is less than p2" << endl;
}

C++中优先级队列的定义和使用方法

  1. 头文件
    要使用 priority_queue,你需要包含<queue> 头文件:
  2. 定义
    priority_queue 是一个模板类,默认使用 std::vector 作为底层容器,使用 std::less 作为比较器(大顶堆),默认最大元素在队列头的位置。你可以使用其他容器和比较器来改变其行为。
    定义时其参数模版为:priority_queue<T, Container, Compare>
    T:存储在队列中的元素类型。
    Container:底层容器的类型,默认使用 std::vector。
    Compare:用于比较元素的比较器(函数对象或函数指针),决定优先级队列的排序规则。
#include <iostream>
#include <queue>
#include <vector>

using namespace std;

int main() {
    //默认使用最大堆
    priority_queue<int> pq;

    // 插入元素
    pq.push(10);
    pq.push(30);
    pq.push(20);
    
    // 访问和删除最大元素
    while (!pq.empty()) {
        cout << pq.top() << " ";  // 输出最大元素
        pq.pop();                 // 移除最大元素
    }
    cout << endl;

    return 0;
}

  1. 自定义比较器
    可以自定义比较器以实现最小堆或者其他自定义的优先级规则。例如,以下代码展示了如何使用最小堆:
    return a > b;,表示小的元素在前(以右侧为队头),即实现小顶堆(最小堆)
    return a < b;,表示大的元素在前(以右侧为队头),即实现大顶堆(最大堆)(默认状态)
#include <iostream>
#include <queue>
#include <vector>

using namespace std;

// 自定义比较器
//在这个比较器中,a > b 返回 true 表示 a 应该排在 b 的后面(即优先级低)
struct Compare {
    bool operator()(int a, int b) {
        return a > b; // 小的元素优先级高
    }
};

int main() {
    // 使用自定义比较器,创建最小堆
    priority_queue<int, vector<int>, Compare> pq;

    // 插入元素
    pq.push(10);
    pq.push(30);
    pq.push(20);

    // 访问和删除最小元素
    while (!pq.empty()) {
        cout << pq.top() << " ";  // 输出最小元素
        pq.pop();                 // 移除最小元素
    }
    cout << endl;

    return 0;
}

  1. 自定义数据类型
    可以使用 priority_queue 存储自定义数据类型,通过定义比较器来实现优先级队列。例如,以下代码展示了如何存储自定义结构体:
#include <iostream>
#include <queue>
#include <vector>

using namespace std;

// 自定义数据结构
struct Person {
    string name;
    int age;
};

// 自定义比较器
struct ComparePerson {
    bool operator()(const Person& p1, const Person& p2) {
        return p1.age < p2.age; // 年龄大的优先级高
    }
};

int main() {
    // 使用自定义数据结构和比较器
    priority_queue<Person, vector<Person>, ComparePerson> pq;

    // 插入元素
    pq.push({"Alice", 30});
    pq.push({"Bob", 25});
    pq.push({"Charlie", 35});

    // 访问和删除优先级最高的元素
    while (!pq.empty()) {
        Person p = pq.top();
        cout << p.name << " (" << p.age << ") ";
        pq.pop();
    }
    cout << endl;

    return 0;
}

创建并初始化一个迭代器(iterator)

unordered_map<int, int> map;
unordered_map<int, int>::iterator it = map.begin();
创建一个 unordered_map<int, int> 类型的迭代器 it,并将其初始化为 map 中第一个元素的位置。
通过 it,你可以访问或遍历 unordered_map 中的元素。例如,it->first访问键,it->second 访问值。

整体代码思路

  1. 新建一个类,新建一个bool类型的函数作为新的比较器,实现最小堆(小顶堆)
  2. 进入目标函数,新建一个unordered_map,遍历数组,将数组的元素值作为key,元素出现的次数作为value,存入map中
  3. 新建一个优先级队列,初始化新的数据类型和比较器
  4. 进入for循环,用迭代器遍历map的元素,每遍历一个元素,将其push进入优先级队列中,并判断优先级队列的长度是否超过k值,如果超过了就pop出头元素(我们自定义的优先级队列中最小的元素在队头)
  5. 此时优先级队列中已经存储了数组中出现次数最多的前k个元素和其出现的次数(作为一个pair存储在优先级队列中),
  6. 新建一个向量result,进入一个for循环,从后往前遍历优先级队列中的元素,result[i] = pri_que.top().first;,随后将其pop
  7. 返回result,

注意点

  1. 自定义比较器类用于 priority_queue 时,需要实现 operator(),而不是定义一个名为 comparison 的成员函数。operator() 是一个函数调用运算符,使得自定义比较器对象可以像函数一样使用。
  2. priority_queue 的模板参数列表应该使用尖括号 <>,而不是圆括号 ()。
  3. 迭代器 it 未初始化
    在遍历 unordered_map 时,it 迭代器没有初始化为 map.begin(),导致访问未定义行为。
  4. result 向量未初始化
    result 向量在访问时未初始化大小,直接使用下标访问会导致越界访问。
  5. pri_que.top().second 错误
    pri_que.top() 返回的是 pair<int,int>,second 是频率,而需要的是元素值 first。

整体代码如下

#include <vector>
#include <queue>
#include <unordered_map>
#include <utility>  // for std::pair
#include <iostream>
using namespace std;


class Solution {
public:
    class mycomparison
    {
        public:
        bool operator()(const pair<int,int> &t1,const pair<int,int> &t2)		//易错点1
        {
            return t1.second > t2.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]]++; //map中first为元素值,second位元素出现的次数
        }
        //新建一个优先级队列,并初始化
        priority_queue<pair<int,int>,vector<pair<int,int>>,mycomparison>pri_que;	//易错点2
         
        for(unordered_map<int,int>::iterator it=map.begin();it!=map.end();it++)		//易错点3
        {
            pri_que.push(*it);
            if(pri_que.size()>k)
            {
                pri_que.pop();
            }
        }
        vector<int>result(k);		//易错点4
        for(int i=k-1;i>=0;i--)
        {
            result[i]=pri_que.top().first;	//易错点5
            pri_que.pop();
        }

    return result;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值