C++初阶(15)stack、queue、优先级队列、双端队列

C++初阶(15)stack、queue、优先级队列、双端队列

1. stack的介绍和使用

1.1 stack的介绍

stack的文档介绍

【说明】

1. stack是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。

2. stack是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部(即栈顶)被压入和弹出。

3. stack的底层容器,可以是任何标准的容器类模板,或者一些其他特定的容器类。

4. 这些容器类应该支持以下操作:empty:判空操作back:获取尾部元素操作push_back:尾部插入元素操作pop_back:尾部删除元素操作

4. 标准容器vector、deque、list均符合这些需求,默认情况下,如果没有为stack指定特定的底层容器,默认情况下使用deque。

1.2 stack的使用

1.2.1 接口说明

函数说明
接口说明
构造空的栈
检测stack是否为空
返回stack中元素的个数
返回栈顶元素的引用
将元素val压入stack中
将stack中尾部的元素弹出

1.2.2 代码测试

1.2.3 OJ练习

1.2.2.1 最小栈

最小栈:是一个栈,支持后进先出,但是增加了一个接口——获取最小元素。

而且要求时间复杂度O(1),不能是O(N)遍历。


思路1:用一个min记录一下最小元素——每次入栈的时候,都用入栈元素跟min比较一下,如果比min小就更新min。

问题1:删栈顶元素之后,min该如何更新——得遍历一遍,但栈不允许遍历(没有提供迭代器),就算允许遍历,时间复杂度也是O(N) 不是O(1)。


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

用C++的代码,就不需要造轮子了,也不用写构造,全是自定义类型成员变量,默认构造就可以。

//最小栈类 //成员变量: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没出完,都可以验不匹配)
如果把入栈序列(数组)和出栈序列(数组)直接比较,不相等就++,而不是借助一个第三方的栈这样一个数据结构来辅助判断,就很麻烦了。

算法能力跟看得算法课多少没多大关系,不是看了就会了,重要的是学会这些方法和思路。

逻辑梳理顺了,代码就很好写了,大框架是这样,无非是一些细节上面的控制。这些算法题,代码已经不那么重要了,重要的是思路。画图把思路理清楚。
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 逆波兰表达式求值
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.3 stack的模拟实现

从栈的接口中可以看出,栈实际是一种特殊的vector,因此使用vector完全可以模拟实现stack。

#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.1 queue的介绍

queue的文档介绍

【说明】

1. 队列是一种容器适配器,专门用于在FIFO上下文(先进先出)中操作,其中从容器一端插入元素,另一端提取元素。

2. 队列作为容器适配器实现,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从队尾入队列,从队头出队列。

3. 底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。该底层容器应至少支持以下操作:empty:检测队列是否为空size:返回队列中有效元素的个数front:返回队头元素的引用back:返回队尾元素的引用push_back:在队列尾部入队列pop_front:在队列头部出队列

4. 标准容器类deque和list满足了这些要求。默认情况下,如果没有为queue实例化指定容器类,则使用标准容器deque。

2.2 queue的使用

2.2.1 接口说明

函数声明
接口说明
构造空的队列
检测队列是否为空,是返回true,否则返回false
返回队列中有效元素的个数
返回队头元素的引用
返回队尾元素的引用
在队尾将元素val入队列
将队头元素出队列

2.2.2 OJ练习

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.3 queue的模拟实现

因为queue的接口中存在头删和尾插,因此使用vector来封装效率太低,故可以借助list来模拟实现queue,具体如下:

#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.2 STL标准库中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.1 deque的原理介绍

deque(Double-End Queue,双端队列):是一种双开口“连续”空间的数据结构。双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1)。与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高。


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

第一段buff的地址不是存储在中控数组的首元素,而是中间,这样头插元素(头插buff数组)之后可以把头插的buff数组地址放到之前第一段buff数组地址的前面。


