【C++】stack、queue和deque(上)

简介: 【C++】stack、queue和deque(上)

👉stack 的介绍和使用👈


stack 的介绍


stack 是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。

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

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

empty:判空操作

back:获取尾部元素操作

push_back:尾部插入元素操作

pop_back:尾部删除元素操作

标准容器 vector、deque、list 均符合这些需求,默认情况下,如果没有为 stack 指定特定的底层容器,默认情况下使用 deque。

4cfa483238394489924b0cf11849ca8a.pngc62ea78e5b9b4d339e41b122710ed342.png



stack 的使用


函数说明 接口说明
stack() 构造空的栈
empty() 检测stack是否为空
size() 返回stack中元素的个数
top() 返回栈顶元素的引用
push() 将元素val压入stack中
pop() 将stack中尾部的元素弹出



1. 最小栈


设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。

实现 MinStack 类:

  • MinStack() 初始化堆栈对象。
  • void push(int val) 将元素val推入堆栈。
  • void pop() 删除堆栈顶部的元素。
  • int top() 获取堆栈顶部的元素。
  • int getMin() 获取堆栈中的最小元素。

a8598f14e54c4b7f9c8396bfb9b2035d.png


思路:最小栈MinStack包含两个栈:一个是存储插入元素的栈_st,另一个是存储最小值的栈_minst。插入元素时,必须向栈_st中插入新元素。对于_minst来说,如果栈_minst为空,直接向_minst插入新元素。如果栈_minst不为空,当新插入的元素小于或等于_minst的栈顶元素,那么向_minst中插入新元素;而新插入的元素大于_minst的栈顶元素,则向_minst中插入栈顶元素。那么,此时的_minst中的栈顶元素就是栈_st对应位置的最小值。出栈时,让栈_st和_minst同时出栈即可。


class MinStack 
{
public:
    MinStack() {}
    void push(int val) 
    {
        _st.push(val);
        if(_minst.empty() || val <= _minst.top())
        {
            _minst.push(val);
        }
        else
        {
            _minst.push(_minst.top());
        }
    }
    void pop() 
    {
        _st.pop();
        _minst.pop();
    }
    int top() 
    {
        return _st.top();
    }
    int getMin() 
    {
        return _minst.top();
    }
    stack<int> _st;
    stack<int> _minst;
};

7c088e6680614569afec3f94e6ba0f7e.png


其实按照上面的思路来解题,栈_minst中可能会存储着大量的重复的最小值。那么我们可以做一些小小的优化:当新插入元素大于_minst栈顶元素时,不再向_minst插入栈顶元素了。按照这种思路的话,出栈也要做相应的改变:当_st栈顶元素等于_minst栈顶元素时,_minst才会出栈。


