【C++】栈和队列的模拟实现 & 经典题目解析

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 【C++】栈和队列的模拟实现 & 经典题目解析

一. Stack & Queue


0a2653c851af460fa595bd959398a8f1.png


常用成员函数:

2d65d23f6d4748949b924e4057485923.png

简单使用一下栈,先进先出


#include<iostream>
#include<stack>
using namespace std;
int main()
{
  stack<int> st;
  st.push(1);
  st.push(2);
  st.push(3);
  st.push(4);
  while (!st.empty())
  {
  cout << st.top() << endl;
  st.pop();
  }
}


💥经典题目

🥑最小栈

题目链接:155. 最小栈


0a2653c851af460fa595bd959398a8f1.png


🌍思路:


辅助栈,以空间换时间,st.push()时同时比较,把较小值也压入 minst中,pop时 st和 minST同时弹栈,这样取 minST栈顶元素即最小值 ——

st尽管可能存储了很多相同的数据,但对于minst,我们只在< = 时入栈,st和 minST相同再一起出栈


class MinStack {
public:
    MinStack() {
        //默认的即可:自定义类型自动调用它的构造函数
    }
    void push(int val) {
        _st.push(val);
        if(_minst.empty() || val <= _minst.top())//顺序不能互换
            // 出现<=前面的值,则插入新的最小数据
            _minst.push(val);
    }
    void pop() {
        if(_minst.top() == _st.top())
            _minst.pop();//minst和st删除顺序不能换,先st删了,top数据就出错了
        _st.pop();
    }
    int top() {
        return _st.top();
    }
    int getMin() {
        return _minst.top();
    }
private:
    stack<int> _st;
    stack<int> _minst;
};


🔥题目升级:如果插入大量重复数据(插入10w个1),怎么办?


minst里面就存一个结构体,遇到相等就计数++


🥑栈的压入、弹出序列

题目链接:JZ31 栈的压入、弹出序列


🌍入栈顺序来模拟出栈顺序


首先对入栈序列进行遍历,对栈进行操作


入栈数据与出栈数据不相同 —>入栈

入栈数据与出栈数据相同 ——> 出栈,也就是刚进栈就出去了

不断重复,直到栈为空/栈顶元素和出栈序列的值不匹配就结束了

画图分析:

0a2653c851af460fa595bd959398a8f1.png


class Solution {
public:
    bool IsPopOrder(vector<int> pushV,vector<int> popV) {
        stack<int> st;
        int popi = 0;
        for(auto e:pushV)
        {
            st.push(e);
            //匹配后要持续比较,可能有多个匹配
            while(!st.empty() && st.top() == popV[popi])
            {
                st.pop();
                popi++;
            }
        }
        return st.empty();
    }
};


🥑逆波兰表达式求值

题目链接:150. 逆波兰表达式求值


后缀表达式转成中缀表达式


🌍思路:


操作数入栈

操作符,连续取两个栈顶元素进行计算(先出的是右操作数,后出的的是左操作数),运算结果继续入栈

0a2653c851af460fa595bd959398a8f1.png


class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<int> st;
        for(auto& str : tokens)
        {
            if(str== "+" || str== "-" || str== "*" ||str== "/")
            {
                int right = st.top();
                st.pop();
                int left = st.top();
                st.pop();
                switch(str[0])//取str[0]就是char
                {
                    case '+':
                        st.push(left+right);
                        break;
                    case '-':
                        st.push(left-right);
                        break;
                    case '*':
                        st.push((long long)left*right);
                        break;
                    case '/':
                        st.push(left/right);
                        break;
                }
            }
            //操作数
            else
            {
                st.push(stoll(str));
            }
        }
        return st.top();
    }
};


吐槽:有一个奇葩的测试用例是 long long,最后还要转成int(无语)


🔥升级:对于计算机而言,中缀表达式有优先级问题,而后缀表达式操作符都按优先级排列好了,如何将中缀表达式转化为后缀表达式呢?(和上面恰恰相反)


遇到操作数,直接输出

操作符

若栈为空,直接入栈

栈不为空,跟栈顶的操作符比较(每次和前一个操作符比较)

若优先级更高,不能运算,因为可能有更高的 → 入栈

相反,优先级低/相等 → 此时栈顶的操作符出栈,表示此时可以进行运算

0a2653c851af460fa595bd959398a8f1.png


再升级:带()的优先级:1 +( 2 + 3 )* 4 - 5


用flag代表括号,临时升高优先级

🥑两个栈实现队列

题目链接:232. 用栈实现队列


思路可参考:此文章——>【数据结构】栈实现队列 & 队列实现栈🥑


