栈与队列 | 进阶

参考:
1.代码随想录
2.labuladong 的算法小抄 > 第一章、手把手刷数据结构 > 手把手设计数据结构 > 单调队列结构解决滑动窗口问题
3.【栈与队列】239滑动窗口最大值、347前 K 个高频元素
4.剑指 Offer 59 - II. 队列的最大值(单调双向队列,清晰图解)
5.如何解决 O(1) 复杂度的 API 设计题
6.347. 前 K 个高频元素
7.【C++】小白友好【优先队列的基础知识】

题目列表:
150. 逆波兰表达式求值【中等】

面试题59 - II. 队列的最大值【中等】
239. 滑动窗口最大值【困难】
剑指 Offer 59 - I. 滑动窗口的最大值【与239相同】

150. 逆波兰表达式求值【中等】

0.思路

逆波兰表达式:是一种后缀表达式,所谓后缀就是指运算符写在后面。
平常使用的算式则是一种中缀表达式,如 ( 1 + 2 ) * ( 3 + 4 ) 。
该算式的逆波兰表达式写法为 ( ( 1 2 + ) ( 3 4 + ) * ) 。

其实逆波兰表达式相当于是二叉树中的后序遍历。 可以把运算符作为中间节点,按照后序遍历的规则画出一个二叉树。
但我们没有必要从二叉树的角度去解决这个问题,只要知道逆波兰表达式是用后序遍历的方式把二叉树序列化了,就可以了。

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

1.方法

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

2.理解

注意点:

  • 力扣修改了后台测试数据,需要用longlong

     stack<long long> temp; 
    
  • 出现运算符的时候的处理:
    将字符串转换为整数stol , stoll 【参考
    取栈顶元素为num1,踢出,再取栈顶元素为num2,踢出,压入栈的是运算之后的结果
    注意: 是num2 - / num1,

     for(auto c: tokens){
            if(c=="+" || c=="-" || c=="*" ||c=="/"){
                long long num1 = temp.top();
                temp.pop();
                long long num2 = temp.top();
                temp.pop();
                if(c== "+") temp.push(num2+num1);
                if(c== "-") temp.push(num2-num1);
                if(c== "*") temp.push(num2*num1);
                if(c== "/") temp.push(num2/num1);
                // 注意: 是num2 - / num1
            }else{
                temp.push(stoll(c));
            }
        }
        int res = temp.top();
        temp.pop();
        return res;
    

在这里插入图片描述

3.代码

参考 代码随想录

239. 滑动窗口最大值【困难】

0.思路

  • 先从整体来看,我们要先定位到第一个窗口,计算最大值,然后窗口不断向右移动,每次产生一个最大值。
    同时,把这些最大值加入结果集,最后返回。
  • 这里是分两阶段来进行的:
    定位第一个窗口时,指针的起点是0,终点是k。
    定位后续窗口时,指针的起点从k开始,每次加1,一直遍历到n-k。由于range是左闭右开,所以范围是(k, n-k+1)。

大的框架有了,我们再来看如何求每个窗口内的最大值。

1.方法

  1. 方法1:
    对一个窗口,遍历其中所有元素求最大值。毫无疑问,时间复杂度是O(k)。整个算法的时间复杂度是O(nk)。

  2. 方法2:
    维护一个单调队列,该队列中所有元素的值都是递减的,每次我们只需要取队头的元素,就知道当前的最大值。
    首先,对于第一个窗口里的k个数,我们可以创建出一个单调队列。那么问题来了,每次窗口向右滑动的时候,我们怎么更新这个队列?
    其实,移动窗口带来的变化,就是左边出去了一个元素,右边进来了一个元素。所以,维护单调队列的实质就是,我们怎么处理这两个元素。
    (1)如果出去的元素与队头元素相等,那么弹出队头元素。否则不进行操作。

    void pop(int value){
    	if(!que.empty() && value == que.front()){
        	que.pop_front();
        }
     }
    

    (2)如果进来的元素比队尾元素大,那么一直弹出队尾元素,直到不满足条件时,放入新元素。当然如果要进来的元素比队尾元素小,那么直接放入就可以。

    void push(int value) {
    	while (!que.empty() && value > que.back()) {
        	que.pop_back();
        }
        	que.push_back(value);
    }
    

    (3)操作完以上两步后,我们再取队头的元素,就是当前窗口的最大值。

    所以,我们的单调队列要包含三个方法:push(放入新元素),pop(弹出队头),top(取队头但是不弹出)
    最后,我们回过头来,其实在处理第一个窗口的时候,也可以用这样的思路,只不过变成了每个元素都是新进来的,没有出去的元素。

