当前位置:首页 » 《随便一记》 » 正文

【C++】——Stack 与 Queue:抽象数据类型的魅力展现

25 人参与  2024年10月29日 14:40  分类 : 《随便一记》  评论

点击全文阅读


在 C C C 语言中已经对栈和队列有了一定的了解,同时 C C C ++中我们对 STL 模板中的 string list vector 等容器进行了详细的探讨,从而获得了对这些容器操作的清晰理解。基于这些知识,现在转向学习 C C C ++ 里的 stack (栈) 和 queue (队列)就显得相对简单了,值得注意的是 STL 里的 stackqueue 通过适配的方法实现,更是一种封装的体现。

1、容器适配器

1.1什么是适配器

适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结),该种模式是将一个类的接口转换成客户希望的另外一个接口
在这里插入图片描述

1.2 STL标准库中 stackqueue的底层结构

虽然 stackqueue 中也可以存放元素,但在 STL 中并没有将其划分在容器的行列,而是将其称为容器适配器,这是因为 stackqueue 只是对其他容器的接口进行了包装STLstackqueue 默认使用 deque ,比如:
在这里插入图片描述

1.3 deque的简单介绍

deque (双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为 O O O ( 1 1 1 ),与 vector 比较,头插效率高,不需要搬移元素;与 list 比较,空间利用率比较高。
在这里插入图片描述
deque 并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际 deque 类似于一个
动态的二维数组,其底层结构如下图所示:
在这里插入图片描述
那么 deque 是如何借助其迭代器维护其假想连续的结构呢

双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其“整体连续”以及随机访问的假象,落在了 deque 的迭代器的 operator ++和 operator--重载运算符上。
** deque 应具备的结构: **

● 能够指出分段连续空间(缓冲区)的位置
● 能判断自己是否已经处于其所在缓冲区的边缘,如果前进或后退时就必须跳跃至下一个或上一个缓冲区。
为了能够正确跳跃, deque 必须要有掌控中心( map )。
在这里插入图片描述

迭代器 start 内的 cur 指针指向缓冲区的第一个元素,迭代器 finish 内的 cur 指向缓冲区的最后元素(的下一个位置)。注意:最后一个缓冲区尚有备用空间,稍后如果有新元素要插入到尾端,可直接拿此备用空间来使用。
在这里插入图片描述

template <class T, class Ref, class Ptr, size_t BufSiz>struct _daque_iterator {//... //保持与容器的联结T* cur;//此迭代器指向缓冲区中的现行T* first;//此迭代器指向缓冲区的头T* last;//此迭代器指向缓冲区的尾(含备用空间)map_pointer node;//指向控制中心(指针数组)};

deque 的底层框架
deque 除了维护一个指向 map 的指针外,也维护 startfinish 两个迭代器,分别指向第一缓冲区的第一个元素和最后缓冲区的最后一个元素(的下一个位置)。此外,它当然也必须记住目前 map 的大小,因为,一旦 map 所提供的节点不足,就必须重新配置更大的一块 map

  iterator start;  iterator finish;  map_pointer map;  size_type map_size;

deque 的缺陷
vector 比较, deque 的优势是:头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不需要搬移大量的元素,因此其效率是比 vector 高的。与 list 比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段。但是, deque 有一个致命缺陷:不适合遍历,因为在遍历时, deque 的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑 vectorlistdeque 的应用并不多,而目前能看到的一个应用就是, STL 用其作为 stackqueue 的底层数据结构。

为什么选择 deque 作为 stackqueue 的底层默认容器?

stack 是一种后进先出的特殊线性数据结构,因此只要具有 push_back ()pop_back
() 操作的线性结构,都可以作为 stack 的底层容器,比如 vectorlist 都可以; queue
是先进先出的特殊线性数据结构,只要具有 push_backpop_front 操作的线性结构,都可以作为 queue
的底层容器,比如 list 。但是 STL 中对 stackqueue 默认选择 deque
作为其底层容器。主要是因为:

** stackqueue 不需要遍历 (因此 stackqueue没有迭代器),只需要在固定的一端或者两端进行操作。 在 stack 中元素增长时, dequevector的效率高(扩容时不需要搬移大量数据);
queue 中的元素增长时, deque 不仅效率高,而且内存使用率高。 结合了 deque 的优点,而完美的避开了其缺陷。

2 、stackqueue 的介绍使用

2.1stackqueue 介绍

stack介绍
stack 是一种容器适配器,专门用在具有**LIFO(后进先出)**操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。类似与向箱子里放入取出物品,只能一端进行操作
在这里插入图片描述

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

stack 的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类,这些容器类应该支持以下
操作:

empty :判空操作
back :获取尾部元素操作
push_back :尾部插入元素操作
pop_back:尾部删除元素操作

标准容器 vectordequelist 均符合这些需求,默认情况下,如果没有为 stack 指定特定的底层容器,默认情况下使用 deque
在这里插入图片描述

queue介绍

queue 是一种容器适配器,专门用于在**FIFO(先进先出)**中操作,其中从容器一端插入元素,另一端
提取元素。类似于排队打饭,只能从尾端进入,从头离开。
在这里插入图片描述

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

底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。该底层容器应至少支持以下操
作:

empty :检测队列是否为空
size :返回队列中有效元素的个数
front :返回队头元素的引用
back:返回队尾元素的引用
push_back :在队列尾部入队列
pop_front :在队列头部出队列

标准容器类 dequelist 满足了这些要求。默认情况下,如果没有为 queue 实例化指定容器类,则使用标准容器 deque
在这里插入图片描述

2.2stackqueue 模拟

Stack 模拟实现

#define _CRT_SECURE_NO_WARNINGS#pragma once#include<list>#include<vector>#include<iostream>#include<deque>namespace xc{// container 适配转换 stacktemplate<class T,class Container=deque<T>>class stack{public:void push(const T&x){_con.push_back(x);}void pop(){_con.pop_back();}const T& top()const{return _con.back();}size_t size()const{return _con.size();}bool empty()const{return _con.empty();}private:Container _con;};}

Queue 模拟实现

#define _CRT_SECURE_NO_WARNINGS#pragma once#include<list>#include<vector>#include<iostream>#include<deque>namespace xc{// container 适配转换 queuetemplate<class T, class Container = deque<T>>class queue{public:void push(const T& x){_con.push_back(x);}void pop(){_con.pop_front();}const T& front()const{return _con.front();}const T& back()const{return _con.back();}size_t size()const{return _con.size();}bool empty()const{return _con.empty();}private:Container _con;};}

2.3stackqueue 使用

接下来我们在解题中体会 stackqueue 的使用方法

有效的括号
在这里插入图片描述
思路分析:

遍历字符串 s s s 左括号入栈,右括号与出栈的左括号匹配,如果不匹配直接 false

Tip :仅有右括号或者左括号,数量不匹配问题也应该注意结合判空返回真假
在这里插入图片描述

class Solution {public:    bool isValid(string s) {        stack<char>st;        for(int i=0;i<s.size();i++)        {            //左括号入栈            if(s[i]=='('||s[i]=='{'||s[i]=='[')            {                st.push(s[i]);            }            else            {                if(st.empty())//栈里没有左括号 直接到右括号 匹配失败                return 0;                char tmp=st.top();                st.pop();//取出右括号 进行匹配                if(tmp=='('&&s[i]!=')'                ||tmp=='['&&s[i]!=']'                ||tmp=='{'&&s[i]!='}')                return 0;            }        }        if(!st.empty())//遍历完字符串 如果栈里还有左括号 也是属于匹配错误        return 0;        return 1;    }};

队列实现栈
在这里插入图片描述
思路分析:

因为队列是先进先出,而需要实现的栈是后进先出的。所以我们需要借助另外一个队列导数据才能实现后进先出的栈
在这里插入图片描述
如何出栈得到 4 4 4 呢?将非空队列 q q q 1 1 1 前 size - 1 1 1 个数据挪到空队列 q q q 2 2 2 ,再 pop q q q 1 1 1 最后一个元素即栈顶元素
在这里插入图片描述
在这里插入图片描述
即非空队列入数据

class MyStack {public:    queue<int>q1;    queue<int>q2;    MyStack() {            }        void push(int x) { //非空队列入数据        if(!q1.empty())        {            q1.push(x);        }        else         q2.push(x);    }        int pop() {  //先判断哪个队列是空的然后while循环导size-1数据,最后pop        if(!q1.empty()) //假设法判断出空队列 q1为非空 交换q2非空        {            swap(q1,q2);        }        while(q2.size()>1) //将前面数据导入空队列 此时 q1 空队列        {            q1.push(q2.front());            q2.pop();        }        int ret=q2.front();        q2.pop();        return ret;;    }        int top() {        if(!q1.empty())        return q1.back();        else return q2.back();    }        bool empty() {       return q1.empty()&&q2.empty();    }};

还有一种思路 只需要一个队列实现栈:简单来说就是把队列当成一个环用,每次都把除了队列末端的元素都出队然后依次加到原本末端元素的后端,这样原本最后的元素就被推到了队列最前端,实现了 Last In First Out

class MyStack {public:    queue<int>q;    MyStack() {            }        void push(int x) {         int n=q.size();        q.push(x);               while(n>=1)        {            q.push(q.front());            q.pop();            n--;        }    }        int pop() {        int ret=q.front();        q.pop();        return ret;    }        int top() {        return q.front();    }        bool empty() {        return q.empty();    }};

栈实现队列
在这里插入图片描述
思路分析:
在这里插入图片描述
出数据应该是 1 1 1 2 2 2 3 3 3 4 4 4 ,发现只要将数据导入另一个栈中 pop 顺序刚好是 1 1 1 2 2 2 3 3 3 4 4 4
在这里插入图片描述
这样我们就可以利用一个 push 栈 和一个 pop 栈实现队列的先进先出,pop 为空时就需要导入数据

class MyQueue {public:    stack<int> pushst;    stack<int> popst;    MyQueue() {            }        void push(int x) {        pushst.push(x);    }       int pop() {        int ret =peek();        popst.pop();        return ret;    }        int peek() {        if(popst.empty()) //判断出的栈是否为空        {            while(!pushst.empty())//导数据            {                popst.push(pushst.top());                pushst.pop();            }        }        return popst.top();    }        bool empty() {        return popst.empty()&&pushst.empty();    }};

二叉树的层序遍历
在这里插入图片描述
思路一:
因为题目要求返回一个二维vector 所以我们需要记录节点层数
在这里插入图片描述

class Solution {public:    vector<vector<int>> levelOrder(TreeNode* root) {        vector<vector<int>> ret;        if (root == nullptr)            return ret; // 空树 返回空数组        queue<TreeNode*> nodeQueue; // 存储节点的队列        queue<int> levelQueue;      // 存储节点层数的队列        nodeQueue.push(root);        levelQueue.push(0);        while (!nodeQueue.empty()) {            auto node = nodeQueue.front();            nodeQueue.pop();            auto level = levelQueue.front();            levelQueue.pop();            // 如果 ret 的大小还没有达到当前层数,需要扩展 ret            if (ret.size() == level) {                ret.push_back(vector<int>());            }            ret[level].push_back(node->val); // 将当前节点的值加入对应层            // 把左右子节点加入队列并记录层数            if (node->left) {                nodeQueue.push(node->left);                levelQueue.push(level + 1);            }            if (node->right) {                nodeQueue.push(node->right);                levelQueue.push(level + 1);            }        }        return ret;    }    };

思路二:
在这里插入图片描述

class Solution {public:    vector<vector<int>> levelOrder(TreeNode* root) {        vector<vector<int>>ret;        if(root==nullptr)        return ret;        queue<TreeNode*>q;        q.push(root);        while(!q.empty())        {            int CurrentLevelSize=q.size();                    ret.push_back(vector<int>()); // 添加一个新的向量           while(CurrentLevelSize>0) { //依次将当前层数据插入vector里                auto node = q.front();                q.pop();                ret[ret.size()-1].push_back(node->val); // 将当前节点的值加入当前层                if (node->left) q.push(node->left);                if (node->right) q.push(node->right);                CurrentLevelSize--; //更新值 否则循环错误            }                    }        return ret;    }};

设计循环队列
在这里插入图片描述
思路分析:

题意有限的资源重复利用保证先进先出,也就是要求 headtail 在队列里回绕重复使用队列的空间

在这里插入图片描述
上述方法可以解决掉判空和判满的冲突问题
在这里插入图片描述
在循环队列中,当队列为空,可知 head = tail ;而当所有队列空间全占满时,也有 tail = head 。为了区别这两种情况,假设队列使用的数组有 capacity 个存储空间,则此时规定循环队列最多只能有 capacity − 1 1 1 个队列元素,当循环队列中只剩下一个空存储单元时,则表示队列已满。根据以上可知,队列判空的条件是 head = tail ,而队列判满的条件是 head =( tail + 1 1 1 ) mod capacity
对于一个固定大小的数组,只要知道队尾 tail 与队首 head ,即可计算出队列当前的长度:

( tailhead + capacity ) mod capacity

class MyCircularQueue {  private:   int head;   int tail;   vector<int>con;   public:    MyCircularQueue(int k) {        con.reserve(k+1);        head=tail=0;    }        bool enQueue(int value) {        if(isFull())        return 0;        con[tail]=value;        tail=(tail+1)%con.capacity(); //解决回绕问题 假设 k为4 tail==4时候 再插入 tail应为0        return 1;    }        bool deQueue() {         if(isEmpty())        return 0;;        head=(head+1)%con.capacity();        return 1;    }        int Front() {        if(isEmpty())        return -1;        return con[head];    }        int Rear() {  //tail-1可能为负值 可以 tail-1+capacity %capacity        if(isEmpty())        return -1;        return con[(tail-1+con.capacity())%con.capacity()];    }        bool isEmpty() {        return head==tail;    }        bool isFull() {  //解决tail+1溢出问题 现需要循环回绕        return (tail+1)%con.capacity()==head;    }};

总结:循环队列的实现
数组 + size 变量:代码清晰、操作简便,但数组大小固定。
仅使用 headtail 的数组:保留一个空位,减少变量开销,适合容量固定的情况。
链表实现:动态大小,适用于需要弹性容量的场景,但性能相对较低。
双端队列实现:可以直接使用 STL deque 实现循环队列,方便但缺乏灵活性。

最小栈
在这里插入图片描述
思路
要做出这道题目,首先要理解栈结构先进后出的性质。
对于栈来说,如果一个元素 a a a 在入栈时,栈里有其它的元素 b b b , c c c , d d d ,那么无论这个栈在之后经历了什么操作,只要 a a a 在栈中, b b b , c c c , d d d 就一定在栈中,因为在 a a a 被弹出之前, b b b , c c c , d d d 不会被弹出。
因此,在操作过程中的任意一个时刻,只要栈顶的元素是 a a a ,那么我们就可以确定栈里面现在的元素一定是 a a a , b b b , c c c , d d d 。
那么,我们可以在每个元素 a a a 入栈时把当前栈的最小值 m m m 存储起来。在这之后无论何时,如果栈顶元素是 a a a ,我们就可以直接返回存储的最小值 m m m 。
在这里插入图片描述

class MinStack {public:    stack<int>st;    stack<int>minst;    MinStack() {       //minst.push(INT_MAX);    }        void push(int val) {        st.push(val);        if(minst.empty()|| val<=minst.top()) //第一次插入 minst可能空栈 无法调用 top        minst.push(val);    }        void pop() {        if(st.top()==minst.top())        minst.pop();        st.pop();    }        int top() {        return st.top();    }        int getMin() {        return minst.top();    }};

栈的压入、弹出序列
在这里插入图片描述
在这里插入图片描述
可以用来判断出栈序列的正确性

bool IsPopOrder(vector<int>& pushV, vector<int>& popV) {        stack<int>st;        int n=pushV.size();        int j=0;        //并列遍历 而不是嵌套        for(int i=0;i<n;i++)//依次遍历 push和pop数组 这里 i 控制的是 popV        {            while(j<n&&(st.empty()||st.top()!=popV[i])) //j 控制的是 push V            {                st.push(pushV[j]);                j++;            }            if(st.top()==popV[i]) //遇到了popV数组元素就需要出栈            st.pop();            else return 0;        }        return 1;    }

逆波兰表达式求值
在这里插入图片描述
在这里插入图片描述

class Solution {public:    int evalRPN(vector<string>& tokens) {        stack<int>st;        int n=tokens.size();        for(int i=0;i<n;i++)        {            string&str=tokens[i];            if(!(str=="+"||str=="-"||str=="*"||str=="/")) //数字入栈            st.push(stoi(str)); // stoi 将 string转为int            else  //操作符            {                int left=st.top(); st.pop(); //后入的元素为左操作数                int right=st.top(); st.pop();                if(str=="+")                st.push(right+left);                if(str=="-")                st.push(right-left);                if(str=="*")                st.push(right*left);                if(str=="/")                st.push(right/left);            }        }        return st.top();    }};

总结:
多是观察栈的特性 后进先出,借用辅助栈来解决一些问题
队列涉及到
BFS 广度优先
——层序遍历,还有注意的就是循环队列的一些特性方法


点击全文阅读


本文链接:http://zhangshiyu.com/post/179534.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1