算法学习记录~2023.4.26~栈与队列Day2~239.滑动窗口最大值 & 347.前K个高频元素 & 42.接雨水

239.滑动窗口最大值

题目链接

力扣题目链接

思路1

生成一个单调递减的队列

  1. 队列的最前端存储窗口范围内最大值的下标
  2. (当队列不为空时)如果新元素大于队尾(最小值),则把此时队尾pop掉,然后再比较前一个,直到新元素成为新的队尾,然后插入,这样就能找到新的下一元素对应的位置,并且可以把其他更小的没有用的元素删除掉
  3. 队列中的元素必须始终在窗口范围内

代码1

#include<bits/stdc++.h>

using namespace std;

//leetcode submit region begin(Prohibit modification and deletion)
class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        if ( k == 0 )           //这个的作用是什么?
            return {};
        deque<int> quexb;       //定义从大到小单调递减的队列,此处记录下标
        vector<int> result;     //用于存储每一个滑动窗口的最大值
        for (int i = 0; i < k; i++ ){       //先初始化前k个元素进队列
            //(当队列不为空时)如果新元素大于队尾(最小值),则把此时队尾pop掉,然后再比较前一个,直到新元素成为新的队尾
            //这样可以保证队列的单调性,且可以将所有比新元素小的(已经没有用了)都丢弃掉
            while ( !quexb.empty() && nums[i] > nums[quexb.back()] ){
                quexb.pop_back();
            }
            quexb.push_back( i );                       //找到对应位置后,就把元素插入
        }
        result.push_back( nums[quexb.front()] );    //前k个全部输入完毕后,记录最大值
        for ( int i = k; i <nums.size(); i++ ){
            if ( !quexb.empty() && quexb.front() <= i - k )     //如果此时最大元素已经不在区间内则pop掉
                quexb.pop_front();
            while ( !quexb.empty() && nums[i] > nums[quexb.back()] ){  //同初始化一样,找到自己对应位置并抛弃比它小的无用元素
                quexb.pop_back();
            }
            quexb.push_back( i );                       //找到对应位置后,就把元素插入
            result.push_back( nums[quexb.front()] );    //记录最大值
        }
        return result;
    }
};
//leetcode submit region end(Prohibit modification and deletion)

思路2

和思路1一致,只不过队列并不储存下标而是直接储存数据内容,其实只有涉及一些条件判断时才有一些区别。同时这个写法更符合能够复用的原则,更加规范

代码2

#include<bits/stdc++.h>
using namespace std;

class Solution{

private:
    class Queue {
    public:
        deque<int> que;

        //用于弹出队列中的第一个元素。如果向后遍历过程中出区间的正好是最大值,那么应当把它pop掉
        // 如果不是则不影响最大值的获取
        void pop (int i){
            if ( !que.empty() && i == que.front() ){
                que.pop_front();
            }
        }

        //对于新的需要push进去的值,从最后(最小)处开始比较,只要新值不是最小的就把队尾pop掉,直到找到它应在的位置
        void push (int j){
            while ( !que.empty() && j > que.back() ){
                que.pop_back();
            }
            que.push_back( j );
        }

        //队首即为最大值
        int max() {
            return que.front();
        };
    };

public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        Queue que;
        vector<int> result;
        for ( int i = 0; i < k; i++ ){      //初始化前k个值
            que.push( nums[i] );
        }
        result.push_back(que.max());  //初始化完前k个值后获取到的最大值

        for ( int i = k; i < nums.size(); i++ ){
            que.pop( nums[i-k]);
            que.push( nums[i] );
            result.push_back( que.max() );      //前k个值以后每个新滑动窗口都会有一个新的最大值,都需要存储
        }
        return result;
    }
};

总结

两种方式其实思路一致,就是具体写法有细微差别。
最暴力的解法很好考虑,就是嵌套两层循环,复杂度为O(n∗k) 。
本题巧妙解法的思路是通过维护滑动窗口中的元素,每进入一个新元素只对其进行处理,对前k-1个元素充分利用上一次的处理结果,同时根据题目要求将其构造成单调队列以方便直接获取题目要求的最值。
当一个新元素进入时如何找到它应该在的位置,也就是如何去构造满足要求的单调队列是解题关键