2. 理解

单调队列如何维护队列里的元素
代码随想录里的图片,”单调队列如何维护队列里的元素“示意图。
过程理解如下:

  • 首先,2进入队列(队尾为2),当3进入时候,根据方法2(2)中想法,“ 进来的元素比队尾元素大,那么一直弹出队尾元素”(则,弹出队尾2,新的队尾为3),5同理。
  • 此时,5为队尾,1出现,“进来的元素比队尾元素小,那么直接放入就可以”,1直接进入。此时队尾为1,队列为 5,1
  • 4再进入, “进来的元素比队尾元素大,那么一直弹出队尾元素”(则,弹出队尾1,新的队尾为4)

窗口移动
代码随想录里的图片,”窗口移动“示意图。
窗口大小为3,前3个“1, 3, -1 ” 的过程如上文,不赘述。
理解代码:

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]);
        }
        // 此时,队列里面的值,只有 3,-1。
        
        result.push_back(que.front()); 		// result 记录前k的元素的最大值
        
        for (int i = k; i < nums.size(); i++) {
            que.pop(nums[i - k]); 			// 滑动窗口移除最前面元素
            // que.pop(nums[0])  num[0]= 1
            // “如果出去的元素与队头元素相等,那么弹出队头元素。否则不进行操作” 出去元素为1,对头元素为3,无操作
            // 此时为 3,-1
            
            
            que.push(nums[i]); 				// 滑动窗口前加入最后面的元素
            //  que.push(nums[3]); k=3,num[3]= -3
            //  -3进入,“ 要进来的元素比队尾元素小,那么直接放入”
            // 此时为 3,-1,-3
	
            result.push_back(que.front()); 	// 记录对应的最大值
            
            // que.pop(nums[1])  num[1]= 3
            // “如果出去的元素与队头元素相等,那么弹出队头元素。否则不进行操作” 出去元素为3,对头元素为3,直接弹出
            // 此时为 -1,-3
            // que.push(nums[4]); k=3,num[4]= 5
            //  5进入,“ 如果进来的元素比队尾元素大,那么一直弹出队尾元素” 弹出-3,再-1
            // 此时为 5
        }
        return result;
    }

验证:
在这里插入图片描述

3. 代码

参考 代码随想录

4. 注意点

单调队列的定义,分清pop 和push的作用,因为push里面也有弹出

  • void pop(int value)
    什么时候需要使用que.pop( 滑动窗口的最左边 ) ?
    是当滑动窗口移动的时候,需要移除最前面元素,判断移除的是否就是最大值。
    是 if,就判断一次

  • void push(int value)
    作用类似于整理当前滑动窗口里的元素。
    是while,因为是不断整理

面试题59 - II. 队列的最大值【中等】是该题的基础版本。

解题方法:
参考 剑指 Offer 59 - II. 队列的最大值(单调双向队列,清晰图解)
参考 如何解决 O(1) 复杂度的 API 设计题

0. 题目:

请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_value、push_back 和 pop_front 的均摊时间复杂度都是O(1)。
若队列为空,pop_front 和 max_value 需要返回 -1

1. 思路:

对于普通队列,入队push_back() 和出队pop_front() 的时间复杂度都是O(1)
本题难点为实现查找最大值 max_value()的O(1) 时间复杂度。
假设队列中存储 N 个元素,从中获取最大值需要遍历队列,时间复杂度为 O(N) ,单从算法上无优化空间。

2. 方法

最直观的想法是 维护一个最大值变量 ,在元素入队时更新此变量即可;但当最大值出队后,并无法确定下一个 次最大值 ,因此不可行。
在这里插入图片描述
考虑利用 数据结构 来实现,即经常使用的 “空间换时间” 。如下图所示,考虑构建一个递减列表来保存队列 所有递减的元素 ,递减链表随着入队和出队操作实时更新,这样队列最大元素就始终对应递减列表的首元素,实现了获取最大值 O(1) 时间复杂度。