deque的缺陷:

  • []访问不够极致,比vector慢;
  • 在头尾插删还好,在某个满buff内部插删——思路1:数据挪动;思路2:对buff扩容,但这会导致每个buff的大小不一样了,那[]访问就需要逐个减每个buff的大小,例如前三个buff大小为10和12和15,访问第25个元素就需要27-10-12=5,访问第3个buff数组的第5个元素。

这就是说想:

  • 保持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作为其底层容器,主要是因为:

1. stack和queue不需要遍历(因此stack和queue没有迭代器),只需要在固定的一端或者两端进行操作。

2. 在stack中元素增长时,deque比vector的效率高(扩容时不需要搬移大量数据);queue中的元素增长时,deque不仅效率高,而且内存使用率高。

结合了deque的优点,而完美的避开了其缺陷。

3.5 STL标准库中对于stack和queue的模拟实现

3.5.1 stack的模拟实现

#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.2 queue的模拟实现

#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.1 priority_queue的介绍

priority_queue文档介绍

priority_queue:有一个container参数,需要传一个容器去适配,所以优先级队列是一个容器适配器。

priority_queue默认使用vector进行适配。
【问】为什么stack和queue默认用deque去适配,而priority_queue默认用vector?

【答】因为priority_queue需要大量使用[]访问——底层是(二叉)堆。最大元素:大堆;最小元素:小堆;
STL库没有现成的堆,有堆算法;要使用堆就直接使用优先级队列——优先级队列就是堆;

【说明】

1. 优先队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的。

2. 此上下文类似于堆,在堆中可以随时插入元素,并且只能检索最大堆元素(优先队列中位于顶部的元素)。

3. 优先队列被实现为容器适配器,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从特定容器的“尾部”弹出,其称为优先队列的顶部。

4. 底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。容器应该可以通过随机访问迭代器访问,并支持以下操作:empty():检测容器是否为空size():返回容器中有效元素个数front():返回容器中第一个元素的引用push_back():在容器尾部插入元素pop_back():删除容器尾部元素

5. 标准容器类vector和deque满足这些需求。默认情况下,如果没有为特定的priority_queue类实例化指定容器类,则使用vector。

6. 需要支持随机访问迭代器,以便始终在内部保持堆结构。容器适配器通过在需要时自动调用算法函数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】默认大堆

1. 默认情况下,priority_queue是大堆。

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

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


【注意】如果数据不在vector里面,而是在数组int arr [10]里面,也可以用迭代器构造,同时数组也可以用sort排序。
【注意】数组——连续的物理空间,可以直接传原生指针给迭代器参数。实现vector的迭代器就可以直接使用原生指针。

【注意】建小堆需要传仿函数。
【注意】要显式给第3个模版参数,就要先把前2个模版参数给出来。
#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

2. 如果在priority_queue中放自定义类型的数据,用户需要在自定义类型中提供>或者<的重载。
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中的使用

数组中第K个大的元素

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.4 priority_queue的模拟实现

priority_queue的底层结构就是堆,因此此处只需对堆进行通用的封装即可。


(1)基本框架

(2)成员函数

① push插入

逻辑上是二叉树,物理上是数组。

【堆的插入】——“两步走”数据尾插数据向上调整
//向上调整——需要支持下标访问(计算父节点) 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)迭代区间初始化

//迭代区间初始化 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的空间的地址一定大。


此时就可以写一个仿函数——自定义建堆方式:按插入数据的解引用的值比较建堆。
除了控制简单的大于小于比较逻辑,我们还可以控制仿函数实现更复杂的比较逻辑,来建造特定的堆。
仿函数在这里就充当了一种回调函数的作用。

Read more

【Python】正则表达式的艺术:轻松驾驭 Python 的re库

【Python】正则表达式的艺术:轻松驾驭 Python 的re库

