【C++】STL:stack/queue/priority_queue/deque
来喽,STL的栈和队列!
[TOC]
1.Stack
栈是一个遵循LIFO
规则的容器,即后进先出(last in first out)。后放入容器内的数据会先出来。
如果你不太理解栈的性质,可以先看看我写的C语言栈的博客【链接】
打开Cplusplus一看,栈的函数肉眼可见的少。这和我们C语言实现的功能基本是一样的。
它甚至没有拷贝构造!
1.1 容器适配器
等会,这个container
是什么玩意?
别急,先让我们来看看栈的类定义👇
1 | std::stack |
和之前不同的是,这里出现了container
,和一个我们好像没有接触过的deque
搜索后得知,deque
也是一种容器,被称为双端队列
- 那么栈为什么需要一个容器呢?
实际上,栈不是直接写出来的代码,而是借用了deque
这个容器,进行封装后的结果。
意思就是,栈其实是解用了别人的代码!
比如deque
有头插头删,尾插尾删。而我们的栈只需要尾插尾删,那么我们包装一下deque,只提供尾插尾删的函数,不就成了栈了?
1.2 模拟实现
直接这么说可能有点干巴巴,我们直接上代码!
1 | template<class T, class Container = deque<T>> |
以上便是栈的模拟实现代码!
What?这就结束了?——甚至写的比我们C语言版本的栈更少!
本质上,我们只需要创建一个Container
的对象_con
,再借助这个对象的函数进行封装,就完成了一个栈!
因为模板里面默认传入的是deque
,所以这里用的便是deque
的函数了。这就好比函数传入了一个缺省值。你可以将deque
换成其他有尾插尾删函数的容器,比如vector
测试一下,莫得问题!
2.queue
和栈不同,队列遵循FIFO
,即先进先出(first in first out)
同样的,我们只需要包装一下容器,即可完成模拟实现
1 | std::queue |
2.1 模拟实现
1 | template<class T, class Container = deque<T>> |
需要注意的是,队列就不能用vector
来当作容器辣!因为vector
头插删的效率很低,需要挪动数据。我们可以使用list
来当作容器
当然,你可以把头插头删改成insert和erase,但是那样没有意义,依旧拖慢了效率
因为栈和队列压根不支持迭代器,自然也没有迭代器失效问题!
2.2 关于queue的front
学习linux课程的时候,突然想到一个问题,如果queue里面是空的,那么我直接访问front会是什么情况?会出现异常吗?
1 |
|
在linux下用g++编译运行,发现获取到的值是0
1 | [muxue@bt-7274:~/git/test]$ ./test |
简单概括,如果队列中没有数据,此时调用front接口不会报错,但是获取到的数据是无效的。
个人猜测,这里能否获取到数据也和编译器的优化有关。比如相同的代码,在vs2019
下,会直接报assert错误!
所以,为了代码的兼容性,请严格检查调用front的时候队列里面有没有数据,stack的top同理
了解完相对简单的栈和队列之后,我们可以直接来看看优先级队列
3.priority_queue 优先级队列
优先级队列和queue同属于头文件<queue>
1 | template <class T, class Container = vector<T>, |
- 那么它和普通的队列有什么不同呢?
优先级队列是一个堆,且默认是大堆
如果你不知道堆是什么,可以看看我之前C语言数据结构的博客【链接】
简单说来,堆的堆顶(这里是第一个数据)一直保持着最大值或者最小值。每一次插入、删除操作,都需要向上、向下重新调整容器内元素的位置
3.1 make_heap
这里简单说一下另外一个函数make_heap
,这个函数可以通过迭代器区间,将区间内的数据调整为一个堆。
在文档介绍里面可以看到,最后一行提到了优先级队列就是自动调用make_heap
函数来维护自己堆的属性的
3.2 函数接口
优先级队列本质上还是一个队列,它提供的函数也很少
不同的是,这里的top
堆顶指的是容器中首个元素,pop
也是移除堆顶元素
3.3 仿函数
注意这里出现了第三个模板参数,Compare
这是一个仿函数,其就是一个类,但是仿照了函数的调用方式
想做到这一点,我们需要重载()
操作符
1 | //仿函数,重载()操作符 |
比如这里的less
和greater
就是两个仿函数,优先级队列需要用它来进行比较。这样我们就可以快速在大堆和小堆直接进行切换。
它们的使用方式和函数是一样的!不过在使用之前,我们需要先创建一个对象
1 | less func; |
库函数中的less和greater都在<functional>
这个头文件中
- std库中优先级队列默认传的less小于比较,为大堆!
- 我们可以手动传入greater,即可变成小堆
3.4 模拟实现
在3.3
中,我们已经实现出了库函数中的less和greater这两个仿函数,接下来我们只需要包装一下优先级队列的接口,即可使用!
1 | template <class T, class Container = vector<T>,class Compare = less<T> > |
这里需要注意的便是adjustdown/up
这两个用来维持堆属性的调整函数,你可以看我之前的博客,里面有关于这部分的详细解释!回到上面
先来测试一下默认大堆的属性
可以看到,打印出来的数据是按从大到小顺序存放的!
如果我们显示传入greatre仿函数,就会变成从小到大
同时,即便我们pop
了一部分数据,也不会影响堆的属性
优先级队列在一部分地方可以用于帮我们找到一段数据中的topK和第n个最大/小的数据
比如下面这道leetcode OJ
题目215. 数组中的第K个最大元素
我们只需要pop掉前面的K-1个元素,这时候的堆顶就是我们需要的第K个最大值
4.deque
前面已经提到了deque
这个容器,它拥有的库函数和vector
很相似,这里我们不关注如何使用这个容器,而是来谈谈这个“双端队列”相比于vector/list
有没有什么优势
1 | std::deque |
4.1存储结构
deque最特殊的就是它的存储结构了。它并不是一个连续的结构,也不像链表一样每个数据都有自己的单独节点
这个是百度百科上找到的一张图片
其实deque
有点类似二维数组,其中有一个头指针指向队列头,一个尾指针指向队列尾部。主数组“中控器”存放的则是这些数组的指针。注:每一个数组的容量大小都是一样的,所以存放在中间的数组一定是满的!
这样我们就避免了list节点分散,需要多次开空间,无法随机访问的缺陷;又可以灵活快速地进行头插头删操作,而不需要挪动数据。
当然,deque
的随机访问性能肯定不如数据连续存储的vector
,其更适合下面的场景:
- 需要高性能的头插头删,尾插尾删
- 偶尔需要进行随机访问
结语
关于栈和队列的部分到这里就结束辣!
我们还认识了双端队列deque
,以及让其作为容器的模拟实现操作
有任何问题,欢迎评论提出!