
1. stack的介绍和使用
1.1 stack的介绍

【说明】
stack是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。
stack是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部(即栈顶)被压入和弹出。
stack的底层容器,可以是任何标准的容器类模板,或者一些其他特定的容器类。
这些容器类应该支持以下操作:
- empty:判空操作
- back:获取尾部元素操作
- push_back:尾部插入元素操作
- pop_back:尾部删除元素操作
- 标准容器vector、deque、list均符合这些需求,默认情况下,如果没有为stack指定特定的底层容器,默认情况下使用deque。

1.2stack的使用

1.2.1接口说明
|-----------------------------------------------------------------------------|--------------------|
| 函数说明 | 接口说明 |
| stack() | 构造空的栈 |
| empty() | 检测stack是否为空 |
| size() | 返回stack中元素的个数 |
| top() | 返回栈顶元素的引用 |
| push() | 将元素val压入stack中 |
| pop() | 将stack中尾部的元素弹出 |
1.2.2代码测试

1.2.3OJ练习
1.2.2.1最小栈

最小栈:是一个栈,支持后进先出,但是增加了一个接口------获取最小元素。
而且要求时间复杂度O(1),不能是O(N)遍历。
思路1:用一个min记录一下最小元素------每次入栈的时候,都用入栈元素跟min比较一下,如果比min小就更新min。
问题1:删栈顶元素之后,min该如何更新------得遍历一遍,但栈不允许遍历**(没有提供迭代器)**,就算允许遍历,时间复杂度也是O(N) 不是O(1)。

【注意】
- minst栈顶就是当前最小元素,minst栈顶之下是曾经的最小元素,栈顶弹出后下面的"曾经最小元素"才有机会重新成为"当前最小元素"。
- 入栈元素小于等于minst栈顶元素,都得入栈minst,得保证两边的最小元素的个数一致,因为删的时候是一起删的。

用C++的代码,就不需要造轮子了,也不用写构造,全是自定义类型成员变量,默认构造就可以。
cpp
//最小栈类
//成员变量:st、minst
//成员函数:......
class MinStack
{
public:
//入数据
void push(int x)
{
// 只要是压栈,先将元素保存到_elem中------正常栈,直接入
_elem.push(x);
// 如果x小于_min中栈顶的元素,将x再压入_min中------min栈判断一下再入
if(_min.empty() || x <= _min.top())
_min.push(x);
}
//出数据
void pop()
{
// 如果_min栈顶的元素等于出栈的元素,_min顶的元素要移除------min栈判断一下再出
if(_min.top() == _elem.top())
_min.pop();
_elem.pop();------正常栈,直接出
}
int top(){return _elem.top();} //获取栈顶元素
int getMin(){return _min.top();} //获取最小元素
private:
// 保存栈中的元素
std::stack<int> _elem;
// 保存栈的最小值
std::stack<int> _min;
};
不用写构造------编译器生成的默认构造,对自定义类型stack<int>会去调它的默认构造。

1.2.2.2 栈的弹出压入序列

入栈和出栈序列匹配,之前做过选择题。一个入栈序列,会有多个与之匹配的出栈序列。
任何一个数据都是入栈之后,再出栈。这道题感觉简单,写得稍有不慎,就会写成一个复杂题。
一定记住不要直接把入栈序列、出栈序列直接拿来比较。而是任何一个数据都保证它是入栈之后,再去出栈,就没问题。

入完栈之后再判断栈顶元素出不出, 而不是还没入栈就直接判断。

直到入栈序列push_st(数组的形式),本次入栈st的元素num,恰好等于出栈序列pop_st(数组的形式)的首元素,就执行出栈,按着出栈序列pop_st的顺序进行出栈。

匹配到情况b,就执行操作1。
匹配到情况a,就执行操作2。

出完4,用3和5比较,不相等,符合情况b,下一步是操作1。入栈5。

再比较,若相等,出完继续比。一直相等一直比。

结束条件:栈st空了。

不相等就继续入,一直不等一直入。

相等出完继续比,一直相等一直比。

5入栈,相等再出栈。

不相等,要再入数据,但是入栈序列已经空了。
【结束条件】
入栈序列必然已经结束了;
- ① 上面这种情况------匹配的时候:出栈序列也结束了,同时栈空了
(栈空or出完,都可以验匹配)- ② 下面这种情况------不匹配的时候:出栈序列没结束,同时栈也没空
(栈非空or没出完,都可以验不匹配)
如果把入栈序列(数组)和出栈序列(数组)直接比较,不相等就++,而不是借助一个第三方的栈这样一个数据结构来辅助判断,就很麻烦了。

