文章目录
前言
数据结构有三要素——逻辑结构、数据的运算、存储结构(物理结构)
每种数据结构我们都将讨论它的定义(逻辑结构)和基本操作(数据的运算)
其中存储结构不同,运算的实现方式也有不同
一、定义
栈(Stack)是一种只允许在一端进行操作的线性表,不仅如此,栈还是一种后进先出(last in first off,LIFO)的数据结构。
栈可以顺序存储也可以链式存储,顺序栈和顺序表的定义几乎没有区别,只是在处理数据时加了一条后进先出的限制,链栈也是同样的道理。
二、顺序栈的基本操作
1.定义
#define MaxSize 10 //定义栈中元素的最大个数
typedef struct
{
ElemType data[MaxSize]; //静态数组存放栈中元素
int top; //栈顶指针,指向栈顶元素
}SqStack;
注意:顺序栈的缺点是栈的大小不可以改变,我们为了保证有足够大的空间去存储数据,通常会开辟一段比较大的连续空间去存储顺序栈,但这又导致了空间的浪费,这时候我们可以通过共享栈(两个栈共享一片空间)来最大化利用存储空间。要想使用共享栈,就需要两个栈顶指针,一个初始化在空间的最大位置,并且从上往下进行入栈,另一个初始化在空间的最小位置,从上往下进行入栈。
#define MaxSize 10 //定义栈中元素的最大个数
/*定义*/
typedef struct
{
ElemType data[MaxSize]; //静态数组存放栈中元素
int top0; //0号栈的栈顶指针
int top1; //1号栈的栈顶指针
}ShStack;
/*初始化*/
bool InitStack(ShStack& S)
{
S.top0 = -1;
S.top1 = MaxSize;
}
/*栈满的判断*/
bool IsFull(ShStack S)
{
if (S.top0 + 1= S.top1)
return true;
else
return false;
}
2.初始化
bool InitStack(SqStack& S)
{
S.top = -1; //初始化栈顶指针即可
}
注意:把栈顶指针设为0也是一种常用的初始化方式,本文中涉及的操作栈顶指针都设为-1
3.进栈
bool Push(SqStack& S, ElemType x)
{
if (S.top == MaxSize - 1) //栈满,报错
return false;
S.top++; //栈顶指针加一
S.data[S.top] = x; //新元素入栈
return true;
}
4.出栈
bool Pop(SqStack& S, ElemType& x)
{
if (S.top == -1) //栈空,报错
return false;
x = S.data[S.top]; //元素出栈
S.top--; //栈顶指针减一
return true;
}
注意:如果不用delete的话,栈顶元素还是存在物理内存中,只是在逻辑上被删除了
5.读出栈顶元素
bool GetTop(SqStack S, ElemType& x)
{
if (S.top == -1) //栈空,报错
return false;
x = S.data[S.top]; //读出元素
return true;
}
6.判空
bool StackEmpty(SqStack S)
{
if (S.top == -1) //栈空
return true;
else
return false;
}
三、链式栈的基本操作
1.定义
typedef strunct LinkNode
{
ElemType data;
struct LinkNode* next;
}LinkNode, *LinkStack;
注意:因为在定义上,链栈和单链表没有什么区别,所以链栈的实现也有带头结点和不带头结点两种方式,但是在实现链栈时推荐不带头结点
2.初始化
bool InitStack(LinkStack S)
{//类似于不带头结点的单链表的初始化
S == NULL;
return true;
}
3.进栈
bool Push(LinkStack S, ElemType x)
{
LinkNode* p = new LinkNode; //创建一个新的结点
p->data = x;
p->next = S; //新结点的指针域指向原来的首元结点
S = p; //新结点成为了新的首元结点
return true;
}
注意:因为链栈的进栈操作必须后进先出,所以对应着单链表中头插法的操作
4.出栈
bool Pop(LinkStack S, ElemType &x)
{
if (S == NULL) //此时栈中没有元素,无法出栈
return false;
LinkNode* p = S; //临时指针
x = p->data; //元素出栈,由x带回
S = p->next; //此时的首元结点成为原结点的下一个
delete p; //释放出栈结点的内存
p = NULL;
return true;
}
5.读出栈顶元素
bool GetTop(LinkStack S, ElemType& x)
{
if (S == NULL) //栈空,报错
return false;
x = S->data; //读出元素
return true;
}
6.判空
bool Empty(LinkStack S)
{
if(S == NULL)
return true;
else
return false;
}
7.销毁
bool DestroyLinkStack(LinkStack S)
{
LinkNode* p = S; //临时指针
while (p->next != NULL)
{
LinkNode* q = p;
p = q->next;
delete q;
}
S = NULL;
return true;
}
注意:顺序栈的销毁前面没有给出,因为顺序栈是通过在函数体内声明来分配空间的(如,SqStack S),这个空间是分配在栈区,函数体运行完毕后会由系统自动释放内存,理论上是不需要我们手动销毁的。而链栈的内存是我们手动开辟new出来的,这块内存开辟在堆区,函数体结束后系统不会自动释放,所以要由我们手动释放。
四、栈的应用
1.括号匹配
问题描述
比如有这样一组括号 {( ) [ ( { } ) ] }
按照我们之前所学习的逻辑,最后出现的左括号应该最先被匹配,这正与栈的特性有异曲同工之妙。
解决方法
遇到左括号就入栈,遇到右括号就拉上一个左括号一起“出栈”。这个过程就类似消消乐。但要注意,如果括号匹配后发现左右括号不是对应的,那么将直接报错不再继续检测下面的括号。
如果以代码的方式实现就是依次扫描所有字符,遇到左括号就入栈,遇到右括号就弹出栈顶元素并检查是否匹配。
匹配失败的情况:左括号单身,右括号单身,左右括号不匹配。
图片源自知乎用户AProgrammer
实现代码
bool bracketCheck(char str[], int length)
{
SqStack S; //申明一个栈
InitStack(S); //初始化这个栈
for (int i = 0; i < length; i++) {
if (str[i] == '(' || str[i] == '[' || str[i] == '{') { //扫描到左括号
Push(S, str[i]); //入栈
}
else { //扫描到右括号
if (StackEmpty(S)) //栈空,没有与之匹配的左括号
return false; //匹配失败
char topElem; //用来存放弹出的栈顶元素
Pop(S, topElem); //弹出栈顶元素
/*匹配的括号类型不一致,报错*/
if (str[i] == ')' && topElem != '(')
return false;
if (str[i] == ']' && topElem != '[')
return false;
if (str[i] == '}' && topElem != '{')
return false;
}
}
return StackEmpty(S); //检索完全部括号之后,如果栈空则匹配成功
}
2.递归
在函数调用的时候,总是最后被调用的函数最先执行结束,这又与栈的特性有着异曲同工之妙,那么函数的调用应该可以使用栈来实现。实际也是如此,在系统开始一段函数时,就要开辟一个函数调用栈。
递归调用时,函数调用栈可称为“递归工作栈”,每进入一层递归,就将递归调用所需信息压入栈顶,每退出一层递归,就从栈顶弹出相应信息。但是,如果递归层数过多,可能会导致栈溢出。