栈是限定仅仅在表尾进行插入和删除操作的线性表。
在软件中,栈的这种后进先出数据结构的应用是非常普遍的。比如我们在使用浏览器时,浏览器都有一个“后退键”,你点击后可以按照访问顺序的逆序加载浏览过的网页。很多类似的软件,比如Word、Photoshop等文档或图像编辑软件中,都有撤销的操作,也是用栈这种方式来实现的。
我们把允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom),不含任何元素的栈称为空栈,栈又被称为后进先出(Last In First Out)的线性表,简称LIFO结构。
如下图:
网站的返回键:
word里面的撤销键:
还有很多涉及到栈的软件,这里就不一一说了,下面我们来看压栈和出栈
压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据也在栈顶。
动图演示进栈:
有个问题问问大家?最先进栈的元素,是不是就只能是最后出栈呢?
答案是不一定,栈对线性表的插入和删除的位置进行了限制,并没有对插入和删除的时间进行限制,所以,在不是所有元素都进栈的情况下,事先进去的元素也可以出栈,只要保证是栈顶元素出栈就可以。
我们举个例子:
现在有3个数字元素1、2、3一次进栈,会有那些的出栈次序呢?
第一种:1、2、3进,再3、2、1出,出栈次序为321
第二种:1进,1出,2进,2出,3进,3出,出栈次序为123
第三种:1进,2进,2出,1出,3进,3出,出栈次序为213
第四种:1进,2进,2出,3进,3出,1出,出战次序为231
第五种:1进,1出,2进,3进,3出,2出,出栈次序为132
有没有可能是312这种出栈顺序呢?答案是不可能的,要让3先出栈,那么123必须全部进栈,那么出栈顺序必须是321。
我们想一下栈的实现我们是使用顺序表实现呢还是链表实现呢?其实相对而言顺序表结构的实现会更优一些,因为数组在尾上插入和删除数据的代价比较小。
进栈示意图:
出栈示意图:
注意:
栈的链式结构不能让尾做栈顶,因为栈的操作都在栈顶,在栈顶进行进栈和出栈时,如果让尾做栈顶,那就相当于链表的尾插尾删,而链表的尾插尾删是需要遍历找尾和尾的前一个元素的,而栈这种结构我们是无法进行遍历的,所以我们需要让链表的头做栈顶。
因为栈的顺序表结构的实现会更优一些,因为数组在尾上插入和删除数据的代价比较小,所以我们实现顺序表结构的栈。
我们首先创建三个文件:
Stack.h对相关头文件的包含,以及实现栈的结构和函数的声明
Stack.c对实现栈的操作的函数进行定义
test.c文件进行栈相关函数和栈功能的测试
话不多说,我们开始实现栈,首先实现栈的进栈和出栈操作,我们需要先定义栈这样的一个结构体:
typedef int STDataType;
typedef struct Stack
{
STDataType* a;//指向动态开辟的内存的指针
int top;
int capacity;
}Stack;
我们实现顺序表结构的栈,和博主前面讲的顺序表的实现一样,我们使用动态顺序表,这样大小灵活可变,我们这里定义一个指向动态开辟的内存的指针a,我们定义一个top来指示栈顶元素的位置,为了满足动态顺序表我们还需定义一个表示容量的变量。
下面我们来完成栈的这些操作:
// 初始化栈
void StackInit(Stack* ps);
// 入栈
void StackPush(Stack* ps, STDataType data);
// 出栈
void StackPop(Stack* ps);
// 获取栈顶元素
STDataType StackTop(Stack* ps);
// 获取栈中有效元素个数
int StackSize(Stack* ps);
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0
int StackEmpty(Stack* ps);
// 销毁栈
void StackDestroy(Stack* ps);
// 初始化栈
void StackInit(Stack* ps)
{
assert(ps);
ps->a = NULL;
ps->capacity = ps->top = 0;
}
我们经过调试发现已经将栈已经初始化
// 入栈
void StackPush(Stack* ps, STDataType data)
{
assert(ps);
//检测容量
if (ps->capacity == ps->top)
{
//增容
int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
STDataType* temp = (STDataType*)realloc(ps->a, newcapacity * sizeof(STDataType));
if (temp == NULL)
{
perror("realloc");
return;
}
ps->a = temp;
ps->capacity = newcapacity;
}
//操作入栈
ps->a[ps->top] = data;
ps->top++;
}
在入栈元素前,我们需要先考虑增容,仔细想想,当前top值等于我们的当前容量时我们要增容,我们在增容时,如果capacity是0时我们先开辟存储4个数据的内存,不是0时,我们就进行二倍的增容,如果开辟失败了,我们加一个判断pal->a是不是NULL,是NULL则打印错误返回,最后将capacity更新,入栈的话就简单了,因为top就是我们栈里面最后一个元素的下一个元素的下标,所以我们直接赋值即可,最后将top++。
下面我们调试一下入栈操作的代码:
发现没有问题,下面我们继续看接下来的操作:
// 出栈
void StackPop(Stack* ps)
{
assert(ps);
assert(!StackEmpty(ps));//栈不能为空
ps->top--;
}
首先出栈,我们得栈不能为空,所以我们这里断言一下,这个判空得函数会在下面会写。出栈我们直接top–就可以了
我们进行调试代码:
发现此时top已经改变。
// 获取栈顶元素
STDataType StackTop(Stack* ps)
{
assert(ps);
return ps->a[ps->top - 1];
}
我们的top是栈中最后一个元素的下一个位置的下标,所以top-1就是最后一个元素的下标,我们将它返回即可
// 获取栈中有效元素个数
int StackSize(Stack* ps)
{
assert(ps);
return ps->top;
}
top就是我们的元素个数。返回即可
// 检测栈是否为空,如果为空返回true结果,如果不为空返回false
bool StackEmpty(Stack* ps)
{
return ps->top == 0;
}
判断栈是否为空,当ps->top==0时,我们的栈是空的,这句话是真,则返回真,ps->top!=0时,这句话为假,则返回假。
// 销毁栈
void StackDestroy(Stack* ps)
{
assert(ps);
if (ps->a)
{
free(ps->a);
ps->a = NULL;
}
ps->capacity = 0;
ps->top = 0;
}
销毁栈,如果指向存储栈中元素空间的指针不为NULL的话,我们free掉这个指针,并将他置空,防止出现野指针,最好将top以及capacity都清0。
栈的元素的打印我们是没办法进行的,因为栈只能在栈顶操作,我们不能遍历它,我们可以这样打印,但是打印出来的元素只能是逆序的。
while (!StackEmpty(&s))
{
printf("%d ", StackTop(&s));
StackPop(&s);
}
我们每次打印栈顶元素,打印为将它pop,可以打印出逆序的,但是此时栈已经空了
我们接下来来看一个栈的面试题:
点击括号匹配问题跳转到做题网站
问题描述:
给定一个只包括 ‘(’,’)’,’{’,’}’,’[’,’]’ 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
1、左括号必须用相同类型的右括号闭合。
2、左括号必须以正确的顺序闭合。
示例1:
输入:s = "()"输出:true
示例 2:
输入:s = "()[]{}"输出:true
示例 3:
输入:s = "(]"输出:false
示例 4:
输入:s = "([)]" 输出:false
示例 5:
输入:s = "{[]}" 输出:true
思路:
该题需要用到栈的功能,遍历字符串,遇到左括号就入栈,直到遇到右括号,然后取栈顶元素跟它比较是否匹配,不匹配返回false,匹配则继续
我们把我们上面实现的栈的操作函数拷贝进去:
typedef char STDataType;
typedef struct StackNode
{
STDataType *a;//存储动态开辟内存的指针
int top;
int capacity;
}Stack;
// 初始化栈
void StackInit(Stack* ps);
// 入栈
void StackPush(Stack* ps, STDataType data);
// 出栈
void StackPop(Stack* ps);
// 获取栈顶元素
STDataType StackTop(Stack* ps);
// 获取栈中有效元素个数
int StackSize(Stack* ps);
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0
bool StackEmpty(Stack* ps);
// 销毁栈
void StackDestroy(Stack* ps);
// 初始化栈
void StackInit(Stack* ps)
{
assert(ps);
ps->a = NULL;
ps->capacity = ps->top = 0;
}
// 入栈
void StackPush(Stack* ps, STDataType data)
{
assert(ps);
//检测容量
if (ps->capacity == ps->top)
{
//增容
int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
STDataType* temp = (STDataType*)realloc(ps->a, newcapacity * sizeof(STDataType));
if (temp == NULL)
{
perror("realloc");
return;
}
ps->a = temp;
ps->capacity = newcapacity;
}
//操作入栈
ps->a[ps->top] = data;
ps->top++;
}
// 出栈
void StackPop(Stack* ps)
{
assert(ps);
assert(!StackEmpty(ps));//栈不能为空
ps->top--;
}
// 获取栈顶元素
STDataType StackTop(Stack* ps)
{
assert(ps);
return ps->a[ps->top - 1];
}
// 获取栈中有效元素个数
int StackSize(Stack* ps)
{
assert(ps);
return ps->top;
}
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0
bool StackEmpty(Stack* ps)
{
return ps->top == 0;
}
// 销毁栈
void StackDestroy(Stack* ps)
{
assert(ps);
if (ps->a)
{
free(ps->a);
ps->a = NULL;
}
ps->capacity = 0;
ps->top = 0;
}
bool isValid(char * s)
{
Stack st;
StackInit(&st);
while(*s)
{
if(*s=='('||*s=='['||*s=='{')
{
StackPush(&st,*s);
s++;
}
else
{
char ch=StackTop(&st);
StackPop(&st);
if((*s==')' && ch!='(')
||(*s==']' && ch!='[')
||(*s=='}' && ch!='{'))
{
//不匹配
return false;
}
else
{
//匹配
s++;
}
}
}
return true;
}
我们按逻辑写下来然后提交发现有错误,这里我们就要看它的错误案例:
它说只输入"["时输出的是true,但是正确答案是false,于是我们对照代码走一遍,发现我们将[入栈之后循环就结束了,返回了true,所以我们返回时不能这么写,我们应该写return StackEmpty(&st);上面循环走完,栈为空返回true,栈不为空返回false。
我们提交后还有一个错误:
我们对照代码走一遍,发现我们栈里面还没有元素时,就取栈顶元素了,于是我们在前面加个判断:
if(StackEmpty(&st))
{
return false;
}
当没有进栈元素时,直接返回false
我们改正后,发现可以通过了:
此时别高兴太早,我们的代码还有问题,有什么问题呢?
是不是我们动态开辟的内存还没有释放啊,对,此时造成了内存泄漏。
所以我们最后应该这样改:
bool isValid(char * s)
{
Stack st;
StackInit(&st);
bool match=true;
while(*s)
{
if(*s=='('||*s=='['||*s=='{')
{
StackPush(&st,*s);
s++;
}
else
{
if(StackEmpty(&st))
{
match=false;
break;
}
char ch=StackTop(&st);
StackPop(&st);
if((*s==')' && ch!='(')
||(*s==']' && ch!='[')
||(*s=='}' && ch!='{'))
{
//不匹配
match=false;
break;
}
else
{
//匹配
s++;
}
}
}
if(match==true)
{
match = StackEmpty(&st);
}
StackDestroy(&st);
return match;
}
我们设定了match来保存真假,初始为真,进去循环那里比较时如果栈为空将match改为false,并且在不匹配那里也要将match改为false,循环出来,判断match是不是为真,为真的话将判断栈是不是空赋给它,是空则match为true,不是空则match为false,最后统一销毁栈,最后返回match。
关于栈就讲解到这里,觉得可以的话点个赞再走吧,欢迎大家学习交流