class MinStack 
{
public:
    MinStack() {}
    void push(int val) 
    {
        _st.push(val);
        if(_minst.empty() || val <= _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();
    }
    stack<int> _st;
    stack<int> _minst;
};

7f76f9a1fe0245ca9c1c7b64ceeb768d.png

但是第二种思路面对着大量重复的最小值的场景时,也会存在空间浪费的情况。那么我们还可以进一步的优化,增加多一个计算器并将其封装成一个类struct valCont,该类的成员变量_val和_count。此时,栈_minst的元素是struct valCount。当新插入元素等于_minst.top()._val时,++_minst.top()._count。当_minst.top()._count == 0时,则_minst栈顶元素出栈。


struct valCount
{
    int _val;
    int _count;
    valCount(int val = 0, int count = 0)
    {
        _val = val;
        _count = count;
    }
};
class MinStack 
{
public:
    MinStack() {}
    void push(int val) 
    {
        _st.push(val);
        if(_minst.empty() || val < _minst.top()._val)
        {
            _minst.push(valCount(val, 1));
        }
        else if(val == _minst.top()._val)
        {
            ++_minst.top()._count;
        }
    }
    void pop() 
    {
        if(_st.top() == _minst.top()._val)
        {
            --_minst.top()._count;
            if(_minst.top()._count == 0)
                _minst.pop();
        }        
        _st.pop();
    }
    int top() 
    {
        return _st.top();
    }
    int getMin() 
    {
        return _minst.top()._val;
    }
    stack<int> _st;
    stack<valCount> _minst;
};


注:struct valCount 最好自己提供默认构造函数,这样就可以支持匿名对象的用法了。以上思路实现的 MinStack 都没有提供构造函数,为什么也有过呢?因为对于自定义类型,编译器会调用自定义类型的默认构造函数。

b14a7b0441334e2ebf0f3ffc78612bea.png


2. 验证栈序列


给定 pushed 和 popped 两个序列,每个序列中的值都不重复,只有当它们可能是在最初空栈上进行的推入 push 和弹出 pop 操作序列的结果时,返回 true;否则,返回 false 。

663cae9d53f44fd39c44132b7830c095.png


思路:给定一个入栈序列,可能会有多个出栈序列。因为会存在边入边出、入几个再出等等情况。本道题是让我们验证出栈序列popped是不是入栈序列pushed的出栈序列。那么如何解决这道题呢?这道题可以这样来解决:1. 入栈序列的元素先入栈 2. 栈顶元素和出栈序列的元素比较,相等则栈顶元素出栈并继续比较 3. 当入栈序列的元素已全部入栈,循环结束。如果栈为空或者出栈序列也全部遍历了一次,那么popped就是pushed的出栈序列了,否则不是。

7a35afef1e38424c9939193fe198a84a.png

class Solution 
{
public:
    bool validateStackSequences(vector<int>& pushed, vector<int>& popped) 
    {
        stack<int> st;
        int i = 0;     // 遍历出栈序列popped用的下标
        for(auto& e : pushed)
        {
            st.push(e);     // 入栈序列先入栈
            // 比较:相同则出栈且继续比较
            while(!st.empty() && st.top() == popped[i])
            {
                st.pop();
                ++i;
            }
        }
        //return i == popped.size();
        return st.empty();
    }
};

eb87b21e5fca46f788c7a6bfa3fd63af.png


3. 逆波兰表达式求值


给你一个字符串数组 tokens ,表示一个根据逆波兰表示法表示的算术表达式。

请你计算该表达式。返回一个表示表达式值的整数。


注意:


有效的算符为 ‘+’、‘-’、‘*’ 和 ‘/’ 。

每个操作数(运算对象)都可以是一个整数或者另一个表达式。

两个整数之间的乘法总是 向零截断 。

表达式中不含除零运算。

输入是一个根据逆波兰表示法表示的算术表达式。

答案及所有中间计算结果可以用 32 位 整数表示。

d67fb2b2156e4e64901d5d293be3d72a.png


逆波兰表达式是一种后缀表达式,而我们日常学习用到的是中缀表达式。中缀表达式操作符在中间,操作数在两遍;而后缀表达式则操作符在后面,操作数在前面。如:前缀表达式2 + 3 * 4对应的后缀表达式为2 3 4 * +。那为什么要有后缀表达式呢?因为计算机是顺序读入表达式的,当读到操作符时,计算机就会去两个操作数进行运算。如果采用的是中缀表达式,计算机将无法识别该操作符后是否存在更高级的操作符。而后缀表达式不会存在这样的情况,可以依据后缀表达式的次序计算出正确结果。


那么解决这道题的思路是什么呢?其实很简单,就是遇到数字则入栈;遇到操作符则取栈顶两个数组进行计算(对于减法和除法,需区分左操作数和右操作数),并将结果压入栈中。遍历结束,栈顶元素就是后缀表达式的计算结果。


6fe3bdde64ef4e6fa74e8c699a7993d3.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])  // 计算并将计算结果压入栈中
                {
                    case '+':
                        st.push(left + right);
                        break;
                    case '-':
                        st.push(left - right);
                        break;
                    case '*':
                        st.push(left * right);
                        break;
                    case '/':
                        st.push(left / right);
                        break;                       
                }
            }
            else
            {
                st.push(stoi(str));   // 操作数入栈
            }
        }
        return st.top();    // 后缀表达式的结果
    }
};

