栈与队列总结点睛

一、知识点总结

栈 stack & 队列 queue

1、HP STL
这个是 c++ STL的第一个实现版本,但不够成熟

2、P.J.Plauger STL
被visual c++编译器采用,不是开源的

3、SGI STL (常用的)
Linux的C++编译器 GCC 所采用的,开源

通常我们所使用的的STL就是 SGI STL,不同·的版本,对应的底层实现也有差异

1、stack和queue都不是容器,而是容器适配器

2、Standard Template Library ,即STL标准模板库,C++中三个最为普遍的STL版本:

3、stack 的底层实现可以是vector,deque,list 都是可以的

SGI STL中, stack 和 queue 如果没有指定底层实现的话,默认是以deque为缺省情况下栈的低层结构。

deque是一个双向队列,只要封住一段,只开通另一端就可以实现栈的逻辑了。

1、单调栈

有时候也可以延展为 双端队列,视情况而定:

  • 定义:栈中的元素保持升序或者降序

  • 时间复杂度 O(n)

  • 什么情况下用单调栈这种结构呢?

    • 通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了
  • 本质

    • 空间换时间。如果不用单调栈的结构,那么就是O(n^2),要双层遍历
    • 使用单调栈的话,就是在遍历的过程中,需要用一个栈来记录右边第一个比当前元素大的元素,所以只用遍历一次
  • 需要明确以下几点

    • 1、单调栈中存放的元素是什么?
    • 2、设定的栈的是单调增(top元素最小)还是单调减(top元素最大)
  • 如何判别是否需要使用单调栈?
    • 如果需要找到左边或者右边第一个比当前位置的数大或者小,则可以考虑使用单调栈;
  • 满足 先进后出,注意这里说的栈顶是那个唯一的出口
  • 同时 从栈顶到栈底 严格递增(以单调递增为例)
  • 具体实现
    • 若当前进栈元素为e,从栈顶开始遍历元素,把小于e或者等于e的元素弹出栈,
    • 直接遇到一个大于e的元素或者栈为空为止,然后再把e压入栈中。

题型:

2、单调队列

  • 解决问题:

    • 需要得到当前的某个范围内的最小值或最大值
  • 队列中的元素其对应在原来的列表中的顺序必须是单调递增的。

  • 队列中元素的大小必须是单调递 (增/减/甚至是自定义也可以)

  • 与普通队列的区别:

    • 单调队列的队首和队尾都可以出队或者入队
    • 普通队列只能是队尾入队,队首出队
  • 通常是自己通过 deque 来实现的

    • 设当前准备入队的元素为e,从队尾开始把队列中的元素逐个与e对比,
    • 把比e大或者与e相等的元素逐个删除,
    • 直到遇到一个比e小的元素或者队列为空为止,然后把当前元素e插入到队尾。
    • 其实就是在插入数据的过程中,维护队列的单调性
    • 通常在做题的时候,还需要维护队列的长度,比如滑动窗口题

    最后的效果相当于:

    • 当插入元素比队首元素大,则将队伍中的所有元素删掉,然后再将这个元素插入
    • 当插入元素比队首元素小,但是比队尾元素大,则 while 循环将从队尾开始的每个比该元素小的都删掉,最后再插入

单调队列(属于一种重要的数据结构)

一些陌生的概念:
小根堆,(小顶堆),大根堆,(大顶堆):是完全二叉树,小即升序,大即降序
单调队列:可以是单调递增或递减
优先级队列:对所有元素进行排序

3、优先级队列 priority_queue

常用场景

对于前 k 个大,或者前 k 个小的问题,有一个通用的解法,就是优先级队列

  • 其实是一种堆,是一棵完全二叉树,而这个二叉树又满足一个规律——任意节点都小于(或大于)其子节点
  • C++提供了内置函数 priority_queue,包含在头文件 #include 中
  • 与队列的基本操作相同,top pop push <取出队首元素,删除队首元素,从队尾插入元素>
3.1 使用示例
  • priority_queue 的 三个参数 (数据类型 容器类型 比较器)

    第二个参数:没用容器的时候,那就是默认是vector,T就是第一个参数指定的那个类型

    第三个参数:cmp可以是定义为一个任意类型,

    ​ 可以定义成一个 struct 或者 class ,里面包含 operate(),此时实例化的后面不用跟(),参考 pdf 中的 优先级队列部分

    ​ 也可以定义成一个 lambda 表达式,此时实例化的后面需要跟() LC692

    • 写法一:定义为 lambda 表达式
    priority_queue<PIS, vector<PIS>, decltype(cmp)> que(cmp);
    
    using PIS = pair<int, string>;
    auto cmp = [](const PIS& a, const PIS& b)
    {
    	return a.first > b.first;   // 按照pair中的 int 降序排序
    }
    
    • 写法二:定义一个类/结构体
    priority_queue<pair<int,int>,vector<pair<int,int>>,mycomparison> pri_que;
    
    // 这里就是实现小顶堆
    class mycomparison
    {
    public:
        bool operator()(const pair<int,int>& lhs,const pair<int,int>& rhs)
        {
        	return lhs.second > rhs.second;
        }
    };
    
    // 1、 lambda表达式 实现一个排序函数
    sort(intervals.begin(), intervals.end(), 
    [](const vector<int>& a, const vector<int>& b)
    {return a[0] < b[0]; });
    