算法能力跟看得算法课多少没多大关系,不是看了就会了,重要的是学会这些方法和思路。
- 逻辑梳理顺了,代码就很好写了,大框架是这样,无非是一些细节上面的控制。
- 这些算法题,代码已经不那么重要了,重要的是思路。画图把思路理清楚。
cpp
class Solution {
public:
bool IsPopOrder(vector<int> pushV,vector<int> popV)
{
//入栈和出栈的元素个数必须相同
if(pushV.size() != popV.size())
return false;
// 用s来模拟入栈与出栈的过程
int outIdx = 0; //当前出栈序列的下标
int inIdx = 0; //当前入栈序列的下标
stack<int> s;
while(outIdx < popV.size())
{
// 如果s是空,或者栈顶元素与出栈的元素不相等,就入栈
while(s.empty() || s.top() != popV[outIdx])
{
if(inIdx < pushV.size())
s.push(pushV[inIdx++]);
else
return false;
}
// 栈顶元素与出栈的元素相等,出栈
s.pop();
outIdx++; //下标迭代
}
return true;
}
};

- 栈为空:所有数据都匹配上了。
- 栈不为空:有数据没匹配上。
1.2.2.3**逆波兰表达式求值**
cpp
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> s;
for (size_t i = 0; i < tokens.size(); ++i)
{
string& str = tokens[i];
// str为数字
if (!("+" == str || "-" == str || "*" == str || "/" == str))
{
s.push(atoi(str.c_str()));
}
else
{
// str为操作符
int right = s.top();
s.pop();
int left = s.top();
s.pop();
switch (str[0])
{
case '+':
s.push(left + right);
break;
case '-':
s.push(left - right);
break;
case '*':
s.push(left * right);
break;
case '/':
// 题目说明了不存在除数为0的情况
s.push(left / right);
break;
}
}
}
return s.top();
}
};
1.2.2.4**两个栈实现队列**
......
1.3stack的模拟实现
从栈的接口中可以看出,栈实际是一种特殊的vector,因此使用vector完全可以模拟实现stack。
cpp
#include<vector>
namespace bite
{
template<class T>
class stack
{
public:
stack() {}
void push(const T& x) {_c.push_back(x);}
void pop() {_c.pop_back();}
T& top() {return _c.back();}
const T& top()const {return _c.back();}
size_t size()const {return _c.size();}
bool empty()const {return _c.empty();}
private:
std::vector<T> _c;
};
}

适配器的作用就是用于转换的,以电力系统为例,在外传输需要转换到特高压降低损耗,接入家庭需要转换220V,各种功率电器使用需要使用电源适配器转换成对应的电压。
同理,为了满足不同场合的不同需求,栈的实现也要提高兼容性。
用vector(list)来适配,只提供尾插尾删,封装一下,就成了一个顺序栈(链栈)。

但是这样用list<T> _lt的方式,就写死了,没法灵活适配。
所以它在这增加一个模版参数------容器Container,用来提供合适的容器,适配出一个栈。
模版参数平时没有具体含义的时候,就叫T(Type),这里有具体含义,就可以使用它的含义。

这样构造就不用写了,直接使用自定义类型的默认构造。

栈的设计打开了一种新的设计模式:
- 之前设计一个数据结构,就要自己一笔一画去写,一点一点去搓。
- 而现在只需要对其他合适的数据结构封装一下,转换一下就完成了我需要的数据结构,而且依靠模版参数的泛型编程,灵活适配不同容器来作底层架构,从而适配不同的应用场景。

代码测试。


在表层,使用的stack都是一个后进先出的特性的容器。
但是底层数据结构却很不一样。
库里面的stack的使用,可以不用传第2个参数,stack<int> st就可以了,所以我们也可以给到一个缺省参数。
- 模版参数和函数参数很类似,都是实参传递给形参。
- 模版参数是<>尖括号,函数参数是圆括号()。
- 模版参数传的是类型,函数参数传的是对象。
- 模版参数和函数参数一样,都可以给缺省值,只是这个地方给的缺省值是一个类型。


