目录
什么是栈?
栈是一种数据结构,它的特点是先进后出(First In Last Out)。数据结构的作用简单来讲就是组织数据的方式,这里所谓的先进后出,说的就是数据在栈中进出的特点。
玩栈,你首先的明白几个概念:
栈顶:栈顶是栈唯的一个“开口”,入栈、出栈操作只能在栈顶进行。
入栈:把元素从栈顶放到栈里面。
出栈:把栈顶的元素从栈里拿出。
配个图,更好理解一点:
栈的实现方式
栈的实现方式有两种:
数组实现
数组实现的方法,就是用一个数组来模拟栈,我们需要一个top来标记栈顶元素的位置(或者栈顶元素的下一个位置),通过这样的方式来实现栈的功能。
链表实现
链表实现(以单链表为例)需要注意,你可以把链表的头部作为栈顶,因为单链表的头插是方便的,尾插需要找尾的前一个,需要遍历是不方便的。
当然你可以可以采取增加一个变量的方法,用一个ptail指针指向尾部,把尾作为栈顶,也是完全可以的,如下图所示。
总之,两种实现方法都是可以的,但实现过程需要注意栈各项功能的要求。
栈的实现
我选择使用数组的方式实现栈。
栈对应的接口如下(C语言实现)
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
//对int 类型的重命名,方面以后修改栈存的元素
typedef int STDataType;
//用一个结构体把栈的相关参数放在一起
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
} ST;
void STInit(ST *pst);//初始化
void STPush(ST* pst, STDataType x);//入栈
void STPop(ST* pst);//出栈
STDataType STTop(ST* pst);//获取栈顶元素
bool STEmpty(ST* pst);//判断栈是否为空
int STSize(ST* pst);//获取栈的元素个数
void STDestory(ST *pst);//销毁栈
我们的实现方法是,创建一个结构体,就相当于创建了一个栈,我们通过修改结构体里面的元素,来模拟栈的功能。
那么接下来,我们对接口,进行分析。
初始化
void STInit(ST* pst) {
pst->a = NULL;
pst->top = 0;//栈顶元素的下一个
pst->capacity = 0;
}
初始化很简单,把我们栈的相关参数给一个初始值。
这里重点说一下,top为什么给0。
前面提到了,top可以标识栈顶元素的下标,也可以标识栈顶元素下一个位置的下标,那么这两者的区别就是字面意思,那么为什么这里,栈里面还没有元素,要给零呢?
其实可以这样考虑,栈为空是一个特殊的状态,不管你选择那个标识方法,栈为空的这个特殊状态你都得唯一标识。
假定,我们top标识栈顶元素的下一个位置,那么给零,如果栈里面进了一个元素,top++变成了1,是不是刚好就是栈顶元素的下一个位置。
假定,我们top表示的是栈顶元素,那么你说为空时,给0合适吗?明显不合适,得给-1才行,这样你插入元素后top++,top指向了栈顶元素。
所以,要注意这个细节,这里我选择的是标识栈顶元素的下一个位置。
capacity显示的是数组的容量。
判空
判空就是看看栈内有没有元素,返回一个bool值。
bool STEmpty(ST* pst) {
assert(pst);
return pst->top == 0;
}
前面提到了,我们top标记的时栈顶的下一个位置的元素,栈为空即top为0,那么这里返回
pst->top == 0这个表达式的结果。
如果top为零,那么表达式结果为true,栈为空。
如果top不为零,表达式结果为false,栈不为空。
(这里用的assert就是用来检查参数的值是否不满足要求(一般判断不为空)。
比如这里pst是不能为空的,因为栈得存在,如果pst是NULL或false(在C语言里面NULL、false和0是等价的)。
如果为空,程序会直接死掉,并且报出这一行的错误,下面用到的这个断言都是这个功能。)
如下图所示:
大小
计算栈内有多少个元素。
int STSize(ST* pst) {
assert(pst);
return pst->top;
}
我们的栈是用数组来实现的,top指向的是栈顶元素的下一个元素。
比如栈内有五个元素,下标从0到4,top的值是5,刚好是栈内元素的个数,栈为空也是没问题的,返回的就是0。
入栈
入栈操作就是向栈内从栈顶,入一个元素。
void STPush(ST* pst, STDataType x) {
//判断是否需要增容
if (pst->top == pst->capacity) {
int newCapacity = pst->capacity == 0 ? 4 : pst->capacity * 2;
STDataType* tmp = (STDataType*)realloc(pst->a, newCapacity * sizeof(STDataType));
if (tmp == NULL) {
perror("realloc fail");
return;
}
pst->a = tmp;
pst->capacity = newCapacity;
}
//添加数据
pst->a[pst->top] = x;
pst->top++;
}
由于我们的栈是用数组来模拟的,所以在插入数据的时候需要判断是否需要增容。
这里capacity这个参数就需要用上了,那么什么情况需要增容呢?需要注意什么呢?
假设我们的capacity是6,也就是栈内目前最多放6个元素,假定我们的栈满了,那么最后一个元素的下标是不是应该是5,那么此时top的值是不是应该是6,在数值上是和capacity相等。
如图所示:
这个状态就是栈满的状态,还需要插入数据,就需要增容了。
对于数组数组,增容就用realloc,一般扩容为原来的两倍。注意,这里有个魔鬼细节,经过初始化后,我们的容量时0,这时候你应该开辟一段空间,此时realloc功能就和malloc一样了。
所以出现了这句代码:
int newCapacity = pst->capacity == 0 ? 4 : pst->capacity * 2;
判断capacity是否为零,为零就给赋值为4,下面就会栈开辟四个元素的空间,不为零,就变为原来的二倍,下面正常扩容。
之后就正常的把数据放入栈中,然后不要忘了让top++。
出栈
出栈操作更简单,操纵top即可。
void STPop(ST* pst) {
//为空不能删
assert(pst);
assert(!STEmpty(pst));
pst->top--;
}
出栈的前提是栈得有元素,所以咱先判断是否为空,这里如果为空,STEmpty返回值是 true,用 !取反后,值就是flase,就会报错啦。
然后,如果不为空,这里删除,我们就让top减减。
注意这里的删除,并不是真真正正的把数据从数组中移除了,只是在逻辑上,我们把top--,这个元素就不属于我们的栈了。
但这个数据在内存中还是占着内存的,这块内存还是属于我们数组的,下一次插入数据,可以直接覆盖掉。
如图所示:
获取栈顶元素
只返回栈顶元素,不修改站内的元素。
STDataType STTop(ST* pst) {
assert(pst);
assert(!STEmpty(pst));
return pst->a[pst->top - 1];
}
同样的,栈要存在且不为空才能得到栈顶元素。
top记录的是栈顶元素下一个位置的下标,那么top-1就是栈顶元素的下标,直接通过这个下标,访问数组的元素,返回即可。
销毁栈
释放内存,变量置空。
void STDestory(ST* pst) {
assert(pst);
free(pst->a);
pst->a = NULL;
pst->capacity = pst->top = 0;
}
这个就很简单了,malloc开辟,free释放,指针置空,变量设为零。
测试
接下来,通过简单的代码来测试一下我们的栈,是否能正常使用。
void test01()
{
//创建栈,即创建了一个结构体
ST st;
//初始化
STInit(&st);
//入栈 10 20 30 40
STPush(&st, 10);
STPush(&st, 20);
STPush(&st, 30);
STPush(&st, 40);
//获得栈元素个数
int sz = STSize(&st);
printf("栈内元素个数为:>%d\n", sz);//4
//获得栈顶元素
int tmp = STTop(&st);
printf("栈顶元素为:>%d\n", tmp);//40
//元素以此出栈,顺序应为40 30 20 10 满足先进后出
printf("出栈:>");
while (!STEmpty(&st))
{
tmp = STTop(&st);
printf("%d ", tmp);
STPop(&st);
}
//销毁栈
STDestory(&st);
}
int main()
{
test01();
return 0;
}
结果如下:
从测试结果看,我们栈的实现是成功的!!!
总结
栈的实现要注意以下几点:
- 栈的的特点是先进后出
- 栈的实现方式可以用数组、链表
- 栈的功能有push、pop、top、size、empty、destory
你以为到这里就结束了吗?开玩笑,看我标题,来用我们写的栈做一道题吧!!
OJ有效——括号匹配问题
题目链接在这里:
20. Valid Parentheses - 力扣(Leetcode)
题目描述:
题目概述:
就是有一个字符串,只有'( )[ ] { }'这些括号组成,让我们判断它给的字符串中括号是否是匹配的。
所谓匹配,就是相同的括号,一左一右才叫匹配,题目给的用例很简单,我给大家写一个,你看看匹不匹配。
"( [ { } ] )" (中间的空格是为了隔开字符),大家认为这个字符串中,括号是匹配的吗?
答案是:匹配的。
字符串中,左括号和有括号尽管不是相邻两个,但在逻辑上,它是匹配的,
你可以这样考虑,从中间看,两个花括号匹配,然后移除掉,剩下"( [ ] )" ,然后中间的方括号也是匹配的,移除掉,同理圆括号也是匹配的。
基于这些理解,我们给出一下思路:
我们采用栈,把字符中的字符入到栈内。
如果遇到左括号,入栈。
如果遇到右括号,获得出栈顶的元素和这个右括号进行匹配判别。
匹配情况
- 单对匹配的情况好说,继续进行下一个字符的判别即可。
- 那不匹配的情况也有一下几种:
- 遇到右括号,和栈顶元素不匹配,直接返回false。
- 字符串走到空,站内还有元素(肯定是左括号,因为只有左括号入栈),直接返回false
那么返回true的情况就很清楚了,当字符串走到空,并且栈内是没有元素,就要返回true.
栈的拷贝
把我们写好的栈拷贝过来,拷贝到OJ题目里面。
//接口型的OJ不需要包含头文件,删掉它
//#pragma once
//#include<stdio.h>
//#include<stdlib.h>
//#include<stdbool.h>
//#include<assert.h>
//typedef int STDataType;
//因为站内要放字符,所以这里改为char
typedef char STDataType;
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
} ST;
//其实这里函数声明也可以不写,写了也不错。
void STInit(ST *pst);
void STPush(ST* pst, STDataType x);
void STPop(ST* pst);
STDataType STTop(ST* pst);
bool STEmpty(ST* pst);
int STSize(ST* pst);
void STDestory(ST *pst);
//头文件删掉
//#define _CRT_SECURE_NO_WARNINGS 1
//#include"stack.h"
void STInit(ST* pst) {
pst->a = NULL;
pst->top = 0;//栈顶元素的下一个
pst->capacity = 0;
}
void STPush(ST* pst, STDataType x) {
//判断是否需要增容
if (pst->top == pst->capacity) {
int newCapacity = pst->capacity == 0 ? 4 : pst->capacity * 2;
STDataType* tmp = (STDataType*)realloc(pst->a, newCapacity * sizeof(STDataType));
if (tmp == NULL) {
perror("realloc fail");
return;
}
pst->a = tmp;
pst->capacity = newCapacity;
}
pst->a[pst->top] = x;
pst->top++;
}
void STPop(ST* pst) {
//为空不能删
assert(pst);
assert(!STEmpty(pst));
pst->top--;
}
STDataType STTop(ST* pst) {
assert(pst);
assert(!STEmpty(pst));
return pst->a[pst->top - 1];
}
bool STEmpty(ST* pst) {
assert(pst);
return pst->top == 0;
}
int STSize(ST* pst) {
assert(pst);
return pst->top;
}
void STDestory(ST* pst) {
assert(pst);
free(pst->a);
pst->a = NULL;
pst->capacity = pst->top = 0;
}
代码实现
bool isValid(char * s){
ST st;//创建一个栈
STInit(&st);//初始化栈
while(*s!='\0'){//判别过程
char tmp= *s;//取出一个字符进行判断
if(tmp=='('||tmp=='['||tmp=='{'){//左括号入栈
STPush(&st,tmp);
s++;
}
else{//右括号,出栈顶元素,比较(前提是栈内有数据,所以这里要判空)
//栈为空,肯定不匹配,返回false
if(STEmpty(&st)){
STDestory(&st);//返回前最好销毁一下栈,下面的步骤也是
return false;
}
else{//栈不为空,取出栈顶元素,匹配
STDataType top= STTop(&st);
if(top=='('&&tmp==')'||
top=='['&&tmp==']'||
top=='{'&&tmp=='}')
{
STPop(&st);//匹配了,就出栈顶元素,并让s++去找下一个字符
s++;
}
else{//不匹配,直接返回false
STDestory(&st);
return false;
}
}
}
}
bool ret=STEmpty(&st);//true 说明栈为空同时也说明括号是匹配的
//false 说明栈不为空同时也说明括号是不匹配的
STDestory(&st);
//栈不为空 不匹配 栈为空 匹配
return ret;
}
代码分析,
首先创建栈并初始化。
接下来从字符串中获取一个字符。
若是左括号,入栈。
若是右括号,说明要匹配。
但首先要判断栈是否为空,若为空说明不匹配(栈内没元素和它匹配)。
若不为空,取出栈顶元素,判断是否匹配,如果匹配继续下一个匹配,如果不匹配返回false。
最后,如果while退出了,我们要看看栈是否为空,若为空这返回true,若不为空则返回false。
代码的整体逻辑就是这样,来看看结果。
我们的代码是可以通过的,说明栈的实现和解题的方式都是没有问题的!!!
以上,就是我对栈实现的理解和分析,如果文章对您有所帮助,不放给博主点点关注,点点赞,
后续会继续更新博客,下一次就是队列的实现和相关习题!!
!!我们下一期再见!!
如果需要源代码的可以到博主的仓库自取哦~~
test_5_19 · 琦琦爱敲代码/test_c - 码云 - 开源中国 (gitee.com)