347.前K个高频元素

题目链接

力扣题目链接

相关知识

涉及知识点:优先队列(最小堆/最大堆)

本题题解较详细版
堆排序(浅谈大顶堆与小顶堆)

priority_queue<Type, Container, Functional>;
  • Type:要存放的数据类型
  • Container:实现底层堆的容器,必须是数组实现的容器,如vector、deque
  • Functional:比较方式/比较函数/优先级

默认容器是vector,默认的比较方式是大顶堆less
举例

//小顶堆
priority_queue <int,vector<int>,greater<int> > q;
//大顶堆
priority_queue <int,vector<int>,less<int> >q;
//默认大顶堆
priority_queue<int> a;
//使用pair为类型的默认优先队列
priority_queue<pair<int, int> > a;
pair<int, int> b(1, 2);
pair<int, int> c(1, 3);
pair<int, int> d(2, 5);
a.push(d);
a.push(c);
a.push(b);
while (!a.empty()) 
{
   cout << a.top().first << ' ' << a.top().second << '\n';
   a.pop();
}
//输出结果为:
2 5
1 3
1 2

当数据类型并不是基本数据类型,而是自定义的数据类型时,就不能用greater或less的比较方式了,而是需要自定义比较方式

  1. 重载运算符
  2. 仿函数

比如假设数据类型为自定义的水果

struct fruit
{
	string name;
	int price;
};

如果使用重载运算符的方式,比如重载“<“

  • 希望水果价格高为优先级高
//大顶堆
struct fruit
{
	string name;
	int price;
	friend bool operator < (fruit f1,fruit f2)
	{
		return f1.peice < f2.price;
	}
};
  • 希望水果价格低为优先级高
//小顶堆
struct fruit
{
	string name;
	int price;
	friend bool operator < (fruit f1,fruit f2)
	{
		return f1.peice > f2.price;  //此处是“>”
	}
};

如果使用仿函数的方式

  • 希望水果价格高为优先级高
//大顶堆
struct myComparison
{
	bool operator () (fruit f1,fruit f2)
	{
		return f1.price < f2.price;
	}
};

//此时优先队列的定义应该如下
priority_queue<fruit,vector<fruit>,myComparison> q;

思路

这道题目主要涉及到如下三块内容:

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

1.一般统计元素出现频率,会使用map来统计
2.对频率进行排序使用优先级队列。本题使用小顶堆(堆头为最小元素)
3.因为需要保留前k个最大的值,所以每次更新堆时弹出的堆头应该为较小的那个,剩下堆中的k个值即为题目所需,所以本题应该使用小顶堆

具体思路可以看下面代码及注释

代码

#include<bits/stdc++.h>

using namespace std;

//leetcode submit region begin(Prohibit modification and deletion)
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[], 出现次数>来统计元素和出现的频率
        for ( int i=0; i < nums.size(); i++ ){
            map[nums[i]]++;         //记录每个元素出现的次数
        }
        //对元素进行排序,构建一个符合要求的小顶堆,根据本题要求使其大小固定为k
        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 ){     //如果小顶堆大小大于k,则pop掉堆顶(频率最小)元素
                pri_que.pop();
            }
        }

        vector<int> result(k);              //定义大小固定为k的存储结果

        for ( int i = k-1; i >= 0; i-- ){       //从频率最高的数值开始存储
            result[i] = pri_que.top().first;
            pri_que.pop();
        }
        return result;

    }
};
//leetcode submit region end(Prohibit modification and deletion)

总结

求前 k 大,用小根堆,求前 k 小,用大根堆
另外由于题目时间复杂度要求,所以不能把所有元素都压入堆,因此要用小根堆而不用大根堆

另外疑惑点:为什么左大于右就会建立小顶堆,反而建立大顶堆?(第一次查了半天没查到,先记住,再见到时候处理)


42.接雨水

可能涉及到动态规划,等第一遍刷完第二遍补上

题目链接

力扣题目链接

思路

代码


总结


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

山药泥拌饭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值