队列也是类似的,也可以这样实现。

- vector支持尾插尾删,不支持头插头删(虽然有insert、erase但慎用)
- list支持尾插尾删头插头删,但是不支持[ ]访问。
deque支持头插头删尾插尾删,和[ ]访问。

2.queue的介绍和使用
2.1queue的介绍

【说明】
队列是一种容器适配器,专门用于在FIFO上下文(先进先出)中操作,其中从容器一端插入元素,另一端提取元素。
队列作为容器适配器实现,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从队尾入队列,从队头出队列。
底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。该底层容器应至少支持以下操作:
- empty:检测队列是否为空
- size:返回队列中有效元素的个数
- front:返回队头元素的引用
- back:返回队尾元素的引用
- push_back:在队列尾部入队列
- pop_front:在队列头部出队列
- 标准容器类deque和list满足了这些要求。默认情况下,如果没有为queue实例化指定容器类,则使用标准容器deque。

2.2queue的使用

2.2.1接口说明
|-----------------------------------------------------------------------------|--------------------------------|
| 函数声明 | 接口说明 |
| queue() | 构造空的队列 |
| empty() | 检测队列是否为空,是返回true,否则返回false |
| size() | 返回队列中有效元素的个数 |
| front() | 返回队头元素的引用 |
| back() | 返回队尾元素的引用 |
| push() | 在队尾将元素val入队列 |
| pop() | 将队头元素出队列 |
2.2.2OJ练习
2.2.2.1**队列实现栈**
......
2.2.2.2**二叉树的层序遍历**
思路:先进先出,上一层的父结点出去的时候,带入下一层它的子结点。
例如:先入3,3出的时候带入9、20,9出的时候带入9的子树(NULL),20出的时候带入15、7
这道题是进阶版:要求每一层都存到一个数组里面,总的存成一个数组,即二维数组。
这样子的话用C语言写起来就很麻烦了
动态返回二维数组,上次vector<vector<......>>已经说过这个问题了。
问题来了:什么时候一行就走完了呢???
先来实现层序遍历入队列。
问题:怎么一层一层地取。怎么知道一个数据是那一层的呢???
思路1:再搞一个队列存层数------队列内每一个元素,都在另一个队列中对应的位置存放着它对应的层数。
常规队列存完3,再搞一个队列存它的层数1。
3出去的时候,1也就跟着出。同时带入下一层的数据,同时跟着入层数2。
元素9和层数2出去,就带入元素15和层数3。
评价:这种思路可行,但是这种思路在操控上可能略复杂。因为每一层的数据你知道它是第几层以后,你还要把它放到vector里面去,并且你是第1层的要放到第0行的vector。还得把vector提前resize好,不然的话,放数据的时候,用插入数据就会频繁扩容。
思路2:直接在这个地方用一个levelsize(每一层的数据个数)变量控制,用while循环控制一层一层出。
第一层一个数据,levelsize为1,用一个while循环控制,出完就减到0。
2.3queue的模拟实现
因为queue的接口中存在头删和尾插,因此使用vector来封装效率太低,故可以借助list来模拟实现queue,具体如下:
cpp
#include <list>
namespace bite
{
template<class T>
class queue
{
public:
queue() {}
void push(const T& x) {_c.push_back(x);}
void pop() {_c.pop_front();}
T& back() {return _c.back();}
const T& back()const {return _c.back();}
T& front() {return _c.front();}
const T& front()const {return _c.front();}
size_t size()const {return _c.size();}
bool empty()const {return _c.empty();}
private:
std::list<T> _c;
};
}
queue的适配容器可以选list、deque,默认是deque。

使用vector来适配可能会报错------先进先出,需要头删,vector不(直接)支持头删------效率低。
2.4测试

注意queue提供的接口是front,而不是top。
而vector没有支持pop_front,用vector来适配会报错------避免使用低效的方式来适配队列。
3. 容器适配器
3.1什么是适配器
适配器:适配器是一种设计模式。

