基本定义

栈是限定仅在表尾进行插入或删除操作的线性表。先进后出

通常,表头端称为栈底,表尾端称为栈顶。

栈只是对表插入和删除操作的位置进行了限制,并没有限定插入和删除操作进行的时间。

基本操作
InitStack( &S )              //构造空栈
StackEmpty( S )              //判断栈空
Push( &S, e )                //出栈
Pop( &S, &e )                //退栈、出栈
GetTop( S, &e )              //取出栈顶元素
DestroyStack( &S )           //销毁栈
ClearStack( &S )             //清空栈
StackLength( S )             //返回栈长    
StackTraverse( S, visit() )  //遍历栈    
栈的表示和实现

顺序栈

base指针指向线性表的第一个元素,称为栈底指针。

top指针指向线性表的最后一个元素的下一个位置,称为栈顶指针。

栈不存在:base == NULL;空栈:base == top

//结构的定义
typedef int SElemType;
typedef struct
{
    SElemType *base;   //栈底指针
    SElemType *top;    //栈顶指针
    int stacksize;     //当前可用空间大小
}SqStack;

readme

上面结构的定义只是为了更加方便理解顺序栈

下面代码是栈的顺序实现,需要借助顺序表进行实现

由于和顺序表基本一致,我们采用类的继承来简化代码

同时,我们省略了下面代码的头文件

如需运行,和顺序表相关代码结合即可

需要将顺序表中析构函数设为虚函数

//顺序表实现栈,继承自List
//使用L来表示栈,另外设栈顶和栈底指针
template <typename ElemType>
class Stack :public List<ElemType>
{
public:
    ElemType* base;//指向L实现的栈的栈底
    ElemType* top;//指向L实现的栈的栈顶
    Stack();//构造函数
    ~Stack();//析构函数
    Status InitStack();//栈的初始化
    Status Stack_Pop();//弹出栈顶元素
    Status Stack_Push(ElemType x);//压入栈元素
    Status Stack_Destroy();//销毁栈
    Status Stack_Clear();//清空栈
    ElemType Stack_GetTop();//返回栈顶元素
    Status Stack_Empty();//判断栈是否为空
};

//Stack的构造函数
template <typename ElemType>
Stack<ElemType>::Stack()
{
    InitStack();
}

//Stack的析构函数,只需要在base和top非空时释放指针即可
template <typename ElemType>
Stack<ElemType>::~Stack()
{
    if (!base)
        free(base);
    if (!top)
        free(top);
}

//Stack的初始化
template <typename ElemType>
Status Stack<ElemType>::InitStack()
{
    //若构造的类为模板类,那么派生类不可以直接使用继承到的基类数据和方法,需要通过this指针使用
    List<ElemType>::InitList_Sq();
    base = this->L.elem;
    top = base;//初始化的时候,top = base
    return OK;
}
//从栈的初始化我们也能看出来
//base为空代表栈不存在,而base == top时代表栈为空

//栈的pop其实就是顺序表删除最后一个元素(相当于栈顶元素)
template <typename ElemType>
Status Stack<ElemType>::Stack_Pop()
{
    ElemType e;
    //这里根据需要设置e,如需返回pop的元素值,可以在参数列表中增加一个实参,将pop的元素值返回,同List_Delete
    List<ElemType>::List_Delete(this->L.length - 1, e);
    //不采取-1是为了避免删除失败而导致top错误
    //另一种写法是根据List_Delete的返回值判断是否-1
    top = this->L.elem + this->L.length;
    return OK;
}

template <typename ElemType>
Status Stack<ElemType>::Stack_Push(ElemType x)
{
    //尽管我们在线性表实现中已经考虑了空间不足L指针变化,
    //但由于base和top并未同时改变,所以我们还需在此基础上对top和base重新赋值
    base = this->L.elem;
    top = this->L.elem + this->L.length;
    List<ElemType>::List_Insert(this->L.length,x);
    ++top;//保持top始终指向最后一个元素的下一个位置
    return OK;
}

