1. 定义
    只允许在表的末端进行插入和删除的线性表。允许插入删除的叫栈顶,不允许插入删除的叫栈底。其性质为后进先出。

  2. 基本操作

    • 初始化
    • 求长度
    • 取栈顶元素
    • 入栈
    • 出栈
    • 判断栈是否为空
    • 清空栈
  3. 常见存储方式:顺序存储与链式存储

顺序存储

  1. 顺序栈的类模板定义
    元素:栈顶指针(下标形式)、最大容量、数据元素的数组(elems)
    函数:构造函数、析构函数、入栈、出栈、求长度等等。

  2. 注意点

    • 对于top指针的初值可以设置为两种,-1或0。在本文中设置为-1.
    • 不同的top初值在写函数时操作完全不同。主要体现在入栈与出栈上
      top==-1,先加一,再入栈;先出栈,再减一。
      top== 0 , 先入栈,再加一;先减一,再出栈。
  3. 类模板定义

    template<class ElemType>
    class SeqStack
    {
    protected:
    // 顺序栈的数据成员:
    int top1; // 栈顶指针
    int maxSize; // 栈的最大容量
    ElemType* elems; // 元素存储空间

    public:
    // 顺序栈的方法声明及重载编译系统默认方法声明:
    SeqStack(int size = DEFAULT_SIZE); // 构造函数
    virtual ~SeqStack(); // 析构函数
    int GetLength() const; // 求栈的长度
    bool IsEmpty() const; // 判断栈是否为空
    void Clear(); // 将栈清空
    void Traverse(void (*Visit)(const ElemType&)) const; // 遍历栈
    Status Push(const ElemType e); // 入栈
    Status Top(ElemType& e) const; // 取顶元素
    Status Pop(ElemType& e); // 出栈
    SeqStack(const SeqStack<ElemType>& s); // 复制构造函数
    SeqStack<ElemType>& operator =(const SeqStack<ElemType>& s);
    // 赋值语句重载
    };
  4. 入栈出栈代码

    template<class ElemType>// 入栈
    Status SeqStack<ElemType>::Push(const ElemType e)
    // 操作结果:将元素e追加到栈顶,如成功则返加SUCCESS,如栈已满将返回OVER_FLOW
    {
    if (top == maxSize - 1) // 栈已满
    return OVER_FLOW;
    else { // 操作成功
    elems[++top] = e; // 将元素e追加到栈顶
    return SUCCESS;
    }
    }

    注意点:入栈判满;top初值为-1,先加一,再入栈。

    template<class ElemType>
    Status SeqStack<ElemType>::Pop(ElemType& e)
    // 操作结果:如栈非空,删除栈顶元素,并用e返回栈顶元素,函数返回SUCCESS,否则
    // 函数返回UNDER_FLOW
    {
    if (IsEmpty()) // 栈空
    return UNDER_FLOW;
    else { // 操作成功
    e=elems[top--];
    return SUCCESS;
    }
    }

    **注意点:出栈判空;top初值为-1,先出栈,再减一。

  5. 共享数组的顺序栈
    对于顺序存储来说,存储空间很浪费,而且容易产生溢出现象,因此共享数组的顺序栈存储方式出现了。其本质是将一个顺序栈放在数组头,另一个放在数组的尾,等于共享顶部的资源空间。

    • 栈满情况 top1== top2-1
    • 栈空情况 top1等于0 并且 top2等于 maxsize
    • 对于栈的基本操作需要指明具体操作哪一个栈,所以对基本所有的类内成员函数需要多加一个参数no来确定要操作的是哪一个栈。
      修改后的类模板定义如下
    template<class ElemType>
    class SeqStack
    {
    protected:
    // 顺序栈的数据成员:
    int top1,top2; // 栈顶指针
    int maxSize; // 栈的最大容量
    ElemType* elems; // 元素存储空间
    public:
    // 顺序栈的方法声明及重载编译系统默认方法声明:
    SeqStack(int size = DEFAULT_SIZE); // 构造函数
    virtual ~SeqStack(); // 析构函数
    int GetLength() const; //求长度
    bool IsEmpty(int no) const; // 判断栈是否为空
    void Clear(); // 将栈清空
    void Traverse(void (*Visit)(const ElemType&),int no) const; // 遍历栈
    Status Push(const ElemType e); // 入栈
    Status Top(ElemType& e,int no) const; // 取顶元素
    Status Pop(ElemType& e,int no); // 出栈
    SeqStack(const SeqStack<ElemType>& s); // 复制构造函数
    SeqStack<ElemType>& operator =(const SeqStack<ElemType>& s); // 赋值语句重载
    };