class MyQueue {
public:
    MyQueue() {
        //不用写,用系统自动生成的即可
    }
    void push(int x) {
        pushst.push(x);
    }
    int pop() {
        if(popst.empty())
        {
            while(!pushst.empty())
            {
                popst.push(pushst.top());
                pushst.pop();
            }
        }
        int top = popst.top();
        popst.pop();
        return top;
    }
    int peek() {
        if(popst.empty())
        {
            while(!pushst.empty())
            {
                popst.push(pushst.top());
                pushst.pop();
            }
        }
        return popst.top();
    }
    bool empty() {
        return pushst.empty() && popst.empty();
    }
private:
    stack<int> pushst;
    stack<int> popst;
};


🥑两个队列实现栈

题目链接:225. 用队列实现栈


详细思路:详细看上面的链接,就是_q2完全用作辅助队列,出入数据都在 _q1


class MyStack {
public:
    MyStack() {
        //用系统自动生成的
    }
    void push(int x) {
        _q1.push(x);
    }
    int pop() {
        while(_q1.size()>1)
        {
            _q2.push(_q1.front());
            _q1.pop();
        }
        int top = _q1.front();
        _q1.pop();
        //数据倒回给_q1
        while(_q2.size())
        {
            _q1.push(_q2.front());
            _q2.pop();
        }
        return top;
    } 
    int top() {
        return _q1.back();
    }
    bool empty() {
        return _q1.empty();
    }
private:
    queue<int> _q1;
    queue<int> _q2;
};


二. 容器适配器


🌈什么是适配器


适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结),该种模式是将一个类的接口转换成客户希望的另外一个接口,说白了就是:不是写死的,谁只要合适就可以来用


0a2653c851af460fa595bd959398a8f1.png


🌈STL标准库中stack和queue的底层结构


虽然stack和queue中也可以存放元素,但在STL中并没有将其划分在容器的行列,而是将其称为容器适配器,这是因为stack和队列只是对其他容器的接口进行了包装,STL中stack和queue默认使用deque,比如:

2d65d23f6d4748949b924e4057485923.png


三. 栈和队列的模拟实现


💥stack的模拟实现


#pragma once
#include<deque>
namespace ljj
{ 
  //适配器
  template<class T, class Container = deque<T>>//deque后面细说
  class Stack
  {
  public:
  void push(const T& x)
  {
    _con.push_back(x);
  }
  void pop()
  {
    _con.pop_back();
  }
     T& top()//使用通用的接口,不要用[]
  {
    return _con.back();
  }
  const T& top() const
  {
    return _con.back();
  }
  bool empty() const
  {
    return _con.empty();
  }
  size_t size() const
  {
    return _con.size();
  }


private:


//封装一个容器vector
  //vector<T> _con; 不够灵活
  //只要Container满足上面的接口都可以
  Container _con;
  };
}
void test_stack()
{
  //底层大有不同,但是表面我们看不出来
  //ljj::Stack<int , vector<int>> st;
  //ljj::Stack<int , list<int>> st;
  ljj::Stack<int> st;//deque适配出来的
  st.push(1);
  st.push(2);
  st.push(3);
  st.push(4);
  while (!st.empty())
  {
  cout << st.top() << endl;
  st.pop();
  }
}


💥Queue的模拟实现


#pragma once
#include<deque>
namespace ljj
{
  //适配器
  template<class T, class Container = deque<T>>
  class queue
  {
  public:
  void push(const T& x)
  {
    _con.push_back(x);
  }
  void pop()
  {
    _con.pop_front();
  }
  T& top()
  {
    return _con.back();
  }
  T& front()
  {
    return _con.front();
  }
  T& top() const
  {
    return _con.back();
  }
  T& front() const
  {
    return _con.front();
  }
  bool empty() const
  {
    return _con.empty();
  }
  size_t size() const
  {
    return _con.size();
  }
  private:
  Container _con;
  };
}
void test_queue()
{
  //ljj:queue<int> q;
  //不想用默认的deque可以用其他
  ljj:queue<int, list<int>> q;
  q.push(1);
  q.push(2);
  q.push(3);
  q.push(4);
  while (!q.empty())
  {
  cout << q.front() << endl;
  q.pop();
  }
}


四. deque: stack和queue的底层结构(了解)


deque强烈的勾起了我们的好奇心。

众所周知,vector连续的物理空间带来了优点,它支持随机访问,CPU高速缓存命中率高,另外尾插尾删效率高;但也同时带来了缺点,不适合头插头删,另外扩容效率较低,也会造成一定的空间浪费。list按需申请释放空间,支持任意位置的 O(1) 插入删除;但不支持下标随机访问。


而双端队列deque就是为了融合二者优点而产生的 ~ 比如詹姆斯(进攻防守都厉害)(有伏笔)


它支持随机迭代器、下标访问、任意位置的插入和删除 ——


0a2653c851af460fa595bd959398a8f1.png


⚡底层原理


deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高


2d65d23f6d4748949b924e4057485923.png


🎨deque并不是真正连续的空间,而是由一段段连续的小空间buffer拼接而成的,其底层结构如下 ——


4cebaac233b3433da32a72337a77fc60.png