//对栈进行销毁,不仅需要考虑我们在stack子类中新增的指针
//还需要对父类中设置的elem指针进行释放
template <typename ElemType>
Status Stack<ElemType>::Stack_Destroy()
{
    if (!base || !top)
    {
        printf("\nREFREE\n");
        exit(REFREE);
    }
    free(base);//栈底指针的释放
    free(top);//栈顶指针的释放
    //释放后将指针置空,避免成为野指针
    base = NULL;
    top = NULL;
    List<ElemType>::List_Destroy();//栈的释放
    return OK;
}

//栈的清空,不需要将栈销毁,只需要清空栈的元素
//由于栈的元素是在父类中,我们只需要调用父类函数即可
template <typename ElemType>
Status Stack<ElemType>::Stack_Clear()
{
    List<ElemType>::List_Clear();
    base = this->L;
    top = this->L;
    return OK;
}

//得到栈顶元素,一个看似鸡肋却很实用的功能
template <typename ElemType>
ElemType Stack<ElemType>::Stack_GetTop()
{
    if (base == top)
        return ERROR;
    //再次提醒,top指向最后一个元素的下一个位置
    return *(top-1);
}

//判断栈是否为空,结合栈的初始化函数来看,更加容易理解
template <typename ElemType>
Status Stack<ElemType>::Stack_Empty()
{
    if(!base)
    {
        printf("\nError!!No exist\n");
        exit(ERROR);
    }
    if (top == base)
        return TRUE;
    return FALSE;
}

链式栈

同顺序栈,我们借助链表来实现链式栈

其实仔细思考就会发现,链表和链式栈的底层原理都一样,只是一些函数(或者叫方法)不同,我们在后续代码中会更加详细的看到。

为了方便出栈和入栈操作,我们将链表的首元结点作为栈顶。(如果采用链表结尾,我们还需要遍历到尾部,增加了不必要开销)

top指针指向头结点(首元结点的前一个附加节点),这符合top指针指向最后一个元素的后一个位置的特点。

栈不存在:top == NULL;空栈:top -> next == NULL;(top指向最后一个元素的下一个位置,这里top->next实际就是最后一个元素,为空时自然就代表栈为空)

//结构的定义
typedef int SElemType;
typedef struct SNode
{
    SElemTye data;
    dtruct SNode *next;
}SNode, *LinkStack;

readme

上面结构的定义只是为了更加方便理解链式栈

下面代码是栈的链式实现,需要借助链表进行实现

由于和链表基本一致,我们采用类的继承来简化代码

同时,我们省略了下面代码的头文件

如需运行,和链表相关代码结合即可

需要将链表中析构函数设为虚函数

//链式表实现栈,继承自Link
template <typename ElemType>
class Stack :public Link<ElemType>
{
public:
    //链表的头指针即为top指针
    //不再专门设base指针(实际为链表最后一个元素)
	Stack();//构造函数
	~Stack();//析构函数
	Status InitStack();//栈的初始化
	Status Stack_Pop();//弹出栈顶元素
	Status Stack_Push(ElemType e);//压入栈元素
	Status Stack_Destroy();//销毁栈
	Status Stack_Clear();//清空栈
	ElemType Stack_GetTop();//返回栈顶元素
	Status Stack_Empty();//判断栈是否为空
};

//Stack的构造函数
template <typename ElemType>
Stack<ElemType>::Stack()
{
	Link<ElemType>::Init_LinkList();
}

//Stack的析构函数
template <typename ElemType>
Stack<ElemType>::~Stack()
{
    //这里子类中没有增加需要释放的指针
    //在保证父类虚构函数时会自动释放空间
}

//Stack的初始化
template <typename ElemType>
Status Stack<ElemType>::InitStack()
{
	Link<ElemType>::Init_LinkList();
}

template <typename ElemType>
Status Stack<ElemType>::Stack_Pop()
{
	ElemType e;//e暂时用不到,如需使用,在参数列表中增加实参即可
	Link<ElemType>::LinkList_Delete(0, e);
    //LinkList_Delete不会改变头指针,即top指针仍然指向链表第一个元素(链式栈的最后一个元素)
	return OK;
}

