【C++游记】栈vs队列vs优先级队列
枫の个人主页
你不能改变过去,但你可以改变未来
算法/C++/数据结构/C
Hello,这里是小枫。C语言与数据结构和算法初阶两个板块都更新完毕,我们继续来学习C++的内容呀。C++是接近底层有比较经典的语言,因此学习起来注定枯燥无味,西游记大家都看过吧~,我希望能带着大家一起跨过九九八十一难,降伏各类难题,学会C++,我会尽我所能,以通俗易懂、幽默风趣的方式带给大家形象生动的知识,也希望大家遇到困难不退缩,遇到难题不放弃,学习师徒四人的精神!!!故此得名【C++游记】
话不多说,让我们一起进入今天的学习吧~~~
1>> 栈的说明——Stack
1.1>> 栈的基本定义
栈是一种线性数据结构,其核心特性遵循 “后进先出(Last-In-First-Out,LIFO)” 原则——即最后加入栈的元素,会最先被取出。
可以类比现实中的“堆叠物品”(如叠盘子、堆书):只能从顶部添加新物品,也只能从顶部拿走物品,无法直接操作中间或底部的物品。
1.2>> 栈的核心术语
术语 | 定义 | 对应操作 |
---|---|---|
栈顶(Top) | 栈中最顶端的元素,是唯一能被直接操作的位置 | 插入、删除操作均在此处进行 |
栈底(Bottom) | 栈中最底端的元素,是最早加入栈的元素 | 只有栈中所有元素被删除后,才能被操作 |
入栈(Push) | 向栈顶添加一个新元素的操作 | 操作后,新元素成为新的栈顶 |
出栈(Pop) | 从栈顶删除并返回该元素的操作 | 操作后,原栈顶的下一个元素成为新栈顶 |
判空(Is Empty) | 判断栈中是否没有任何元素的操作 | 若栈空,出栈操作会触发“栈下溢(Underflow)” |
判满(Is Full) | 判断栈是否达到最大容量的操作(仅固定大小栈需要) | 若栈满,入栈操作会触发“栈上溢(Overflow)” |
查看栈顶(Peek) | 返回栈顶元素但不删除该元素的操作 | 仅读取栈顶值,不改变栈的结构 |
1.3>> 栈的两种常见实现方式
栈的实现依赖于底层存储结构,主要分为“数组实现”和“链表实现”,两者各有优劣:
1.3.1>> 数组实现(顺序栈)
使用数组作为底层存储,通过一个“栈顶指针(top)”标记当前栈顶位置:
- 初始化:创建固定大小的数组,top 初始化为 -1(表示栈空)。
- 入栈(Push):先判断栈是否满,若不满则 top 加 1,将元素存入 top 指向的数组位置。
- 出栈(Pop):先判断栈是否空,若不空则取出 top 指向的元素,top 减 1。
- 优点:元素访问速度快(数组随机访问特性),实现简单。
- 缺点:容量固定,若需动态扩容需重新分配数组空间,可能浪费内存或触发上溢。
1.3.2>> 链表实现(链式栈)
使用链表作为底层存储,通常以“头节点”作为栈顶(无需遍历链表即可操作栈顶):
- 初始化:链表头节点为 null(表示栈空)。
- 入栈(Push):创建新节点,将新节点的 next 指向原头节点,再将头节点更新为新节点(新节点成为栈顶)。
- 出栈(Pop):判断栈是否空,若不空则取出头节点的值,将头节点更新为原头节点的 next(原头节点的下一个节点成为新栈顶)。
- 优点:容量动态变化,无需预先指定大小,不会触发上溢(理论上受内存限制)。
- 缺点:每个元素需额外存储 next 指针,内存开销略大;访问速度不如数组(需通过指针遍历)。
2>> 队列的说明——Queue
2.1>> 队列的基本定义
队列是一种线性数据结构,其核心特性遵循 “先进先出(First-In-First-Out,FIFO)” 原则——即最早加入队列的元素,会最先被取出。
可以类比现实中的“排队”场景(如银行排队、超市结账):新的人从队尾加入,排在最前面的人先得到服务并离开队伍。
2.2>> 队列的核心术语
术语 | 定义 | 对应操作 |
---|---|---|
队头(Front) | 队列中最前端的元素,是即将被取出的元素 | 删除操作在此处进行 |
队尾(Rear) | 队列中最后端的元素,是最后加入队列的元素 | 插入操作在此处进行 |
入队(Enqueue) | 向队尾添加一个新元素的操作 | 操作后,新元素成为新的队尾 |
出队(Dequeue) | 从队头删除并返回该元素的操作 | 操作后,原队头的下一个元素成为新队头 |
判空(Is Empty) | 判断队列中是否没有任何元素的操作 | 若队空,出队操作会触发“队下溢” |
判满(Is Full) | 判断队列是否达到最大容量的操作(仅固定大小队列需要) | 若队满,入队操作会触发“队上溢” |
查看队头(Peek) | 返回队头元素但不删除该元素的操作 | 仅读取队头值,不改变队列的结构 |
2.3>> 队列的常见实现方式
队列的实现主要有以下两种方式:
2.3.1>> 数组实现(顺序队列)
使用数组作为底层存储,通过两个指针(front 和 rear)分别标记队头和队尾:
- 初始化:创建固定大小的数组,front 和 rear 初始化为 -1(表示队空)。
- 入队(Enqueue):先判断队列是否满,若不满则 rear 加 1,将元素存入 rear 指向的数组位置。
- 出队(Dequeue):先判断队列是否空,若不空则取出 front 指向的元素,front 加 1。
- 循环队列优化:为解决普通顺序队列的"假溢出"问题,将数组视为环形,当 rear 到达数组末尾时,若前面有空位则绕回数组开头。
- 优点:实现简单,元素访问速度快。
- 缺点:容量固定,需处理假溢出问题。
2.3.2>> 链表实现(链式队列)
使用链表作为底层存储,通过头指针和尾指针分别指向队头和队尾:
- 初始化:头指针和尾指针均为 null(表示队空)。
- 入队(Enqueue):创建新节点,若队空则头、尾指针均指向新节点,否则将新节点链接到尾节点后,并更新尾指针。
- 出队(Dequeue):判断队列是否空,若不空则取出头节点的值,更新头指针为原头节点的下一个节点,若队空则同时更新尾指针为 null。
- 优点:容量动态变化,无需预先指定大小,不会有假溢出问题。
- 缺点:每个元素需额外存储指针,内存开销略大。
3>> 优先级队列说明——Pirority_queue
3.1>> 优先级队列的基本定义
优先级队列是一种特殊的队列,它不遵循严格的 FIFO 原则,而是每次出队的是优先级最高的元素。
可以类比现实中的“医院急诊”场景:急诊病人(高优先级)会比普通病人(低优先级)先得到治疗,而不管他们到达的顺序。
3.2>> 优先级队列的核心特性
- 优先级:每个元素都有一个关联的优先级(通常是一个数值)。
- 动态排序:元素按优先级排序,优先级最高的元素位于“队头”。
- 灵活出队:出队操作始终移除优先级最高的元素,而非最早加入的元素。
- 优先级规则:可以定义“数值越大优先级越高”或“数值越小优先级越高”,根据具体需求而定。
3.3>> 优先级队列的实现方式
优先级队列的常见实现方式有:
3.3.1>> 数组或链表实现
- 入队操作:直接在数组或链表末尾插入元素,时间复杂度 O(1)。
- 出队操作:遍历整个数组或链表,找到优先级最高的元素并删除,时间复杂度 O(n)。
- 优点:实现简单。
- 缺点:出队操作效率低,适用于数据量小的场景。
3.3.2>> 堆(Heap)实现
堆是实现优先级队列的高效数据结构,通常使用二叉堆:
- 最大堆:父节点的优先级高于子节点,根节点是优先级最高的元素。
- 最小堆:父节点的优先级低于子节点,根节点是优先级最低的元素。
- 入队操作:将元素插入堆的末尾,然后“上浮”调整堆结构,时间复杂度 O(log n)。
- 出队操作:移除根节点(优先级最高),将最后一个元素移到根节点位置,然后“下沉”调整堆结构,时间复杂度 O(log n)。
- 优点:入队和出队操作效率高,是优先级队列的首选实现方式。
- 缺点:实现相对复杂。
4>>三种结构模拟实现
4.1>>stack.h
#pragma once // 防止头文件被多次包含#include<bits/stdc++.h> // 包含标准库头文件
#include<deque> // 包含deque容器的头文件namespace wc { // 自定义命名空间,避免命名冲突// 栈类模板实现,使用容器适配器模式// T: 栈中元素的类型,Container: 底层容器类型,默认使用dequetemplate<class T, class Container = deque<T>>class stack{public:// 压栈操作:向栈顶添加元素void push(const T& x) {_con.push_back(x); // 调用底层容器的尾插接口}// 出栈操作:移除栈顶元素void pop() {_con.pop_back(); // 调用底层容器的尾删接口}// 获取栈中元素个数size_t size()const {return _con.size(); // 调用底层容器的size接口}// 判断栈是否为空bool empty()const {return _con.empty(); // 调用底层容器的empty接口}// 获取栈顶元素(const版本,用于const对象)const T& top()const {return _con.back(); // 调用底层容器的back接口获取最后一个元素}// 获取栈顶元素(非const版本,用于非const对象)T& top() {return _con.back(); // 调用底层容器的back接口获取最后一个元素}private:Container _con; // 底层容器对象,用于存储栈的元素};
}
4.2>>queue.h
#pragma once
#include<bits/stdc++.h> // 包含标准库头文件,提供deque等容器支持namespace wc { // 自定义命名空间,避免与标准库queue冲突// 模板参数:// T:队列中存储的元素类型// Container:底层容器类型,默认使用deque<T>(双端队列,兼顾头删和尾插效率)template<class T, class Container = deque<T>>class queue {public:// 向队列尾部插入元素// 借助底层容器的push_back实现,时间复杂度O(1)void push(const T& x) {_con.push_back(x);}// 移除队列头部的元素// 借助底层容器的pop_front实现,注意:不返回被移除的元素// 选择deque作为默认容器的原因:deque的pop_front效率高于vector(O(1) vs O(n))void pop() {_con.pop_front();}// 返回队列中元素的个数size_t size()const {return _con.size();}// 判断队列是否为空(无元素返回true)bool empty()const {return _con.empty();}// 获取队列头部元素的const引用(只读)const T& front()const {return _con.front();}// 获取队列头部元素的引用(可修改)T& front() {return _con.front();}// 获取队列尾部元素的const引用(只读)const T& back()const{return _con.back();}// 获取队列尾部元素的引用(可修改)T& back() {return _con.back();}private:Container _con; // 底层容器对象,所有操作通过封装该容器实现};
}
4.3>>priority_queue.h
#pragma once
#include<bits/stdc++.h> // 包含标准库头文件,提供容器基础支持// 定义less仿函数:用于比较两个元素,返回x < y的结果
// 作为默认比较方式,用于构建大顶堆(父节点大于子节点)
template<class T>
class less {
public:bool operator()(const T& x, const T& y) {return x < y; // 当x小于y时返回true}
};// 定义Greater仿函数:用于比较两个元素,返回x > y的结果
// 用于构建小顶堆(父节点小于子节点)
template<class T>
class Greater {
public:bool operator()(const T& x, const T& y) {return x > y; // 当x大于y时返回true}
};namespace wc { // 自定义命名空间,避免与标准库冲突// 优先队列模板类// 模板参数:// T:存储的元素类型// Container:底层容器类型,默认使用vector(连续空间适合堆结构)// Compare:比较仿函数,默认使用Less<T>(构建大顶堆)template<class T, class Container = vector<T>, class Compare = less<T>> class priority_queue {public:// 默认构造函数:使用默认成员初始化(_con会调用vector的默认构造)priority_queue() = default;// 迭代器区间构造函数:用[first, last)区间的元素初始化优先队列template<class InputIterator>priority_queue(InputIterator first, InputIterator last):_con(first, last) // 先用区间元素初始化底层容器{// 从最后一个非叶子节点开始,依次向下调整堆// 最后一个非叶子节点索引:(元素个数-1-1)/2for (int i = (_con.size() - 1 - 1) / 2; i >= 0; i--) {adjust_down(i);}}// 向优先队列插入元素void push(const T& x) {_con.push_back(x); // 先将元素插入到底层容器尾部adjust_up(_con.size() - 1); // 从新元素位置向上调整堆结构}// 删除优先队列的顶部元素(优先级最高的元素)void pop() {swap(_con[0], _con[_con.size() - 1]); // 交换堆顶与最后一个元素_con.pop_back(); // 删除最后一个元素(原堆顶)adjust_down(0); // 从堆顶位置向下调整堆结构}// 获取优先队列顶部元素(优先级最高的元素)const T& top() {return _con[0]; // 堆顶元素位于底层容器的第一个位置}// 判断优先队列是否为空bool empty()const {return _con.empty(); // 委托底层容器判断}// 获取优先队列中元素的个数size_t size()const {return _con.size(); // 委托底层容器获取}private:// 向上调整函数:用于插入元素后维持堆结构// 参数child:新插入元素的索引位置void adjust_up(int child) {Compare com; // 创建比较仿函数对象int parent = (child - 1) / 2; // 计算父节点索引// 当子节点索引大于0(未到达根节点)时循环while (child > 0) {// 若父节点不符合比较规则(需要交换)if (com(_con[parent], _con[child])) { // 注意:原代码此处有笔误,应为_con[parent]swap(_con[child], _con[parent]); // 交换父子节点child = parent; // 更新子节点为原父节点parent = (child - 1) / 2; // 重新计算父节点}else {break; // 符合堆结构,停止调整}}}// 向下调整函数:用于删除元素后维持堆结构// 参数parent:需要调整的父节点索引void adjust_down(int parent) {Compare com; // 创建比较仿函数对象int child = parent * 2 + 1; // 计算左子节点索引// 当子节点索引小于容器大小(存在该子节点)时循环while (child < _con.size()) {// 若右子节点存在且右子节点更符合比较规则,则选择右子节点if (child + 1 < _con.size() && com(_con[child], _con[child + 1])) {child++; // 切换到右子节点}// 若父节点不符合比较规则(需要交换)if (com(_con[parent], _con[child])) { // 注意:原代码此处有笔误,应为_con[parent]swap(_con[child], _con[parent]); // 交换父子节点parent = child; // 更新父节点为原子节点child = parent * 2 + 1; // 重新计算左子节点}else {break; // 符合堆结构,停止调整}}}private:Container _con; // 底层容器:用于存储元素,堆结构基于此容器实现};
}
5>>结语
今日C++到这里就结束啦,如果觉得文章还不错的话,可以三连支持一下。感兴趣的宝子们欢迎持续订阅小枫,小枫在这里谢谢宝子们啦~小枫の主页还有更多生动有趣的文章,欢迎宝子们去点评鸭~C++的学习很陡,时而巨难时而巨简单,希望宝子们和小枫一起坚持下去~你们的三连就是小枫的动力,感谢支持~