a2ef87ce2c1a4f4c8fad24ac3b1006e9.png


3fe00a5792c742a282ed67a1af96eb8d.png

0457454abec5456dad5b11ce122d37e4.png


2bb4c88aed4f4387a03bcc298968bea0.png



152f0f8708114c2484583d160f4f9497.png


stoi 函数的第二个参数是一个输出型参数,其值为 str 转化成整型数字时的结束位置的下一个位置,该参数可以设置为nullptr,表明不关心该值;第三个参数则表示要转化的字符串 str 里的数字是什么进制的。该参数的缺省值为 10,表示 str 里的数字是十进制的。如果该值被设置为 0 ,则需要根据 str 里的标致来判别其数字是什么进制的。


stoi 函数的演示使用


#include <iostream>
#include <string>     
using namespace std;
int main()
{
  string str_dec = "2001, A Space Odyssey";
  string str_hex = "40c3";
  string str_bin = "-10010110001";
  string str_auto = "0x77";
  std::string::size_type sz;   // alias of size_t
  int i_dec = std::stoi(str_dec, &sz);
  int i_hex = std::stoi(str_hex, nullptr, 16);  // 十六进制
  int i_bin = std::stoi(str_bin, nullptr, 2);   // 2进制
  int i_auto = std::stoi(str_auto, nullptr, 0); // 
  cout << str_dec << ": " << i_dec << " and [" << str_dec.substr(sz) << "]\n";
  // substr(sz)表示从sz位置取到str_dec的结尾作为子串
  cout << str_hex << ": " << i_hex << '\n';
  cout << str_bin << ": " << i_bin << '\n';
  cout << str_auto << ": " << i_auto << '\n';
  return 0;
}

eb67d1c780844385a8f2c25e8b0b23eb.png


4. 中缀表达式转为后缀表达式


注:该中缀表达式中包含 + - * / ( ) =等符号,且包含空格。转换得到的后缀表达式也不包含空格。


那么如何将中缀表达式转为后缀表达式呢?


从左往右遍历中缀表达式,跳过空格

遇到操作数直接输出

如果当前操作符为左括号,左括号直接入栈

如果当前操作符为右括号,让栈顶到左括号之间的操作符出栈添加到后缀表达式中,括号不出现在后缀表达式中

如果当前操作符优先级小于或等于栈顶操作符的优先级,先将栈顶操作符出栈直至栈顶操作符优先级小于当前操作符优先级,再将当前操作符入栈

遍历完中缀表达式,将栈中的操作符出栈添加到后缀表达式中


注意:出栈有可能会出多个操作符;栈为空时,操作符一定要入栈。遍历结束时,栈内的操作符要依次出栈。


class Solution 
{
public:
    vector<string> convertToRPN(vector<string> &expression) 
    {
        vector<string> ret;
        stack<string> st;   // 存储操作符的栈
        for(auto& str : expression)
        {
            if(str == "(")  // 左括号直接入栈
                st.push(str);
            else if(str == ")")
            {
                // 左括号上面的操作符全部出栈添加到后缀表达式中
                while(st.top() != "(")
                {
                    ret.push_back(st.top());
                    st.pop();
                }
                st.pop();   // 左括号出栈但不添加到后缀表达式中
            }
            else if(str == "+" || str == "-" || str == "*" || str == "/")
            {
                // 栈顶操作符优先级大于或等于当前操作符,则出栈
                while(!st.empty() && compare(st.top(), str))
                {
                    ret.push_back(st.top());
                    st.pop();
                }
                st.push(str);   // 当前操作符入栈
            }
            else
            {
                ret.push_back(str);   // 数字直接添加到后缀表达式中
            }
        }
        while(!st.empty())
        {
            ret.push_back(st.top());
            st.pop();
        }
        return ret;
    }
private:
    // 比较操作符的优先级:first >= second时,返回 true
    bool compare(const string& first, const string& second)
    {
        if(first == "*" || first == "/")
            return true;
        else if(first == "(")
            return false;
        else if(second == "+" || second == "-")
            return true;
        else
            return false;
    }
};