什么是设计模式?
以兵法为例,第一个比较著名的兵法就是《孙子兵法》。
最开始打仗就是拼谁的人多,后面发现打仗也可以有一定的套路,不是说人多一定能赢。
设计模式类似于兵法,最开始大家写代码的时候都是随意写,后面语言趋于成熟后,就形成了一定特定的套路------编程模式。
后面大家在写程序的时候,除了要把功能实现,还讲究一个代码的可维护性。
日常的代码写好了之后,后面就不会再去管了,工作中的代码要持续地去维护,修复用户深度使用过程中出现的各种各样的bug、优化效率、开发新的需求、......等等。
这个时候如果代码的可维护性不高,就会导致后续在维护的时候,按下葫芦浮起瓢,各种问题冒出来,后面就出现了软件工程这门学科,提出了低耦合、高内聚、设计模式等等这样的概念。
现在已经学过的有:
- 迭代器模式**;**
- 适配器模式 。

while的条件是不等于,而不是小于,因为数组、字符串可以说前面的元素的指针小于最后的指针,但链表没有这样的规律。
- 第一层封装:想给你访问的设置成公有,不想给你访问的设置成私有,把数据和方法都封装到类里面。
- 第二层封装:不管底层结构的差异巨大,在里面封装一个统一的访问方式(各种容器的自己的实现可能是原生指针,可能是自定义类型)------typedef ...... iterator,在外层提供统一的访问接口------begin()、end()、*(原生指针本来就可以,自定义类型需要重载运算符)。
C语言也可以实现迭代器,只是++不支持运算符重载------*重载,但是支持typedef++。
故而实现起来没那么方便,可读性也不是很高。
C语言是面向过程的语言,*重载是面向对象的思维,面向过程的话可以使用函数的方式:ListIterEqual(it,end())、getData()、next())。
面向对象支持运算符重载,可以把迭代器封装得更类似于指针,用法上几乎和指针一致。
C语言也可以模拟出成员函数的概念------用函数指针的方式。

结构体可以用函数指针模拟实现成员函数,函数指针可以直接加上()填入参数就调用的。
上面的C语言迭代器就可以用it.get()、it.next()。
迭代器模式,为各种容器提供了统一的访问模式。
像Python这样的其他语言,可能没有迭代器的概念,只有范围for,其底层还是迭代器,在迭代器上又加了一层封装。
- 迭代器模式的核心思想:封装以后提供统一的访问方式。
- (所有容器的访问都可以使用这种方式)
- 适配器模式的核心思想:封装转换------一个容器的底层结构是什么,不是写死的,是由第二个模版参数决定的,但其表现出来的功能是不变的。
适配器模式:用现成的容器来封装出一个新的具有特定功能的容器,这个具有特定功能的容器可以用不同的基础容器来适配,从而产生在基础功能之外的特点。


用不同的容器适配出来的栈,底层完全不同,而表现的功能类似------后进先出。
设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结,该种模式是将一个类的接口转换成客户希望的另外一个接口。
3.2STL标准库中stack和queue的底层结构
虽然stack和queue中也可以存放元素,但在STL中并没有将其划分在容器的行列。
而是将其称为容器适配器,这是因为stack和队列只是对其他容器的接口进行了包装,STL中stack和queue默认使用deque,比如:
- 容器适配器都没有提供迭代器**;**
- 因为不支持使用迭代器访问;
- 容器适配器对其元素的访问是有严格限制的;
- 从哪里插入、从哪里弹出都是有严格限制的。
3.3 deque的简单介绍**(了解)**



- deque在功能上是vector和list的结合体,所以很多适配器的默认容器都是deque。
- vector没有头删(插);
- list没有[]重载;
- deque设计来替代vector和list,但是没有成功,数据结构的基础学习还是"顺序表"、"链表"。(类似于骡子:驴+马)
vector和list除了功能上的差异,它们在结构上也是两种极致的结构------极致的连续 VS 极致的离散
3.3.1deque的原理介绍
deque(Double-End Queue,双端队列)****:是一种++双开口++ 的++"连续"空间++的数据结构。
- 双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1)。
- 与vector比较,头插效率高,不需要搬移元素;
- 与list比较,空间利用率比较高。

- deque在结构上类似于vector<vector>。

第一段buff的地址不是存储在中控数组的首元素,而是中间,这样头插元素(头插buff数组)之后可以把头插的buff数组地址放到之前第一段buff数组地址的前面。
deque的缺陷:
-
\]访问不够极致,比vector慢;
这就是说想:
- 保持insert和erase的效率就需要牺牲[]的效率;(扩容)
- 保持[]的效率就需要牺牲erase和insert的效率;(挪动)
STL库选择了"数据挪动"的方式,保持buff大小一致。
不过总的来说,[]的效率不如vector,erase/insert的效率不如list。