分为大顶堆和小顶堆,以下讨论基于大顶堆

  • 时间复杂度
    • 获取最大值O(1)
    • 取出最大值O(logn)
    • 插入任意值O(logn)
  • 实现方式
    • 常常用堆来实现,堆 是一个完全二叉树,每个节点的值,总是大于等于子节点的值
  • 底层存储方式
    • 实际实现堆的时候,我们通常是用 数组,而不是链表
    • 用数组表示时,位置 i 的节点的父节点位置一定为 i/2,两个子节点的位置一定是 2i 和 2i+1
  • 核心操作
    • 上浮
    • 下沉
  • STL 中的实际实现
    • 底层默认基于vector实现堆结构。
    • O(nlogn)的时间排序数组,
    • O(logn) 的时间插入任意值
    • O(logn) 的时间删除最大值,注意默认不支持删除任意值,只是删除最大值
    • O(1) 的时间获得最大值
    • priority_queue 常用于维护数据结构并快速获取最大或最小值。
  • 与set的对比
    • 底层实现默认为红黑树,即一种特殊的二叉查找树(BST)。
    • O(nlogn) 的时间排序数组
    • O(logn) 的时间插入、删除、查找任意值
    • O(logn) 的时间获得最小或最大值
// 用 vector 实现一个简单的优先级队列
vector<int> heap;

// 上浮
void swim(int pos)
{
	while (pos > 1 && heap[pos / 2] < heap[pos])
	{
		swap(heap[pos / 2], heap[pos]);
		pos = pos / 2;
	}
}

// 下沉
void sink(int pos)
{
	while (2 * pos <= N)
	{
		int i = 2 * pos;
		if (i < N && heap[i] < heap[i + 1])		++i;
		if (heap[pos] >= heap[i])		break;
		swap(heap[pos], heap[i]);
		pos = i;
	}	
}

// 获得最大值
void top()
{
	return heap[0];
}

// 插入任意值:插入的实际操作是 *先把新的数组放在最后一位,*然后对其做上浮的操作
void push(int k)
{
	heap.push_back(k);
	swim(heap.size() - 1);
}

// 删除最大值:删除的实际操作是 *先把最后一个数组覆盖原本的第一个值(也就是最大值),*然后把最后一个删掉,*最后对第一个位置的值做下沉操作
void pop()
{
	heap[0] = heap.back();	// 覆盖操作
	heap.pop_back();
	sink(0);
}

二、做题总结

第232题 双栈实现队列

这个思路还是比较清晰的,第一次看到这个题,竟然还在疑惑初始化的应该考虑以链表的方式还是数组的形式,人家都说了是有现成的栈了嘛,那肯定就是不用考虑数据的存储方式啦~栈已经完成了,只需考虑如何通过两个栈来实现队列就好啦!

两个栈,分别设定为一个 in ,一个 out, 注意分配好各自的职责,不可以越界。

这个里面在pop的时候会有一丢丢的麻烦,这里需要先判断 out 栈是否为空,
如果为空,就把 in 中的元素全部转移到out,
如果不为空,则直接pop.

第225题 双队列实现栈

其实这个用一个队列完全是可以实现的,

这个的思路与双栈实现队列不同,没有“各司其职”,而是另外一个仅仅是用来暂存元素的
一主一辅,故而只是主次之分,

这里出现有个奇怪的错误。

这个错误的原因是 que.size() 因为要做 pop 操作,所以每次迭代时会发生改变,注意:::for循环里的边界最好不要变

第22题 括号匹配问题

注意:要求括号的顺序是一样的,有左括号,相应的位置必须要有右括号。

([)] 这样是错误的!!!!!

先来分析一下 这里有三种不匹配的情况,

  • 第一种情况,字符串里左方向的括号多余了 ,所以不匹配。
  • 第二种情况,括号没有多余,但是 括号的类型没有匹配上。
  • 第三种情况,字符串里右方向的括号多余了,所以不匹配。

第150题 逆波兰表达式

Reverse Polish notation,RPN
实现逆波兰式的算法,难度并不大,但为什么要将看似简单的中缀表达式转换为复杂的逆波兰式?原因就在于这个简单是相对人类的思维结构来说的,对计算机而言中序表达式是非常复杂的结构。相对的,逆波兰式在计算机看来却是比较简单易懂的结构。因为计算机普遍采用的内存结构是栈式结构,它执行先进后出的顺序。