链式存储

  1. 对于链式栈采用链表的方式存储元素,可以同时让多个栈共享存储。这里主要使用不带表头的单链表实现链式栈。表头为栈顶,入栈出栈都是对表头进行操作。
    问题:
    1)栈顶放在链表的头部/尾部,哪个操作起来方便?

    • 表头,因为表尾不便于删除,每次删除都需要遍历找前驱。

    2)带头结点和不带头结点相比,有多大影响?

    • 不带头结点链表的入栈出栈都在表头进行,比较简单方便。

    3)和链表相比,类定义主要不同点在哪里?

    • 主要区别在于链式栈只对头结点操作,而链表有插入删除等等对于中间结点的操作。
  2. 链式栈类模板定义
    元素:只需要一个栈顶指针即可
    成员函数:构造函数、析构函数、入栈与出栈、判空

    template<class ElemType>
    class LinkStack
    {
    protected:
    // 链栈的数据成员:
    Node<ElemType> *top; // 栈顶指针
    public:
    // 链栈的函数成员:
    LinkStack(); // 无参数的构造函数
    virtual ~LinkStack(); // 析构函数
    int GetLength() const; // 求栈长度
    bool IsEmpty() const; // 判断栈是否为空
    void Clear(); // 将栈清空
    void Traverse(void (*Visit)(const ElemType &)) const ; // 遍历栈
    Status Push(const ElemType e); // 入栈
    Status Top(ElemType &e) const; // 返回栈顶元素
    Status Pop(ElemType &e); // 出栈
    LinkStack(const LinkStack<ElemType> &s); // 复制构造函数
    LinkStack<ElemType> &operator =(const LinkStack<ElemType> &s); // 赋值语句重载
    };
  3. 入栈出栈

    template<class ElemType>
    Status LinkStack<ElemType>::Push(const ElemType e)
    // 操作结果:将元素e追加到栈顶,如成功则返加SUCCESS,否则如动态内存已耗尽
    // 将返回OVER_FLOW
    {
    Node<ElemType> *p = new Node<ElemType>(e, top);
    if (p == NULL) // 系统内存耗尽
    return OVER_FLOW;
    else { // 操作成功
    top = p;
    return SUCCESS;
    }
    }

    虽然说链式存储不用像顺序存储那样考虑空间的问题,但是系统在分配动态内存时还是存在动态内存耗尽的可能性,所以仍然要对动态开辟的空间进行判空操作。

    template<class ElemType>
    Status LinkStack<ElemType>::Pop(ElemType &e)
    // 操作结果:如栈非空,删除栈顶元素,并用e返回栈顶元素,函数返回SUCCESS,否则
    // 函数返回UNDER_FLOW
    {
    if (IsEmpty()) // 栈空
    return UNDER_FLOW;
    else { // 操作成功
    Node<ElemType> *p = top; // 保留原栈顶
    e = top->data; // 用e返回栈顶元素
    top = top->next; // 修改栈顶
    delete p; // 删除原栈顶结点
    return SUCCESS;
    }
    }

    出栈首先要判空,然后top向后移动一位,再将之前的栈顶删除

  4. 栈的两种存储结构的比较

  • 栈的顺序存储结构是一种静态的存储结构,必须确定存储空间的大小,太大会造成存储空间的浪费,太小会因栈满而产生溢出。
  • 栈的链接存储结构是一种动态的存储结构,因为结点是动态分配的,所以不会出现溢出问题。