算法和数据都一样,不同的是容器的结构,可以看到效率上的差别还是比较大的。

可以看到deque的[]访问速度不够极致。

头尾插删的效率不错,因为不需要扩容,直接再开buff数组就可以了。
中控数组有可能会扩容------代价很低,只需要拷贝指针就可以了。
不是高频访问,而是偶尔访问,那么deque的[]也还行。
但是大量访问------比如排序(访问数据来比大小),deque相比于vector就很慢了。
【结论】
- 经常在头尾插入删除,偶尔下标访问,就可以使用deque。
- stack和queue使用deque作默认容器就比较合适。
- deque相比于vector不需要扩容;
- deque相比于list有更好的CPU高速缓存效率优势;
- 主流的数据存储还是使用顺序表、链表。
deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际deque类似于一个动态的二维****数组,其底层结构如下图所示:
**双端队列,底层是一段假象的连续空间,实际是分段连续的,为了维护其"整体连续"以及随机访问的假象,落****在了deque的迭代器身上,**因此deque的迭代器设计就比较复杂,如下图所示:

deque的成员变量主要就是两个迭代器,此外还有一个中控数组成员和size成员。
那deque是如何借助其迭代器维护其假想连续的结构呢?

cur:指向当前位置;
first:指向当前buff的第一个有效空间;
last:指向当前buff的最后一个有效空间的下一个位置;
size:每个buff数组的统一大小。
start的cur:指向第一个有效数据;
finish的cur:指向最后一个有效数据的下一个位置;

来看看start和finish的工作原理:

start的左边不一定有数据,finish的右边不一定有数据。

如果没有头插过,那么cur-first==0,偏移量直接就是n,直接让cur+=n。
如果头插过2次(buff大小10),那么cur-first==8:
- 加数n小于2,为0或1,就在首buff让cur+=n。
- 加数n大于2,则偏移量offset大于buff的大小,需要用"除+模"计算准确的位置。
3.3.2 deque的缺陷
【优势】
- 与vector比较: deque在头部插入和删除时,不需要搬移元素,效率特别高 ,而且在扩容时,也不****需要搬移大量的元素,因此其效率是必vector高的。
- 与list比较: 其底层是连续空间,空间利用率比较高,不需要存储额外字段。
但是,deque有一个致命缺陷:不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到****某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历。
- 因此在实际中,需要线性结构****时,大多数情况下优先考虑vector和list,deque的应用并不多。
- 而目前能看到的一个应用就是,STL用其作****为stack和queue的底层数据结构。
3.4 为什么选择deque作为stack和queue的底层默认容器
stack是一种后进先出的特殊线性数据结构,因此只要具有push_back()和pop_back()操作的线性结构,都可以作为stack的底层容器,比如vector和list都可以;queue是先进先出的特殊线性数据结构,只要具有push_back和pop_front操作的线性结构,都可以作为queue的底层容器,比如list。但是STL中对stack和queue默认选择deque作为其底层容器,主要是因为:
-
stack和queue不需要遍历(因此stack和queue没有迭代器),只需要在固定的一端或者两端进行操作。
-
在stack中元素增长时,deque比vector的效率高(扩容时不需要搬移大量数据);queue中的元素增长时,deque不仅效率高,而且内存使用率高。
结合了deque的优点,而完美的避开了其缺陷。
3.5STL标准库中对于stack和queue的模拟实现
3.5.1stack的模拟实现
cpp
#include<deque>
namespace bite
{
template<class T, class Con = deque<T>>
//template<class T, class Con = vector<T>>
//template<class T, class Con = list<T>>
class stack
{
public:
stack() {}
void push(const T& x) {_c.push_back(x);}
void pop() {_c.pop_back();}
T& top() {return _c.back();}
const T& top()const {return _c.back();}
size_t size()const {return _c.size();}
bool empty()const {return _c.empty();}
private:
Con _c;
};
}
3.5.2queue的模拟实现
cpp
#include<deque>
#include <list>
namespace bite
{
template<class T, class Con = deque<T>>
//template<class T, class Con = list<T>>
class queue
{
public:
queue() {}
void push(const T& x) {_c.push_back(x);}
void pop() {_c.pop_front();}
T& back() {return _c.back();}
const T& back()const {return _c.back();}
T& front() {return _c.front();}
const T& front()const {return _c.front();}
size_t size()const {return _c.size();}
bool empty()const {return _c.empty();}
private:
Con _c;
};
}
4.priority_queue的介绍和使用

