当别人都在关注你飞的有多高的时候,只有父母在关心你飞的累不累。💓💓💓
目录
✨说在前面
🍋知识点一:stack
•🌰1.stack介绍
•🌰2.stack的基本操作
🍋知识点二:queue
•🌰2.queue介绍
•🌰2.queue的基本操作
🍋知识点三:priority_queue
•🌰1.priority_queue介绍
•🌰2.priority_queue的基本使用
•🌰3.仿函数
🔥仿函数控制序列单调性
🔥仿函数在优先级队列中的应用
🍋知识点四:容器适配器
•🌰1.什么是适配器
•🌰2.stack和queue的底层结构
•🌰3.deque介绍
🔥deque的实现原理
🔥deque的缺陷
🔥选择deque的原因
• ✨SumUp结语
✨说在前面
亲爱的读者们大家好!💖💖💖,我们又见面了,上一篇文章我给大家介绍了一下list的定义、常用接口以及模拟实现。如果大家没有掌握好相关的知识,上一篇篇文章讲解地很详细,可以再回去看看,复习一下,再进入今天的内容。
我们今天简单给大家讲解一下STL中的两大适配器——stack和queue。stack和queue分别对应C语言中的栈和队列,如果大家准备好了,那就接着往下看吧~
👇👇👇
💘💘💘知识连线时刻(直接点击即可)【C++】_string类字符串万字详细解析
【C++】_vector定义、_vector常用方法解析
【C++】_list常用方法解析及模拟实现
🎉🎉🎉复习回顾🎉🎉🎉
博主主页传送门:愿天垂怜的博客
🍋知识点一:stack
•🌰1.stack介绍
stack是一个容器适配器,它提供了一种后进先出(LIFO, Last In First Out)的数据结构。stack只允许在容器的顶部进行元素的添加(push)和移除(pop)操作,以及访问顶部元素(top)的功能,但不提供遍历容器内部元素的功能。
我们来查看一下文档中对stack的介绍:
注意:stack不属于容器,而是一种容器适配器。它看起来像是容器,但实际上它们是通过封装其他容器来工作的。
•🌰2.stack的基本操作
stack的使用接口如下:
函数说明 | 接口说明 |
stack() | 构造空的栈 |
empty() | 检测stack是否为空 |
size() | 返回stack中元素的个数 |
top() | 返回栈顶元素的引用 |
push()
| 将元素val压入stack中 |
pop() | 将stack中尾部的元素弹出 |
它的接口非常简单,实现部分可以参考C语言的写法:C语言实现栈和队列
如果大家觉得不好理解,可以看我之前写的栈和队列的博客:【数据结构】栈和队列超详细讲解
接口文档如下:
stack::stack - C++ Reference (cplusplus.com)
stack::empty - C++ Reference (cplusplus.com)
stack::size - C++ Reference (cplusplus.com)
stack::top - C++ Reference (cplusplus.com)
stack::push - C++ Reference (cplusplus.com)
stack::emplace - C++ Reference (cplusplus.com)
stack::pop - C++ Reference (cplusplus.com)
stack::swap - C++ Reference (cplusplus.com)
🍋知识点二:queue
•🌰2.queue介绍
queue是一种先进先出(FIFO, First In First Out)的数据结构。它允许在队尾添加元素(enqueue/push),在队首移除元素(dequeue/pop),以及查看队首元素(front)的值。与stack类似,queue也不提供遍历功能。
我们来查看一下文档中对queue的介绍:
注意:queue不属于容器,而是一种容器适配器。它看起来像是容器,但实际上它们是通过封装其他容器来工作的。
•🌰2.queue的基本操作
stack的使用接口如下:
函数说明 | 接口说明 |
queue() | 构造空的队列 |
empty() | 检测队列是否为空,是返回true,否则返回false |
size() | 返回队列中元素的个数 |
front() | 返回队头元素的引用 |
back() | 返回队尾元素的引用 |
push() | 在队尾将元素val入队列 |
pop() | 将队头元素出队列 |
它的接口非常简单,实现部分可以参考C语言的写法:C语言实现栈和队列
如果大家觉得不好理解,可以看我之前写的栈和队列的博客:【数据结构】栈和队列超详细讲解
接口文档如下:
queue::queue - C++ Reference (cplusplus.com)
queue::empty - C++ Reference (cplusplus.com)
queue::size - C++ Reference (cplusplus.com)
queue::front - C++ Reference (cplusplus.com)
queue::back - C++ Reference (cplusplus.com)
queue::push - C++ Reference (cplusplus.com)
queue::emplace - C++ Reference (cplusplus.com)
queue::pop - C++ Reference (cplusplus.com)
queue::swap - C++ Reference (cplusplus.com)
🍋知识点三:priority_queue
•🌰1.priority_queue介绍
学习完了队列,我们再来了解一下优先级队列:
C++中的优先级队列(Priority Queue)是一种特殊的队列,它的元素被赋予优先级,元素的出队顺序基于它们的优先级,而不是它们被加入队列的顺序。默认情况下,priority_queue使用最大堆(Max Heap)来实现,所以队列中最大的元素总是位于队列的顶部,因此这个元素会首先被移除(即出队)。
•🌰2.priority_queue的基本使用
优先级队列默认使用vector作为其底层存储数据的容器,在vector上又使用了堆算法将vector中元素构造成堆的结构,因此priority_queue就是堆,所有需要用到堆的位置,都可以考虑使用priority_queue。注意:默认情况下priority_queue是大堆。
相同地,priority_queue也有如下接口:
函数声明 | 接口说明 |
priority_queue()/priority_queue(first,
last)
| 构造一个空的优先级队列 |
empty( ) | 检测优先级队列是否为空,是返回true,否则返回false |
top( ) | 返回优先级队列中最大(最小元素),即堆顶元素 |
push(x) | 在优先级队列中插入元素x |
pop() |
删除优先级队列中最大
(
最小
)
元素,即堆顶元
素
|
接口文档如下:
priority_queue::priority_queue - C++ Reference (cplusplus.com)
priority_queue::empty - C++ Reference (cplusplus.com)
priority_queue::size - C++ Reference (cplusplus.com)
priority_queue::top - C++ Reference (cplusplus.com)
priority_queue::push - C++ Reference (cplusplus.com)
priority_queue::emplace - C++ Reference (cplusplus.com)
priority_queue::pop - C++ Reference (cplusplus.com)
priority_queue::swap - C++ Reference (cplusplus.com)
•🌰3.仿函数
仿函数(Functors)是那些重载了()操作符的对象,它们的行为类似于函数。通过使用仿函数,你可以将函数的行为封装在对象内部,这样做的好处包括能够传递额外的状态信息、支持泛型编程(例如,与标准库算法一起使用时),以及更好的封装性。
🔥仿函数控制序列单调性
下面是一个仿函数示例,用于控制冒泡排序将原先序列按递增还是递减顺序排列:
//递增
template<class T>
class Less
{
public:
bool operator()(const T& x, const T& y)
{
return x < y;
}
};
//递减
template<class T>
class Greater
{
public:
bool operator()(const T& x, const T& y)
{
return x > y;
}
};
//冒泡排序
template<class Compare>
void BubbleSort(int* arr, int length, Compare com)
{
assert(arr);
int flag = 1;
while (flag && length--)
{
flag = 0;
for (int i = 0; i < length; i++)
{
//if (arr[i] > arr[i + 1])
if (com(arr[i], arr[i + 1]))
{
swap(arr[i], arr[i + 1]);
flag = 1;
}
}
}
}
//打印数组元素
void printArr(int* arr, int length)
{
for (size_t i = 0; i < length; i++)
{
cout << arr[i] << " ";
}
cout << endl;
}
int main()
{
Less<int> LessFunc;
Greater<int> GreaterFunc;
int arr[] = { 5,23,789,12,8,1,86,12,7 };
//升序
BubbleSort(arr, 9, GreaterFunc);
printArr(arr, 9);
//降序
BubbleSort(arr, 9, LessFunc);
printArr(arr, 9);
return 0;
在这个例子中,Less类和Greater类是仿函数,它重载了()操作符以比较两个整数。我们创建了Less或Greater的实例并将其作为第三个参数传递给BubbleSort函数,以指定排序的准则。
结果如下:
1 5 7 8 12 12 23 86 789
789 86 23 12 12 8 7 5 1
🔥仿函数在优先级队列中的应用
利用仿函数,我们可以控制优先级队列的底层是大堆还是小堆,进而控制优先级队列的优先级。我们默认优先级队列的底层是大堆。
它的实现用到了仿函数,可以参考我这里的实现:优先级队列模拟实现
优先级队列举例:
#include <iostream>
using namespace std;
#include <queue>
int main()
{
priority_queue<int> pq;
pq.push(3);
pq.push(7);
pq.push(1);
pq.push(9);
pq.push(10);
while (!pq.empty())
{
cout << pq.top() << " ";
pq.pop();
}
return 0;
}
结果如下:
10 9 7 3 1
🍋知识点四:容器适配器
•🌰1.什么是适配器
适配器是一种设计模式(设计模式是一套倍反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结),该种模式是一个类的接口转换成客户希望的另外一个接口。
•🌰2.stack和queue的底层结构
虽然stack和queue中也可以存放元素,但在STL中并没有将其划分在容器的行列,而是将其称为容器适配器,这是因为stack和队列只是对其他容器的接口进行了包装,STL中stack和queue默认使用deque,比如:
我们来看看stack的模拟实现:
namespace stl_stack
{
//Container适配转换出stack
template<class T, class Container>
class stack
{
public:
void push(const T& x)
{
_con.push_back(x);
}
void pop()
{
_con.pop_back();
}
const T& top()
{
return _con.back();
}
size_t size() const
{
return _con.size();
}
bool empty()
{
return _con.empty();
}
private:
Container _con;
};
}
再来看看stack的模拟实现:
很显然我们发现,这里容器的缺省值给的是deque<T>,这是个什么东西呢?继续往下看。
•🌰3.deque介绍
🔥deque的实现原理
deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高。
deque并不是真正连续的空间, 而是由一段段连续的小空间拼接而成的,实际deque类似于一个动态的二维数组,其底层结果如下图所示:
双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其“整体连续”以及随机访问的假象,落在了deque的迭代器身上,因此deque的迭代器设计就比较复杂,如下图所示:
🔥deque的缺陷
与vector相比,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不需要搬移大量的元素,因此其效率是比vector高的。
与list比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段。
但是,deque有一个致命缺陷:不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构。
我们可以用下面的代码来测试vector和deque的[]利率:
//deque与vector[]的效率对比
void test_op1()
{
srand(time(0));
const int N = 1000000;
deque<int> dq;
vector<int> v;
for (int i = 0; i < N; ++i)
{
auto e = rand() + i;
dq.push_back(e);
v.push_back(e);
}
int begin1 = clock();
sort(dq.begin(), dq.end());
int end1 = clock();
int begin2 = clock();
sort(v.begin(), v.end());
int end2 = clock();
printf("deque:%d\n", end1 - begin1);
printf("vector:%d\n", end2 - begin2);
}
void test_op2()
{
srand(time(0));
const int N = 1000000;
deque<int> dq1;
deque<int> dq2;
for (int i = 0; i < N; ++i)
{
auto e = rand() + i;
dq1.push_back(e);
dq2.push_back(e);
}
int begin1 = clock();
sort(dq1.begin(), dq1.end());
int end1 = clock();
int begin2 = clock();
//拷贝到vector
vector<int> v(dq2.begin(), dq2.end());
sort(v.begin(), v.end());
dq2.assign(v.begin(), v.end());
int end2 = clock();
printf("deque sort:%d\n", end1 - begin1);
printf("deque copy vector sort, copy back deque:%d\n", end2 - begin2);
}
int main()
{
test_op1();
test_op2();
return 0;
}
realse且x86结果如下:
deque:117
vector:66
deque sort:104
deque copy vector sort, copy back deque:59
基本上相差两倍左右。
🔥选择deque的原因
那么问题来了,为什么选择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的优点,而完美的避开了其缺陷。
• ✨SumUp结语
到这里本篇文章的内容就结束了,本节介绍了C++中_stack_queue的相关知识。这里的内容虽然很熟悉了,而且也很简单。但是也希望大家能够认真学习,打好基础,迎接接下来的挑战,期待大家继续捧场~💖💖💖