目录
之前在数据结构中讲过【数据结构】栈和队列-CSDN博客,在C++中 stack 和 queue 又会有哪些不同呢?
在C++ STL(Standard Template Library)标准库中,stack、queue、priority_queue 是常用的容器适配器,它们基于其它基本容器(例如 std::deque 以及 std::vector)进行实现,为程序员提供了一种基于 LIFO(Last In First Out)和 FIFO(First In First Out)规则的数据结构。
一、stack
1.1 stack 介绍
- stack是一种容器适配器,用于具有后进先出操作的环境中,只能从容器的一端进行元素的插入、提取和删除操作。
- stack是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层,元素从特定容器的尾部(栈顶)被压入和弹出。
- stack的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类,这些容器类应该支持以下操作:
empty:判空操作, back:获取尾部元素操作,
push_back:尾部插入元素操作, pop_back:尾部删除元素操作。- 标准容器vector、deque、list均符合这些需求,默认情况下,如果没有为stack指定特定的底层容器, 默认情况下使用deque。
stack 是一个后进先出(LIFO)的数据结构,支持 push(),pop(),top() 和 empty() 等操作。使用堆栈可以避免程序调用函数时的过程冲突,因为每个函数都有自己独立的内存空间。
1.2 stack 使用
常用函数 | 接口说明 |
构造栈 | |
empty() | 检测stack是否为空 |
返回stack中元素的个数 | |
返回栈顶元素的引用 | |
将元素val压入stack中 | |
将stack中尾部的元素弹出 |
void Test1()
{
stack<int> s;
s.push(1);
s.push(2);
s.push(3);
s.push(4);
s.push(5);
cout << "The top element is: " << s.top() << endl;
cout << "The size is: " << s.size() << endl;
while (!s.empty())
{
cout << s.top() << " ";
s.pop();
}
cout << endl;
}
二、queue
2.1 queue 介绍
- 队列是一种容器适配器,用于具有FIFO(先进先出)操作的环境中操作,从容器一端插入元素,另一端提取元素。
- 队列作为容器适配器实现,queue提供一组特定的成员函数来访问其元素。元素从队尾入队列,从队头出队列。
- 底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。该底层容器应至少支持以下操作:
empty:检测队列是否为空 size:返回队列中有效元素的个数
front:返回队头元素的引用 back:返回队尾元素的引用
push_back:在队列尾部入队列 pop_front:在队列头部出队列- 标准容器类deque和list满足了这些要求。默认情况下,如果没有为queue实例化指定容器类,则使用标准容器deque。
2.2 queue 使用
queue 是一个先进先出(FIFO)的数据结构,支持 push(),pop(),front(),back() 和 empty() 等操作。
函数声明 | 接口说明 |
构造队列 | |
检测队列是否为空,是返回true,否则返回false | |
返回队列中有效元素的个数 | |
返回队头元素的引用 | |
返回队尾元素的引用 | |
在队尾将元素val入队列 | |
将队头元素出队列 |
#include <queue>
#include <iostream>
using namespace std;
void Test2()
{
queue<int> q;
q.push(1);
q.push(2);
q.push(3);
q.push(4);
q.push(5);
cout << "The front element is: " << q.front() << endl;
cout << "The back element is: " << q.back() << endl;
q.pop();
cout << "The size is: " << q.size() << endl;
}
int main()
{
Test2();
return 0;
}
三、priority_queue
3.1 priority_queue 介绍
- 优先队列是一种容器适配器,默认根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的。
- 此上下文类似于堆,在堆中可以随时插入元素,并且只能检索最大/小堆元素(优先队列中位于顶部的元素)。
- 优先队列被实现为容器适配器,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从特定容器的“尾部”弹出,其称为优先队列的顶部。
- 底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。
- 标准容器类vector和deque满足这些需求。默认情况下,如果没有为特定的priority_queue类实例化指定容器类,则使用vector。
- 需要支持随机访问迭代器,以便始终在内部保持堆结构。容器适配器通过在需要时自动调用算法函数make_heap、push_heap和pop_heap来自动完成此操作。
3.2 priority_queue 使用
priority_queue 是一个优先队列,元素按照一定顺序排列,每个元素都有一个权值或优先级。支持 push(),pop(),top() 和 empty() 操作。
函数声明 | 接口说明 |
构造一个空的优先级队列 | |
检测优先级队列是否为空,是返回true, 否则返回false | |
返回优先级队列中有效元素的个数 | |
返回优先级队列中最大(最小元素),即堆顶元素 | |
在优先级队列中插入元素 | |
删除优先级队列中最大(最小)元素,即堆顶元素 |
void Test3()
{
priority_queue<int> pq;
pq.push(4);
pq.push(1);
pq.push(2);
pq.push(5);
pq.push(3);
cout << "The top element is: " << pq.top() << endl;
cout << "The size is: " << pq.size() << endl;
while (!pq.empty())
{
cout << pq.top() << " ";
pq.pop();
}
cout << endl;
}
int main()
{
Test3();
return 0;
}
3.3 priority_queue 注意事项
- priority_queue 的构造函数中有一个参数是 const Compare& comp = Compare(),作用是确定优先级队列的排序方式。
- priority_queue 默认情况下是大根堆(使用less<T>),即元素按照从大到小的顺序排列。如果需要使用小根堆,可以使用greater<T>(调用时使用greater()仿函数)传给 priority_queue。
模板参数传的是less<T> 或greater<T> ,不是less<T>() 或greater<T>()- 如果在priority_queue中放自定义类型的数据,用户需要在自定义类型中提供> 或者< 的重载。
四、deque
4.1 deque 介绍
deque(double-ended queue双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高。
deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际deque类似于一个动态的二维数组,其底层结构如下图所示:
如果deque已经满载,需要再找一块更大的区间作为deque
双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其“整体连续”以及随机访问的假象,落在了deque的迭代器身上,因此deque的迭代器设计就比较复杂,如下图所示:
4.2 deque 缺陷
如果deque 真的结合了vector 和 list 的优点,那么vector 和 list就不会被广泛使用了。
- 与vector比较,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不 需要搬移大量的元素,因此其效率是必vector高的。
- 与list比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段。
- 但是,deque有一个致命缺陷:不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构。
- 有一点需要思考的是:deque支持随机访问和插入删除,如果是插入,例如在上面的10后面增加一个数字11,有两种方式,一种是让当前数组的长度加一,在后面加上11;第二种方式是保持每个数组大小不变,都是10,那么就需要把10后面所有数字都移动一下,移动需要较长时间。
看上去第一种方式效率更高,但其实第一种遍历的时候需要花更长时间,因为要计算每个数组的大小。