这样,与vector相比,deque头插头删不需要挪动数据,且扩容时的代价也大大降低;与list相比,deque高速缓存命中率高,且不需要频繁的申请释放空间,且可以“随机访问”


双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其“整体连续”以及随机访问的假象,落在了deque的迭代器身上,因此deque的迭代器设计就比较复杂,如下图所示


0a2653c851af460fa595bd959398a8f1.png


那deque是如何借助其迭代器维护其假想连续的结构呢?


2d65d23f6d4748949b924e4057485923.png


insert和erase也比较复杂。所以要高频随机访问还得是vector,要任意位置插入删除还得是list。它没能取代list和vector的宝座,但它很适合头尾的插入删除(双端)


⚡缺点


operator[]计算稍显复杂,大量使用,性能下降(不如vector的[])

中间插入删除效率不高(要么挪动数据,要么改buffer的空间,都不好)

底层角度迭代器很复杂

所以要高频随机访问还得是vector,要任意位置插入删除还得是list。它没能取代list和vector的宝座,但它很适合头尾的插入删除(双端)


⚡为什么


为什么选deque来作为stack和list的底层默认容器?


stack和queue不需要遍历(因此stack和queue没有迭代器),只需要在固定的一端或者两端进行操作。

在stack中元素增长时,deque比vector的效率高;queue中的元素增长时,deque不仅效率高,而且内存使用率高。vector当容量不够时,会开更大的空间,拷数据然后释放原来的空间,操作复杂。而deque会找另一块内存继续存放数据,效率高。

相比较list,deque的插入操作会更加的高效,而且内存的使用率高,list会导致更多的内存碎片。

刚好都避开了deque的缺点

所以,deque作为stack和queue的默认容器是完胜的,不能独挡一面,但是是一个好用的二把手(暗指谁哈哈哈)


目录
打赏
0
0
0
0
5
分享
相关文章
【c++丨STL】priority_queue(优先级队列)的使用与模拟实现
本文介绍了STL中的容器适配器`priority_queue`(优先级队列)。`priority_queue`根据严格的弱排序标准设计,确保其第一个元素始终是最大元素。它底层使用堆结构实现,支持大堆和小堆,默认为大堆。常用操作包括构造函数、`empty`、`size`、`top`、`push`、`pop`和`swap`等。我们还模拟实现了`priority_queue`,通过仿函数控制堆的类型,并调用封装容器的接口实现功能。最后,感谢大家的支持与关注。
43 1
自己动手编写tcp/ip协议栈1:tcp包解析
学习tuntap中的tun的使用方法,并使用tun接口解析了ip包和tcp包,这是实现tcp/ip协议栈的第一步。
46 15
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
【C++篇】深度解析类与对象(中)
在上一篇博客中,我们学习了C++类与对象的基础内容。这一次,我们将深入探讨C++类的关键特性,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载、以及取地址运算符的重载。这些内容是理解面向对象编程的关键,也帮助我们更好地掌握C++内存管理的细节和编码的高级技巧。
【C++篇】深度解析类与对象(上)
在C++中,类和对象是面向对象编程的基础组成部分。通过类,程序员可以对现实世界的实体进行模拟和抽象。类的基本概念包括成员变量、成员函数、访问控制等。本篇博客将介绍C++类与对象的基础知识,为后续学习打下良好的基础。
|
2月前
|
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
158 77
|
2月前
|
C++
【C++数据结构——栈和队列】括号配对(头歌实践教学平台习题)【合集】
【数据结构——栈和队列】括号配对(头歌实践教学平台习题)【合集】(1)遇到左括号:进栈Push()(2)遇到右括号:若栈顶元素为左括号,则出栈Pop();否则返回false。(3)当遍历表达式结束,且栈为空时,则返回true,否则返回false。本关任务:编写一个程序利用栈判断左、右圆括号是否配对。为了完成本关任务,你需要掌握:栈对括号的处理。(1)遇到左括号:进栈Push()开始你的任务吧,祝你成功!测试输入:(()))
43 7
|
2月前
|
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
【数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】初始化队列、销毁队列、判断队列是否为空、进队列、出队列等。本关任务:编写一个程序实现环形队列的基本运算。(6)出队列序列:yzopq2*(5)依次进队列元素:opq2*(6)出队列序列:bcdef。(2)依次进队列元素:abc。(5)依次进队列元素:def。(2)依次进队列元素:xyz。开始你的任务吧,祝你成功!(4)出队一个元素a。(4)出队一个元素x。
52 13
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
|
2月前
|
【C++数据结构——栈与队列】链栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现链栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储整数,最大
51 9
从入门到精通:H5游戏源码开发技术全解析与未来趋势洞察
H5游戏凭借其跨平台、易传播和开发成本低的优势,近年来发展迅猛。接下来,让我们深入了解 H5 游戏源码开发的技术教程以及未来的发展趋势。

推荐镜像

更多