4.1priority_queue的介绍

**priority_queue:**有一个container参数,需要传一个容器去适配,所以优先级队列是一个容器适配器。
priority_queue默认使用vector进行适配。
【问】为什么stack和queue默认用deque去适配,而priority_queue默认用vector?【答】因为priority_queue需要大量使用[]访问------底层是(二叉)堆。
- 最大元素:大堆;
- 最小元素:小堆;
- STL库没有现成的堆,有堆算法;
- 要使用堆就直接使用优先级队列------优先级队列就是堆;
【说明】
优先队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的。
此上下文类似于堆,在堆中可以随时插入元素,并且只能检索最大堆元素(优先队列中位于顶部的元素)。
优先队列被实现为容器适配器,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从特定容器的"尾部"弹出,其称为优先队列的顶部。
底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。容器应该可以通过随机访问迭代器访问,并支持以下操作:
- empty():检测容器是否为空
- size():返回容器中有效元素个数
- front():返回容器中第一个元素的引用
- push_back():在容器尾部插入元素
- pop_back():删除容器尾部元素
标准容器类vector和deque满足这些需求。默认情况下,如果没有为特定的priority_queue类实例化指定容器类,则使用vector。
需要支持随机访问迭代器,以便始终在内部保持堆结构。容器适配器通过在需要时自动调用算法函数make_heap、push_heap和pop_heap来自动完成此操作。
4.2 priority_queue的使用
- 优先级队列默认使用vector作为其底层存储数据的容器;
- 在vector上又使用了堆算法将vector中元素构造成堆的结构;
- 因此priority_queue就是堆,所有需要用到堆的位置,都可以考虑使用priority_queue。
- 堆的核心操作:取堆顶;插入堆顶;删除堆顶。
注意:默认情况下priority_queue是大堆。
|------------------------------------------------------|----------------------------------------|
| 函数声明 | 接口说明 |
| priority_queue()/priority_queue(first, last) | 构造一个空的优先级队列 |
| empty() | 检测优先级队列是否为空,是返回true,否则返回 false |
| top() | 返回优先级队列中最大(最小元素),即堆顶元素 |
| push() | 在优先级队列中插入元素x |
| pop () | 删除优先级队列中最大(最小)元素,即堆顶元素 |

【注意1】默认大堆
- 默认情况下,priority_queue是大堆。

priority_queue支持迭代器区间初始化。

优点:直接建堆,不用挨着挨着push。

【注意】如果数据不在vector里面,而是在数组int arr [10]里面,也可以用迭代器构造,同时数组也可以用sort排序。

【注意】
- 数组------连续的物理空间,可以直接传原生指针给迭代器参数。
- 实现vector的迭代器就可以直接使用原生指针。
【注意】建小堆需要传仿函数。

【注意】要显式给第3个模版参数,就要先把前2个模版参数给出来。

cpp
#include <vector>
#include <queue>
#include <functional> // greater算法的头文件
void TestPriorityQueue()
{
// 默认情况下,创建的是大堆,其底层按照小于号比较
vector<int> v{3,2,7,6,0,4,1,9,8,5};
priority_queue<int> q1;
for (auto& e : v)
q1.push(e);
cout << q1.top() << endl;
// 如果要创建小堆,将第三个模板参数换成greater比较方式
priority_queue<int, vector<int>, greater<int>> q2(v.begin(), v.end());
cout << q2.top() << endl;
}