template <typename ElemType>
Status Stack<ElemType>::Stack_Push(ElemType e)
{
    //同delete函数,实质就是在链式栈末尾(也就是链表第一个元素掺入和删除)
	Link<ElemType>::LinkList_Insert(0, e);
	return OK;
}

//不同于析构函数自动调用父类析构,我们需要手动回收
template <typename ElemType>
Status Stack<ElemType>::Stack_Destroy()
{
	Link<ElemType>::LinkList_Destroy();
	return OK;
}

template <typename ElemType>
Status Stack<ElemType>::Stack_Clear()
{
	Link<ElemType>::LinkList_Clear();
	return OK;
}

template <typename ElemType>
ElemType Stack<ElemType>::Stack_GetTop()
{
	if (!this->L)
	{
		printf("Error!!NO Exist\n");
		exit(ERROR);
	}
	if (!(this->L->next))
	{
		printf("Error!!NULL\n");
		exit(ERROR);
	}
	return this->L->next->data;
}

template <typename ElemType>
Status Stack<ElemType> ::Stack_Empty()
{
	if (!this->L)
	{
		printf("Error!!NO Exist\n");
		exit(ERROR);
	}
	if (!this->L->next)
		return TRUE;
	return FALSE;
}

顺序表和链表的比较

  • 时间性能:相同,都是常数时间O(1)
  • 空间性能:顺序栈有元素个数的限制和空间浪费的问题;链式栈,没有栈满的问题,只有当内存没有可用空间时才会出现栈满,但是每一个数据元素都需要一个指针域,增加了结构性开销.
  • 总之,在栈的使用过程中,如果元素个数变化较大,用链栈比较适宜,反之应采用顺序栈.
栈的应用

简单的比如,数制转换(实际上只是在输出时用到了栈),括号匹配等。下面重点分析表达式求值和中缀后缀表达式转换。

四则表达式求值