🏠大家好,我是Yui_,目标成为全栈工程师~💬 🍑如果文章知识点有错误的地方,请指正!和大家一起学习,一起进步👀 🚀如有不懂,可以随时向我提问,我会全力讲解~ 🔥如果感觉博主的文章还不错的话,希望大家关注、点赞、收藏三连支持一下博主哦~! 🔥你们的支持是我创作的动力! 🧸我相信现在的努力的艰辛,都是为以后的美好最好的见证! 🧸人的心态决定姿态! 💬欢迎讨论:如有疑问或见解,欢迎在评论区留言互动。 👍点赞、收藏与分享:如觉得这篇文章对您有帮助,请点赞、收藏并分享! 🚀分享给更多人:欢迎分享给更多对编程感兴趣的朋友,一起学习! 文章目录 * 1.案例引入 * 2.正则表达式 * 2.1 核心概念 * 3.正则表达式的语法 * 3.1 正则:`.` * 3.2 正则: `\d` * 3.3 正则:`\D`

By Ne0inhk
Python 多线程日志错乱:logging.Handler 的并发问题

Python 多线程日志错乱:logging.Handler 的并发问题

Python 多线程日志错乱:logging.Handler 的并发问题 🌟 Hello,我是摘星! 🌈 在彩虹般绚烂的技术栈中,我是那个永不停歇的色彩收集者。 🦋 每一个优化都是我培育的花朵,每一个特性都是我放飞的蝴蝶。 🔬 每一次代码审查都是我的显微镜观察,每一次重构都是我的化学实验。 🎵 在编程的交响乐中,我既是指挥家也是演奏者。让我们一起,在技术的音乐厅里,奏响属于程序员的华美乐章。 目录 Python 多线程日志错乱:logging.Handler 的并发问题 摘要 1. 问题现象与复现 1.1 典型的日志错乱场景 2. logging模块的线程安全机制分析 2.1 Handler级别的线程安全 2.2 锁竞争的性能影响分析 3. 深入源码:竞态条件的根本原因 3.1 Handler.emit()方法的竞态分析 3.2 I/O操作的原子性问题

By Ne0inhk
【2026 最新】Python 与 PyCharm 详细下载安装教程 带图展示(Windows 版)

【2026 最新】Python 与 PyCharm 详细下载安装教程 带图展示(Windows 版)

前言 Python 是当今最流行的编程语言之一,广泛应用于 Web 开发、数据分析、人工智能、自动化脚本等领域。而 PyCharm 作为 JetBrains 公司推出的 Python 专业集成开发环境(IDE),凭借智能代码补全、调试器、虚拟环境管理、版本控制集成等强大功能,成为众多开发者首选工具。 本教程专为 Windows 系统用户 编写,将手把手指导你完成 Python 解释器 和 PyCharm IDE 的下载、安装与基础配置,助你快速搭建本地 Python 开发环境。 一、Python 下载与安装 1.1 访问 Python 官网 打开浏览器,访问 Python 官方网站:Download

By Ne0inhk
ksycopg2实战:Python连接KingbaseES数据库的完整指南

ksycopg2实战:Python连接KingbaseES数据库的完整指南

摘要:本文详细介绍了KingbaseES数据库的Python专用驱动ksycopg2的使用方法。内容涵盖驱动安装、连接配置、CRUD操作等基础功能,以及事务管理、连接池等高级特性。ksycopg2作为遵循Python DBAPI 2.0规范的线程安全适配器,针对KingbaseES进行了深度优化,支持数据类型映射、批量操作等特性。文章提供了完整的业务表创建示例和员工管理系统实战案例,包含环境配置、性能优化建议和常见问题解决方案,帮助开发者快速掌握该驱动的使用技巧。通过详细的代码示例,展示了如何高效安全地操作KingbaseES数据库。 一、安装ksycopg2:KingbaseES的Python ksycopg2是 专为KingbaseES数据库设计的Python适配器 ,完全遵循Python DB API 2.0规范,具有线程安全的特性。它不仅提供了高效的数据操作能力,还支持KingbaseES特有的功能特性。 与通用的PostgreSQL驱动psycopg2相比,ksycopg2针对KingbaseES进行了深度优化,特别是在数据类型映射、事务处理和高级功能支持方面表现更加

By Ne0inhk