栈的应用——表达式求值

  1. 中缀表达式:
    我们自然使用的数学表达式,按照规定的优先级(先乘除后加减)进行运算。
    如:(32-26)*5 + 28 / 4

  2. 后缀表达式:
    中缀表达式由于优先级的问题,虽然利于人的理解,但不便于使用计算机语言来实现,所以引入后缀表达式,将运算符放在运算对象之后。
    如:32 26 -5 * 28 4 / +
    特点:运算按照运算符出现顺序进行,没有括号。
    由后缀表达式延伸的还有前缀表达式,即把每一个运算符放在运算对象之前。

  3. 利用栈实现中缀表达式的存储与计算
    具体过程:从左向右扫描;遇到操作数,操作数入栈;遇到操作符,操作数按数量出栈进行计算,再将结果入栈;直至扫描完整个表达式,完成计算。
    异常分析:操作数与操作符一定是对应准确的,如果表达式扫描完成,栈空或栈中仍有操作数,则后缀表达式错误。
    后缀表达式计算代码:

    void PostfixExpressionCalculation ()
    // 操作结果:后缀式的计算
    {
    LinkStack<double> opnd; // 操作数栈
    char ch; // 临时字符
    double operand, first, second; // 操作数

    cout << "输入后缀表达式,以'#'号结束:";
    ch = GetChar(); // 读入一个字符
    while (ch != '#') { // 当前表达式还未运算结束, 继续运算
    if (isdigit(ch) || ch == '.') { // ch为一个操作数的第1个字符
    cin.putback(ch); // 将字符ch放回输入流
    cin >> operand; // 读入操作数
    opnd.Push(operand); // 操作数入栈
    }
    else if(!IsOperator(ch) || ch == '(' || ch ==')') {// ch既不是操作数,也不属于操作符
    throw Error("表达式中有非法符号!"); // 抛出异常
    }
    else { // ch为操作符
    if (opnd.Pop(second) == UNDER_FLOW) // 取第二操作数
    throw Error("缺少操作数!"); // 取数失败,则抛出异常
    if (opnd.Pop(first) == UNDER_FLOW) // 取第一操作数
    throw Error("缺少操作数!"); // 取数失败,抛出异常
    opnd.Push(Operate(first, ch, second)); // 运算结果入opnd栈
    }
    ch = GetChar(); // 读入新字符
    }
    if (opnd.Pop(operand) == UNDER_FLOW)
    throw Error("缺少操作数!"); // 抛出异常
    if (!opnd.IsEmpty())
    throw Error("缺少操作符!"); // 抛出异常
    cout << "表达式结果为:" << operand << endl; // 显示表达式的值
    };
  4. 中缀表达式转后缀表达式
    中缀表达式与后缀表达式的操作数顺序是一致的,只是中缀表达式多了优先级。
    具体过程:

    • 操作符栈初始化,将结束符"#"入栈,读入中缀表达式的首字符ch。
    • 分三种情况重复一下步骤,直至ch与栈顶操作符op同为“#”,停止循环。

      1.ch为数字或.说明当前读入为操作数。先判断前一项是否为操作数或“)”,如果是上述情况则抛出异常;否则退回ch,输入整个操作数,并直接输出操作数,操作数计数器++。
      2.ch不为1中的情况也不是操作符,非法字符,抛出异常。
      3.ch不为上述情况,ch为操作符。

      1)若为“(”,但前项为操作数或“)”,则抛出异常;
      2)若op优先级高于ch,则进行循环,先判断未被使用的操作数数量,如果数量小于2,则缺少操作数,发生异常。反之,将op出栈,再比较新栈顶操作符与输入操作符的优先级决定出栈与否。
      3)若op与串不存在优先级,则说明有非法字符,抛出异常。
      4)若op的优先级低于ch,则ch入栈,继续读下一个字。
      5)若op的优先级等于ch,说明op与ch为“(”和“)”,后缀表达式不需要括号,op退栈,读下一个字。

    代码实现:

    void InfixInToPostfix()
    // 操作结果:中缀式转换为后缀式
    {
    LinkStack<char> optr; // 操作符栈
    char ch; // 临时字符
    char priorChar; // 当前输入的前一个字符
    char op = '#'; // 操作符栈的栈顶字符
    double operand; // 操作数
    int operandCount = 0; // 操作数计数器
    optr.Push('#'); // 在操作符栈中加入一个'#'
    priorChar = '#'; // 前一字符
    cout << "输入中缀表达式,以'#'号结束:";
    ch = GetChar(); // 读入一个字符
    while (op != '#' || ch != '#') { // 当前表达式还未运算结束, 继续运算
    if (isdigit(ch) || ch == '.') { // ch为一个操作数的第1个字符
    if (priorChar == '0' || priorChar == ')')
    throw Error("两个操作数之间缺少运算符!"); // 抛出异常
    cin.putback(ch); // 将字符ch放回输入流
    cin >> operand; // 读入操作数
    cout << operand << " "; // 输出操作数
    operandCount++; // 操作数数目加1
    priorChar = '0'; // 前一字符是操作数
    ch = GetChar(); // 读入下一个字符
    }
    else if(!IsOperator(ch)) { // 既不是操作数,也不是操作符
    throw Error("表达式中有非法符号!"); // 抛出异常
    }
    else { // ch为操作符
    if (ch == '(' && (priorChar == '0' || priorChar == ')'))
    throw Error("'('前缺少操作符!"); // 抛出异常
    while (OperPrior(op, ch) == 2) {
    if (operandCount < 2)
    throw Error("缺少操作数!");
    operandCount--;
    optr.Pop(op);
    cout << op << " ";
    if (optr.Top(op) == UNDER_FLOW)
    throw Error("缺少操作符!"); // 抛出异常
    }
    switch (OperPrior(op, ch)) {
    case -1 : throw Error("括号不匹配!");
    case 0 : optr.Pop(op);
    if (optr.Top(op) == UNDER_FLOW)
    throw Error("缺少操作符!"); // 抛出异常
    priorChar = ch; // 新的前一字符为(
    ch = GetChar(); // 读入新字符
    break;
    case 1 : optr.Push(ch);
    op = ch;
    priorChar = ch; // 新的前一字符为)
    ch = GetChar(); // 读入新字符
    break;
    }
    }
    }
    if (operandCount != 1)
    throw Error("缺少操作数!"); // 抛出异常
    cout << endl;
    };
  5. 中缀表达式简单转换后缀方式
    给所有的操作步骤加上括号,将操作符对应移动到括号外,删除括号。