【注意2】存自定义类型Date
- 如果在priority_queue中放自定义类型的数据,用户需要在自定义类型中提供>或者<的重载。
cpp
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
bool operator<(const Date& d)const
{
return (_year < d._year) ||
(_year == d._year && _month < d._month) ||
(_year == d._year && _month == d._month && _day < d._day);
}
bool operator>(const Date& d)const
{
return (_year > d._year) ||
(_year == d._year && _month > d._month) ||
(_year == d._year && _month == d._month && _day > d._day);
}
friend ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
void TestPriorityQueue()
{
// 大堆,需要用户在自定义类型中提供<的重载
priority_queue<Date> q1;
q1.push(Date(2018, 10, 29));
q1.push(Date(2018, 10, 28));
q1.push(Date(2018, 10, 30));
cout << q1.top() << endl;
// 如果要创建小堆,需要用户提供>的重载
priority_queue<Date, vector<Date>, greater<Date>> q2;
q2.push(Date(2018, 10, 29));
q2.push(Date(2018, 10, 28));
q2.push(Date(2018, 10, 30));
cout << q2.top() << endl;
}
4.3 在OJ****中的使用
cpp
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
// 将数组中的元素先放入优先级队列中
priority_queue<int> p(nums.begin(), nums.end());
// 将优先级队列中前k-1个元素删除掉
for(int i= 0; i < k-1; ++i)
{
p.pop();
}
return p.top();
}
};
4.4priority_queue的模拟实现
priority_queue的底层结构就是堆,因此此处只需对堆进行通用的封装即可。
(1)基本框架

(2)成员函数
① push插入

逻辑上是二叉树,物理上是数组。
【堆的插入】------"两步走"
- 数据尾插
- 数据向上调整



cpp
//向上调整------需要支持下标访问(计算父节点)
void adjust_up(int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
//用小于比较实现大堆------如果父亲小于孩子,就需要向上调整孩子
if (_con[parent] < _con[child])
{
swap(_con[parent], _con[child]);
//迭代------更新孩子坐标,重新计算孩子的父亲
child = parent;
parent = (child - 1) / 2;
}
//否则就结束循环
else
break;
}
}
【注意】循环结束条件是child==0而不能是parent==0,因为当parent==0了还要走完下一轮比较。
② pop删除

【堆的删除】------"三步走"
- 头尾互换
- 删除最后一个元素
- 向下调整


③ top、size、empty

构造函数可以不用写,优先级队列的简单实现就结束了。
(3)迭代区间初始化

cpp
//迭代区间初始化
template <class InputIterator>
priority_queue(InputIterator first, InputIterator last)
{
//全部插入------数据插入完,还不是堆
while (first != last)
{
_con.push_back(*first); //调用优先级队列的push_back,相当于向上调整建堆------效率略低于向下调整建堆
++first;
}
//向下调整建堆------从最后一个父节点(非叶子结点)开始
for (int i = (_con.size() - 1 - 1) / 2; i >= 0; i--) //最后一个结点size-1的父节点就用"先减一后除二"的公式,就是(size-1-1)/2
{
adjust_down(i);
}
}
(4)测试

(5)仿函数
【之前建小堆的思路】改变adjust_down/up的if括号内比较表达式的<或>符号。
C语言通过传递函数指针来解决这个问题,C++不喜欢使用函数指针(太复杂)。
C++给出了一个新概念------仿函数(函数对象)。
- 仿函数(函数对象):重载了operator()的类,用类实例化的对象可以像函数一样使用。
- operator():参数个数和返回值根据需求确定,不固定,很灵活。

仿函数可以考虑实现成struct------默认是公有。

仿函数的作用:传值给这个对象,可以完成预定任务。

注意类型匹配:

仿函数建议实现成类模版。

operator()的特点:
- 参数个数、返回值------根据需求确定,不固定。
- 相比于之前学过的一些运算符重载函数而言,更加灵活。
- 这个特点决定了它的功能非常多样化。
(6)增加仿函数作为模版参数

sort排升序、降序的仿函数设计也是这样子搞的:

用这个模版参数(仿函数的类)实例化出对象,用这个对象实现比较大小。


(7)测试

【模版的思想】**用你传递的具体的模版参数类型,实例化出特定的东西。
【大堆or小堆】通过仿函数控制,仿函数控制堆里面的比较逻辑。
【仿函数vs函数指针】仿函数就是类+运算符重载**,相比于函数指针是更简单的。
在优先级队列(堆)中存放自定义类型:

改为存储日期类的指针:

结果并不是按日期类的升序 ,因为是按日期类的指针的升序。
最终打印结果每次运行都不一样,有30-28-29、28-29-30、28-30-29、29-30-28、29-28-30......。
new(malloc)是带有随机性的,并不是后new的空间的地址一定大。
此时就可以写一个仿函数------自定义建堆方式:按++插入数据的解引用的值++比较建堆。

除了控制简单的大于小于比较逻辑,我们还可以控制仿函数实现更复杂的比较逻辑,来建造特定的堆。

仿函数在这里就充当了一种回调函数的作用。