11d64d85a888407bac8b5eab570c8786.png

41f9fa08864643e18b37acef2297cb12.png




















相关文章
|
3月前
|
存储 算法 调度
【C++打怪之路Lv11】-- stack、queue和优先级队列
【C++打怪之路Lv11】-- stack、queue和优先级队列
51 1
|
3月前
|
设计模式 存储 C++
C++之stack 和 queue(下)
C++之stack 和 queue(下)
47 1
|
3月前
|
C++ 容器
C++之stack 和 queue(上)
C++之stack 和 queue(上)
80 0
|
3月前
|
存储 C++ 容器
C++番外篇——stack、queue的实现及deque的介绍
C++番外篇——stack、queue的实现及deque的介绍
33 0
|
3月前
|
存储 算法 C++
C++入门10——stack与queue的使用
C++入门10——stack与queue的使用
57 0
|
2天前
|
C++ 芯片
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
32 18
|
2天前
|
存储 编译器 数据安全/隐私保护
【C++面向对象——类与对象】CPU类(头歌实践教学平台习题)【合集】
声明一个CPU类,包含等级(rank)、频率(frequency)、电压(voltage)等属性,以及两个公有成员函数run、stop。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。​ 相关知识 类的声明和使用。 类的声明和对象的声明。 构造函数和析构函数的执行。 一、类的声明和使用 1.类的声明基础 在C++中,类是创建对象的蓝图。类的声明定义了类的成员,包括数据成员(变量)和成员函数(方法)。一个简单的类声明示例如下: classMyClass{ public: int
30 13
|
2天前
|
编译器 数据安全/隐私保护 C++
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
20 5
|
2天前
|
存储 算法 搜索推荐
【C++面向对象——群体类和群体数据的组织】实现含排序功能的数组类(头歌实践教学平台习题)【合集】
1. **相关排序和查找算法的原理**:介绍直接插入排序、直接选择排序、冒泡排序和顺序查找的基本原理及其实现代码。 2. **C++ 类与成员函数的定义**:讲解如何定义`Array`类,包括类的声明和实现,以及成员函数的定义与调用。 3. **数组作为类的成员变量的处理**:探讨内存管理和正确访问数组元素的方法,确保在类中正确使用动态分配的数组。 4. **函数参数传递与返回值处理**:解释排序和查找函数的参数传递方式及返回值处理,确保函数功能正确实现。 通过掌握这些知识,可以顺利地将排序和查找算法封装到`Array`类中,并进行测试验证。编程要求是在右侧编辑器补充代码以实现三种排序算法
17 5
|
2天前
|
Serverless 编译器 C++
【C++面向对象——类的多态性与虚函数】计算图像面积(头歌实践教学平台习题)【合集】
本任务要求设计一个矩形类、圆形类和图形基类,计算并输出相应图形面积。相关知识点包括纯虚函数和抽象类的使用。 **目录:** - 任务描述 - 相关知识 - 纯虚函数 - 特点 - 使用场景 - 作用 - 注意事项 - 相关概念对比 - 抽象类的使用 - 定义与概念 - 使用场景 - 编程要求 - 测试说明 - 通关代码 - 测试结果 **任务概述:** 1. **图形基类(Shape)**:包含纯虚函数 `void PrintArea()`。 2. **矩形类(Rectangle)**:继承 Shape 类,重写 `Print
18 4