知识点:C++中queue和deque的区别
queue从队首弹出,从队尾进入,先入先出,但是两端都能访问
deque可以访问两端并且可以在队首和队尾删除和插入元素

在这里插入图片描述
为了实现此递减列表,需要使用 双向队列 ,假设队列已经有若干元素:

  • 当执行入队 push_back() 时: 若入队一个比队列某些元素更大的数字 x ,则为了保持此列表递减,需要将双向队列 尾部所有小于 x的元素 弹出。
  • 当执行出队 pop_front() 时: 若出队的元素是最大元素,则 双向队列 需要同时 将首元素出队,以保持队列和双向队列的元素一致性。

使用双向队列原因:
维护递减列表需要元素队首弹出、队尾插入、队尾弹出操作皆为 O(1) 时间复杂度。
简言之,单项维持列表的原顺序,双列表在于保证最大值,而且双列表可以从队尾弹出。

在这里插入图片描述

3. 理解

动图1
动图2

4. 代码

作者:jyd
链接
来源:力扣(LeetCode)

class MaxQueue {
    queue<int> que;
    deque<int> deq;
public:
    MaxQueue() { }
    int max_value() {
        return deq.empty() ? -1 : deq.front();
    }
    void push_back(int value) {
        que.push(value);
        while(!deq.empty() && deq.back() < value)
            deq.pop_back();
        deq.push_back(value);
    }
    int pop_front() {
        if(que.empty()) return -1;
        int val = que.front();
        if(val == deq.front())
            deq.pop_front();
        que.pop();
        return val;
    }
};

347. 前 K 个高频元素

0. 基础知识

优先队列

优先队列的底层是最大堆或最小堆。

  • 定义

    priority_queue <Type, Container, Functional>;
    
    static bool cmp(pair<int, int>& m, pair<int, int>& n)  {
       	return m.second > n.second;
    }
    priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(&cmp)> 	q(cmp);
    

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

    priority_queue<Type>;
    

    此时默认的容器是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
  • 常用函数
    top()
    pop()
    push()
    emplace()
    empty()
    size()
    
  • 自定义比较方式
    当数据类型并不是基本数据类型,而是自定义的数据类型时,就不能用greater或less的比较方式了,而是需要自定义比较方式
    // 在此假设数据类型是自定义的水果:
    struct fruit
    {
    	string name;
    	int price;
    };
    //有两种自定义比较方式的方法,如下
    
    //1.重载运算符
    //重载”<”
    
    //若希望水果价格高为优先级高,则
    
    
    //大顶堆
    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;  //此处是>
    	}
    };
    
    //2.仿函数
    //若希望水果价格高为优先级高,则
    //大顶堆
    struct myComparison
    {
    	bool operator () (fruit f1,fruit f2)
    	{
    		return f1.price < f2.price;
    	}
    };
    //此时优先队列的定义应该如下
    priority_queue <fruit,vector<fruit>,myComparison> q;
    

1. 思路

  • 出现频率-> 考虑 map
  • 有了频率之后,对频率排序 找出前K个高频
  • 排序的时候为什么不用快排?
    使用快排要将map转换为vector的结构,然后对整个数组进行排序,
    而这种场景下,我们其实只需要维护k个有序的序列就可以了,所以使用优先级队列是最优的。
  • 复杂度分析
    时间复杂度:O(N2 ),其中 N 为数组的长度。
    设处理长度为 N 的数组的时间复杂度为 f(N)。由于处理的过程包括一次遍历和一次子分支的递归,
    最好情况下,有 f(N)=O(N)+f(N/2),根据 主定理,能够得到 f(N)=O(N)。
    最坏情况下,每次取的中枢数组的元素都位于数组的两端,时间复杂度退化为 O(N 2)。
    但由于我们在每次递归的开始会先随机选取中枢元素,故出现最坏情况的概率很低。平均情况下,时间复杂度为 O(N)。
  • 空间复杂度:O(N)。
    哈希表的大小为 O(N),
    用于排序的数组的大小也为 O(N),
    快速排序的空间复杂度最好情况为 O(logN),最坏情况为 O(N)。

2. 代码:

【C++】小白友好【优先队列的基础知识】
代码随想录 347.前 K 个高频元素

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值