目录
[3.1 入栈的代码实现](#3.1 入栈的代码实现)
[3.2 入栈的测试打印](#3.2 入栈的测试打印)
[4.1 出栈的代码实现](#4.1 出栈的代码实现)
[4.2 出栈的测试打印](#4.2 出栈的测试打印)
前言
在前面我们已经完成了对数据结构中顺序表和链表的学习,接下来我们就要学习新的数据结构-栈和队列。本篇文章主要是栈的介绍,像顺序表和链表,栈和队列也是线性表的其中一种。
一、栈的概念
栈 :一种特殊的线性表 ,其只允许在固定的一端进行插入和删除元素操作 。进行数据插入和删除操作的一端称为栈顶 ,另一端称为栈底 。栈中的数据元素遵守后进先出 LIFO(LastIn FirstOut)的原则。
压栈 :栈的插入操作叫做进栈/压栈/入栈 ,入数据在栈顶。
出栈 :栈的删除操作叫做出栈。出数据也在栈顶。
用图就可以让大家更好去理解栈的概念:

大家可以把栈理解成一个羽毛球筒中的羽毛球或者弹匣中的子弹这种东西,这些东西都是后放入的先拿出来,这样就可以更好理解栈了。
二、栈的结构
栈底层结构选型:栈的实现一般可以使用数组或者链表实现,相对而言数组的结构实现更优一些。虽然在前面学习顺序表我们就知道顺序表本质就是数组,而数组在扩容的时候会有消耗,并且可能会浪费空间,但是数组的缓存利用率很高,而且在尾上插入数据的代价比较小。
cpp
//Stack.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>
typedef int STDataType;
typedef struct Stack
{
STDataType* arr;
int top; //栈顶
int capacity;
}ST;
三、栈的相关方法实现
1、栈的初始化
cpp
//Stack.h
//栈的初始化
void STInit(ST* pst);
//Stack.c
//栈的初始化
void STInit(ST* pst)
{
assert(pst);
pst->arr = NULL;
pst->top = 0;//此时top指向的是栈顶数据的下一个位置
//pst->top = -1;//此时top指向的是栈顶数据
pst->capacity = 0;
}
在上面栈的结构中我们的栈顶用top定义,但是top到底指的是栈顶数据呢还是栈顶数据的下一个位置?这其实就取决于我们初始化栈时怎么对top进行初始化。用图就可以刚好解释了:

(1)当pst->top = 0 时,此时top 指向的是栈顶数据的下一个位置 ,并且一般而言都是这样初始化的,虽然这样初始化有违我们正常逻辑,因为栈顶我们一般都会认为就是栈当前最后一个数据,但这样初始化是有几个好处的:首先是计算栈当前数据个数 ,这样初始化top 就可以类似当成 size 作为数据个数来看了 ;其次是判断当前数组数据是否已满,如果是这种初始化则直接判断 top 是否和 capacity 相等即可。
2、栈的销毁
cpp
//Stack.h
//栈的销毁
void STDestroy(ST* pst);
//Stack.c
//栈的销毁
void STDestroy(ST* pst)
{
assert(pst);
free(pst->arr);
pst->arr = NULL;
pst->top = 0;
pst->capacity = 0;
}
3、栈的插入数据(入栈)
3.1 入栈的代码实现
cpp
//Stack.h
//判断数组否有空间或者空间够不够
void STCheckCapacity(ST* ps);
//栈的插入数据(入栈)
void STPush(ST* pst, STDataType x);
//Stack.c
//判断数组否有空间或者空间够不够
void STCheckCapacity(ST* ps)
{
if (ps->top == ps->capacity)//可能没有空间或者空间满了
{
int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity; //操作和顺序表完全一样
STDataType* tmp = (STDataType*)realloc(ps->arr, newcapacity * sizeof(STDataType));
assert(tmp); //判断是否扩容成功
ps->arr = tmp;
ps->capacity = newcapacity;
}
}
//栈的插入数据(入栈)
void STPush(ST* pst, STDataType x)
{
assert(pst);
//扩容
STCheckCapacity(pst);
//插入数据
pst->arr[pst->top] = x;
pst->top++;
}
由于我们的栈是用数组来实现的,所以栈的插入数据其实和顺序表的插入数据基本一致,所以上面代码如有不清楚的地方可以看我的数据结构之顺序表专题,里面的代码有详细的解释,在这里就不重复赘述了。
3.2 入栈的测试打印
cpp
//Stack.h
//打印栈
void STPrint(ST* pst);
//Stack.c
//打印栈
void STPrint(ST* pst)
{
assert(pst);
for (int i = 0; i < pst->top; i++)
{
printf("%d ", pst->arr[i]);
}
}
//Test.c
#include "Stack.h"
void Test1()
{
ST st1;
//初始化栈
STInit(&st1);
//入栈
STPush(&st1, 1);
STPush(&st1, 2);
STPush(&st1, 3);
//打印栈
STPrint(&st1);
}
int main()
{
Test1();
return 0;
}

4、栈的删除数据(出栈)
4.1 出栈的代码实现
cpp
//Stack.h
//栈的删除数据(出栈)
void STPop(ST* pst);
//Stack.c
//栈的删除数据(出栈)
void STPop(ST* pst)
{
assert(pst);
assert(pst->top); //栈里面不能没有数据
pst->top--;
}
4.2 出栈的测试打印
cpp
//Test.c
void Test1()
{
ST st1;
//初始化栈
STInit(&st1);
//入栈
STPush(&st1, 1);
STPush(&st1, 2);
STPush(&st1, 3);
//打印栈
STPrint(&st1);
printf("\n");
//出栈
STPop(&st1);
STPrint(&st1);
printf("\n");
STPop(&st1);
STPrint(&st1);
printf("\n");
STPop(&st1);
STPrint(&st1);
}
int main()
{
Test1();
return 0;
}

5、取栈顶数据
cpp
//Stack.h
//取栈顶数据
STDataType STTop(ST* pst);
//Stack.c
//取栈顶数据
STDataType STTop(ST* pst)
{
assert(pst);
assert(pst->top); //栈里面不能没有数据
return pst->arr[pst->top - 1];
}
6、判断栈是否为空
cpp
//Stack.h
//判断栈是否为空
bool STEmpty(ST* pst);
//Stack.c
//判断栈是否为空
bool STEmpty(ST* pst)
{
assert(pst);
return pst->top == 0; //等于0为空判断为真,即返回True;不等于0有数据判断为假,即返回False
}
7、获取栈中有效数据个数
cpp
//Stack.h
//获取栈中有效数据个数
int STSize(ST* pst);
//Stack.c
//获取栈中有效数据个数
int STSize(ST* pst)
{
assert(pst);
return pst->top;
//初始化top的值不同,这里返回的值也不同,如果初始化top = 0,则top就相当于size
}
四、栈的相关算法题


这道题乍一看感觉不知道用什么方法去解决,但通过一些示例和题目字符串的要求我们可以隐约得到一些逻辑:首先如何判断字符串是否有效第一个条件是左右括号数量是一致的;第二个条件就是每一块区域的括号是对称的,比如"({[]})[()]"类似这种。
那这样怎么去判断呢?我们想想我们最先判断的是不是最里面的一组括号也就是右括号紧挨着的左括号 对吧,如果判断成功的话是不是要把这一组括号扔掉,此时第二个右括号紧挨着的左括号是不是就变成了倒数第二个左括号,一一比较。
那跟栈有什么关系吗?我们想想:我们第一次比较时的左括号是不是最后一个左括号,但却是第一个拿出来比较的,这不就和栈的先进后出的特点相吻合吗?所以这就是一个突破口。
那用栈如何去具体解决这道题呢?**首先判断字符串中当前字符是不是左括号,如果是的话将其入栈;如果是右括号则对栈进行拿出数据,此时拿出的数据就是离其最近的左括号,判断是否相等后进行出栈将这个左括号移出栈内,循环比较即可。**具体代码如下:
cpp
bool isValid(char* s)
{
ST st1;
while(*s)
{
if(*s == '(' || *s == '[' || *s == '{')//左括号入栈
{
STPush(&st1, *s);
}
else //右括号则进行取出数据再出栈,此时拿出的括号就是离其最近的左括号
{
char ret = STTop(&st1);
if((ret == '(' && *s != ')')
|| (ret == '[' && *s != ']')
|| (ret == '{' && *s != '}'))
//判断一次相等不能说明是True,但有一次不相等则一定是False
{
STDestroy(&st1);//返回前要销毁栈释放空间
return false;
}
STPop(&st1);//如果相等就将这个左括号移除栈进行后面的匹配
}
s++;
}
STDestroy(&st1);
return true;
}
的确通过我们上述的逻辑写的这个代码对应返回是true即字符串有效时是完全没问题的,但对于某些字符串无效的示例这个代码是有缺陷的:

我们会发现错误的示例是只有一个左括号,这样为什么就会出现问题呢?我们带着示例回到我们所写代码中看看:首先第一个字符是左括号则将其入栈,然后s++回到开头,此时由于只有一个左括号,s++得到的就是'\0',则while判断为假跳出循环,则返回就是true,但实际上是false。
这样我们怎么解决问题呢?归根结底就是因为左括号的数量多于右括号导致的 ,使得跳出循环后栈内仍然有数据,所以跳出循环后我们还要进行一次判断:
cpp
bool isValid(char* s)
{
ST st1;
while(*s)
{
if(*s == '(' || *s == '[' || *s == '{')//左括号入栈
{
STPush(&st1, *s);
}
else //右括号则进行取出数据再出栈,此时拿出的括号就是离其最近的左括号
{
char ret = STTop(&st1);
if((ret == '(' && *s != ')')
|| (ret == '[' && *s != ']')
|| (ret == '{' && *s != '}'))
//判断一次相等不能说明是True,但有一次不相等则一定是False
{
STDestroy(&st1);//返回前要销毁栈释放空间
return false;
}
STPop(&st1);//如果相等就将这个左括号移除栈进行后面的匹配
}
s++;
}
if(STEmpty(&st1))//判空为真则说明左右括号数量一致
{
STDestroy(&st1);
return true;
}
STDestroy(&st1);
return false;//左括号数量多于右括号数量的情况,字符串无效
}
那这样代码就完成了吗?我们再提交一次看看:

我们会发现这一次竟然变成了报错,还不是上面的解答错误,而报错的示例是只有一个右括号,也就是说这样的示例我们所写的代码是不能运行的。那原因是什么呢?我们还是带着示例回到代码里面看看:
首先由于没有左括号,第一次进入循环则直接到else里面,此时第一条代码是char ret = STTop(&st1); ,而这个函数我们前面讲了就是取出栈的数据,在这个函数中有一条代码 assert(pst->top)指的就是如果当前栈如果没有数据就不能取出数据,会assert断言失败而报错 ,这就是代码报错的原因所在。
那我们怎么解决这个问题呢?在else里面出现的问题我们就在else里面进行修改:归根结底的问题就在于左括号数量少于右括号数量 ,导致一定会出现某一次进入else时栈没有数据的情况,这样拿取栈数据就会报错。所以只需要每次进入else时多判断一次栈是否为空即可:
cpp
bool isValid(char* s)
{
ST st1;
STInit(&st1);
while(*s)
{
if(*s == '(' || *s == '[' || *s == '{')//左括号入栈
{
STPush(&st1, *s);
}
else //右括号则进行取出数据再出栈,此时拿出的括号就是离其最近的左括号
{
if(STEmpty(&st1))//判空为真说明此时栈没有左括号,但进入了循环说明仍有右括号
//说明是右括号数量多于左括号的情况,字符串无效
{
STDestroy(&st1);
return false;
}
char ret = STTop(&st1);
if((ret == '(' && *s != ')')
|| (ret == '[' && *s != ']')
|| (ret == '{' && *s != '}'))
//判断一次相等不能说明是True,但有一次不相等则一定是False
{
STDestroy(&st1);//返回前要销毁栈释放空间
return false;
}
STPop(&st1);//如果相等就将这个左括号移除栈进行后面的匹配
}
s++;
}
if(STEmpty(&st1))//判空为真则说明左右括号数量一致
{
STDestroy(&st1);
return true;
}
STDestroy(&st1);
return false;//左括号数量多于右括号数量的情况,字符串无效
}
到此这个代码就完全没问题了,这样我们就解决了这道题。
结束语
到此栈这个数据结构就讲解完了,栈相较于前面的顺序表链表是比较简单的,逻辑上也非常简洁明了。下一篇我们就会讲解队列这个数据结构了,希望本篇文章对大家学习栈有所帮助!