readme
首先我们需要确保表达式的正确性。
需要设置两个工作栈,分别是运算符栈和操作数栈。
从左往右读取表达式
A.当遇到操作数的时候,直接将操作数进栈。
B.当遇到运算符的时候,需要分为以下三种情况
1.如果当前运算符高于栈顶运算法,则当前运算符进栈,遍历下一个数据
2.如果当前运算符等于栈顶运算符,则将栈顶元素出栈,并遍历下一个元素
3.如果当前运算符低于栈顶运算符,则将栈顶运算符弹出,同时将操作数栈弹出两个操作数,根据栈顶运算符进行运算。这时候不进行表达式下一个数据遍历,仍然保持当前运算符。
----几点说明:
----1.运算符优先级比较时,我们是将相同运算符(除了括号和#)视作不相等的,即如果栈顶运算符和当前运算符相等,那我们视作当前运算符低于栈顶运算符,也就是要进行运算.
----2.只有括号和#才能视作相等,即左括号等于右括号,#等于#
----3.为了保证所有操作符都进行运算(在保证表达式正确基础上),我们在表达式结尾增加一个#
----4.左括号视作优先级最高,即无条件入栈;而右括号由优先级最低,会一直进行运算,直到碰到左括号,然后相等,左括号弹出,同时进行表达式下一个数据遍历.

下面给出一种代码实现

输入:一行字符串,允许出现空格和浮点数

输出:判断表达式是否正确,如果错误,不进行计算.计算结果采用浮点数表示

以下为非法输入,并提示“input error”

1.负数出现在非首位且不加括号

2.运算符(减号出现在首位视为负号)出现在首尾,或者连续出现

3.小数点左右两边存在不为数字的(包括空格

4.小数点出现在末尾非法

5.左右括号不匹配

6.左括号右侧出现 + * / ,右括号左侧出现 + - * /

7.出现 + - * / ( ) 数字,小数点和空格外的任何符号

8.数字右侧出现左括号,数字左侧出现右括号

说明:我们规定如果表达式中含有负数,除了首位的负数可以省略括号外,其余必须在括号内

//判断表达式是否合法
int judge(std::string& str)
{
    //首先我们将字符串中空格去除
    //同时,判断左右括号是否合法,数字是否合法
    int cnt_l = 0;//左括号数量
    int cnt_r = 0;//右括号数量
    std::string s = "";
    for (unsigned int i = 0; i < str.size(); ++i)
    {
        if (str[i] == '(')
            ++cnt_l;
        else if (str[i] == ')')
            ++cnt_r;
        else if (str[i] == '.')
        {
            if (i == 0 || i == s.size() - 1)//小数点在首尾为false
                return FALSE;
            else if (!isdigit(str[i - 1]) || !isdigit(str[i + 1]))//左右不是数字非法
                return FALSE;
        }
        else if (str[i] == ' ')//数字之间存在空格非法
        {
            if (i != 0 && i != str.size() - 1)
                if(isdigit(str[i - 1]) && isdigit(str[i + 1]))
                    return FALSE;
        }
        if (cnt_l < cnt_r)//从左往右,右括号多了就说明左右已经不匹配了
            return FALSE;
        if (str[i] != ' ')
            s += str[i];
    }
    if (cnt_l != cnt_r)//左右括号不相等非法
        return FALSE;
    //接下来就是判断运算符是否合法
    for (unsigned int i = 0; i < s.size(); ++i)
    {
        if (isdigit(s[i]) || s[i] == '.')//跳过数字
        {
            if (i > 0 && s[i - 1] == ')')
                return FALSE;
            else if (i < s.size()-1 && s[i + 1] == '(')
                return FALSE;
        }
        else if (s[i] == '+' || s[i] == '*' || s[i] == '/')
        {
            if (i == 0 || i == s.size() - 1)//+ * / 运算符在首尾非法
                return FALSE;
            else if (!(isdigit(s[i - 1]) || s[i - 1] == ')'))//+ * / 运算符前如果不是数字或者)非法
                return FALSE;
            else if (!(isdigit(s[i + 1]) || s[i + 1] == '('))//+ * / 运算符后如果不是数字或者 ( 非法, ( 是代表后面为负数
                return FALSE;
        }
        else if (s[i] == '-')
        {
            if (i == s.size() - 1)//末尾非法
                return FALSE;
            else if (i == 0)//出现在第一位的减号非法
            {
                if(!(isdigit(s[i + 1])||str[i+1]=='('))//这句话不能写在上面的条件判断里面,已改
                    return FALSE;
            }
            else if (!(isdigit(s[i - 1]) || s[i - 1] == '(' || s[i - 1] == ')'))
                return FALSE;
            else if (!(isdigit(s[i + 1]) || s[i + 1] == '('))
                return FALSE;
        }
        else if (s[i] == '(' && s[i + 1] == ')')//空括号返回FALSE,表达式的括号匹配保证了(不会出现在末尾,i+1不会溢出
            return FALSE;
        else if(s[i] != '('&&s[i] != ')')
            return FALSE;
    }
    str = s;//如果合法,替换str
    return TRUE;
}

//a是运算符栈顶元素,也就是表达式中先出现的运算符
//b是当前运算符,为后出现的运算符
//a<b -1;a=b 0;a>b 1;
//(无条件入栈,右括号无条件计算
//运算符优先级相等视作>,即要进行运算
//由于输入的合法性,a不会为);a为#时,b不会为)
//我们会在表达式末尾加上#以避免最后再进行一次处理,所以需要考虑b为#的情况
int Compare(char a, char b)
{
    if (a == '+' || a == '-')
    {
        if (b == '*' || b == '/' || b == '(')//将b入栈
            return -1;
        return 1;//b只能为+-,进行运算
    }
    else if (a == '*' || a == '/')
    {
        if (b == '(')//将b进栈
            return -1;
        return 1;
    }
    else if (a == '(')//栈顶还为(时,一定未到表达式末尾,所以b不会为#
    {
        if (b == ')')
            return 0;
        return -1;//实际中不会出现b为#
    }
    //a不能为)
    else//这时a只能为#
    {
        if (b == '#')
            return 0;
        return -1;
    }
}

double cmp(std::string& str,Stack<double>& s1, Stack<char>& s2)//四则运算表达式求值
{
    str += '#';//附加#,如果出现到表达式结尾运算符栈仍然包含多个运算符时能继续运算,直到为空(最后一个#和附加#相同出栈)
    s2.Stack_Push('#');
    double x;
    std::string s = "";//预处理字符段
    std::stringstream ss;
    unsigned int i = 0;
    double x1 = 0, x2 = 0;
    char ch;
    while (i < str.size())
    {
        if (isdigit(str[i]))
        {
            s = "";
            ss.clear();
            while ((isdigit(str[i])||(str[i]=='.')) && i < str.size())
            {
                s += str[i];
                ++i;
            }
            ss << s;
            ss >> x;
            s1.Stack_Push(x);//操作数进栈
        }
        else if ((str[i] == '-') &&(i == 0 ||( i>0 && str[i - 1] == '('))&&str[i]!='(')//操作数进栈
        {
            s = "";
            ss.clear();
            s += str[i];
            ++i;
            while ((isdigit(str[i])||(str[i]=='.')) && i < str.size())
            {
                s += str[i];
                ++i;
            }
            ss << s;
            ss >> x;
            s1.Stack_Push(x);
        }
        else//运算符进栈
        {
            char a = s2.Stack_GetTop(), b = str[i];//a是当前运算符,b是运算符栈顶元素
            switch (Compare(a, b))
            {
            case -1://栈顶元素比当前运算符低,入栈
                ++i;
                s2.Stack_Push(b);
                break;
            case 0://相等,出栈
                ++i;
                s2.Stack_Pop();
                break;
            case 1://栈顶元素比当前运算符高,进行计算
            //这时不能++i,要继续将当前运算符和栈顶运算符进行比较,很重要
                x2 = s1.Stack_GetTop();//第二个运算数
                s1.Stack_Pop();
                /*
                说明,我们有时间很难确定负号和减号,比如-(-(-(1))),如果我们把其看作负号的话,如果最里层括号是一个运算式而不是单纯一个数,那么在识别数字的时候是无法完成计算的,也就无法将-看作负号,所以我们统一将-后面是(的看作是减号,这产生的问题就是可能会出现只有一个操作数但是还剩下操作符的情况,比如-(-(-(1)))这种,最后就剩下三个减号和一个1,所以我们就将这时间的第一个操作符设为0从而完成运算.如下面代码所示.
                */
                if(!s1.Stack_Empty());//第一个运算数
                {
                    x1 = s1.Stack_GetTop();
                    s1.Stack_Pop();
                }
                else 
                    x1 = 0;
                ch = s2.Stack_GetTop();
                s2.Stack_Pop();
                switch (ch)
                {
                case '+':
                    x1 += x2;
                    break;
                case '-':
                    x1 -= x2;
                    break;
                case '*':
                    x1 *= x2;
                    break;
                case '/':
                    x1 /= x2;
                    break;
                default:
                    break;
                }
                s1.Stack_Push(x1);
                break;
            default:
                break;
            }
        }
    }
    double ans= s1.Stack_GetTop();//在前期保证表达式合法性的基础上,此时s1只有一个元素
    s1.Stack_Pop();
    return ans;
}

中缀转后缀

中缀表达式:a*b + c/d

前缀表达式:+*ab/cd

后缀表达式(逆波兰表达式):ab*cd/+(便于计算)

  • 遇见操作数,直接进栈
  • 遇见操作符,弹出操作数并将计算结果入栈
  • 无界限符

参考四则表达式,这里只简单介绍算法

从左往右遍历四则表达式

遇见数字 :直接输出

遇见 ( :压入栈

遇见 ) :持续出栈直到 ( ,如果出栈元素不是 ( 则输出,否则停止出栈.左括号和右括号均不输出

遇见符号:判断与栈顶符号的优先级,如果低于等于栈顶元素,则将持续将栈顶元素弹出并输出,直到不再低于等于栈顶元素.最后将当前符号入栈.

处理完字符串,将栈中剩余符号全部输出.

栈与递归的实现

一个函数调用另一个函数,运行前需要完成

  • 传递所有的实在参数和返回地址
  • 为被调用函数的局部变量分配存储区
  • 将控制转移到被调用函数的入口

被调用函数返回之前,需要完成

  • 保存函数计算结果
  • 释放被调函数数据区
  • 将控制返回到调用函数