逆波兰表达式的优点:

  • 1、去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。
  • 2、适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中。
  • 递归就是用栈来实现的。所以,栈与递归之间在某种程度上是可以转换的

  • 其实逆波兰表达式相当于是二叉树中的后序遍历,其中运算符作为中间节

  • 但我们没有必要从二叉树的角度去解决这个问题,只要知道逆波兰表达式是用后续遍历的方式把二叉树序列化了,就可以了。

  • 本题中每一个子表达式要得出一个结果,然后拿这个结果再进行运算,那么 这岂不就是一个相邻字符串消除的过程和对对碰游戏是不是就非常像了。

第239题 滑动窗口最大值

以前没有见过的解题思路

1、首先是在一个类中实现一个类,这个会让我联想到继承关系,可能这里只是为了做题,没必要去设计的很麻烦,就不考虑继承,仅仅完成实现的过程即可。

2、本题是使用 deque 来实现一个queue,因此首先要熟悉 deque的操作都有哪些

3、关于单调队列的用途

  • 1、单调队列主要就是用来进行最值判定。
    • 如果是最大值那就是单调递减,左边队头就是要找的最大值
    • 如果是取最小值则是单调递增,左边队头找最小值。
  • 2、单调队列是如何进行push工作的呢?
    • 如果是单调递减队列,要确保push元素比队尾元素小
    • 如果大于队尾元素则将队尾元素剔除再与新的队尾进行比较,直到大于队尾元素或者队列为空时进行push,注意这里用的是while循环。
  • 3、pop 时需是要和滑动窗口结合进行理解。
    • 如果滑动窗口的left需要剔除的元素恰恰是队列的最左边队头元素,则说明队头元素应经不在此窗口内了,直接进行pop
    • 如果不等于,就不用进行pop了,说明此元素还在窗口内
  • 4、返回最大值,就是返回队头元素(因为是单调递减的)。

4、关于本题的伪代码

​ 伪代码:k=3

  • 1、队列中加入前3个值

  • 2、移除区间第0个元素

    • 2.1、移除的操作:
      • If (移除元素 == 队列front元素) {移除}
  • 3、添加区间第4个元素

    • 3.1、移除掉队列里比第4个元素小的

1、单调栈

第739题 每日温度

本题其实就是找到在 某个元素k 的右边的第一个比 k 大的元素

第84题

第42题

2、单调队列

3、优先级队列(priority_queue)

第23题 合并 K 个链表

其实本题的主要思想就是排序:这里选用一种较快的排序方式————优先级队列

把所有链表存储在一个优先级队列中,每次提取出 所有链表头部节点值最小的那个节点,直到所有链表被提取完

优先级队列的默认实现是 最大堆,即堆的第一个元素是最大值,所以为了获取最小的节点,我们需要自己实现一个最小堆

所以比较函数应该维持 递减关系

1、不会写的点:这个优先级队列,怎么设定那个比较函数呢?

2、比较的顺序,每次比较链表的第一个节点值,比较后就pop,比较完一轮之后,将链表剩下的入队,进入下一轮比较

第692题 前K个高频单词

队首是较小的元素,也就是使用了小顶堆,来维护队列的长度为 K,当长度超过 K ,直接将队首元素弹出即可

// 1、lambda 表达式
// 这个是定义在 class Solution 里面的一个 lambda 表达式
using PIS = pair<int, string>;
auto cmp = [](const PIS& a, const PIS& b)
{
	if (a.first == b.first)  return a.second < b.second;    // 如果pair中的 int 相同,则对于第二个元素按字典升序排
	return a.first > b.first;   // 按照pair中的 int 降序排序
};

priority_queue<PIS, vector<PIS>, decltype(cmp)> que(cmp);

第347题 前K个高频元素

同样也是用小顶堆,维护堆的大小,注意队首每次取出的是队列中的最小元素

// 这里就是实现小顶堆,
// 这个是定义在 class Solution 外面的一个类
class mycomparison
{
	public:
	bool operator()(const pair<int,int>& lhs,const pair<int,int>& rhs)
    {
    	return lhs.second > rhs.second;
    }
};

priority_queue<pair<int,int>,vector<pair<int,int>>,mycomparison> pri_que;

本道题主要涉及三块内容:

  • 1、要统计元素出现的频率
  • 2、对第一步统计处的频率进行排序
  • 3、找出前 K 个高频元素

针对第一部分:使用 map 来进行统计

针对第二部分:使用优先级队列进行排序

针对第三部分:使用小顶堆

知识补充——优先级队列

1、为什么要选用 优先级队列排序 ,而不选用快排呢?

​ 因为快排需要将 map 转换为 vector 的结构。

​ 而优先级队列则不需要转换,仅需要维护 K 个有序的序列就可以。

2、选好了使用优先级队列(也就是堆排序),那么是使用小顶堆还是大顶堆呢?

​ 肯定会觉得 既然求前 K 个高频元素,那肯定是用大顶堆咯~ —这个答案是不对滴!

​ 大顶堆每次弹出的都是最大的元素,而我们最后要保留的是最大的,这样做维护的代价太高,

​ 换个思路想一下,如果用小顶堆,每次弹出去的都是最小元素,那么自然留下的就是最大的 K 个元素咯~

图片
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值