要点:
-
先进后出,后进先出;
-
只能对栈顶元素操作;
一、顺序栈
通过老师讲解,我理解到,顺序栈好像是一个特殊的数组。
1、定义
顺序栈:重点是提前开辟好一块空间,往里面添加数据;所以它的长度是固定的,是定义好的。这里我们先预留出20个数据空间大小;为方便起见,在这里用define把数据个数定为MAX_LEN;后面如果想改变这个空间中数据个数,只需要把20改为需要的数据个数就可以了;
#define MAX_LEN 20
因为我们输入的数据不一定每次都是int类型,所以我们为了方便起见,对int也进行起别名;后续如果需要输入其他类型的数据,只需要把int改为需要添加的数据的类型即可;
typedef int Mytype;
定义顺序栈:
typedef struct SqStack
{
//存储栈中的数据元素
Mytype* data;
//整型指针(相当于数组的数组名)先定义了一个整形指针
int top; //保存栈中数据个数
}SQStack;
2、初始化
这里栈的初始化中,提前定义好栈的空间大小;等用maloc开辟好一块空间之后,用data这个指针去指向;top在这里像数组里面的下标一样,最开始的值是-1(看成在栈最底部),当输入一个值之后,这个值+1,变成0;
这个输入的值可以表示为data[0],当继续输入数值之后,top的值会持续加一;所以整个空间中输入的数据个数是top+1;
这里可以参考数组下标,数组中第一个元素的下标是0,第二个元素的下标是1,数组的长度总是数组最后一个元素的下标+1;
/*
函数名:lnitStack
参数列表:无
返回值:返回创建的栈的首地址(指针:类型是栈类型)
*/
SQStack* lnitStack()
{
SQStack* s = (SQStack*)malloc(sizeof(SQStack));
s->data = (Mytype*)malloc(sizeof(Mytype) * MAX_LEN);
s->top = -1;//一开始指在栈底
return s;
}
理解图
3、销毁一个栈
/*
函数名:DestroyStack
参数列表:传进来一个指向栈的指针
返回值:无
*/
void DestroyStack(SQStack* s)
{
if (s == NULL)
{
return;
}
if (s->data)
{
free(s->data);
}
s->top = -1;
free(s);
}
理解图
4、清空栈
/*
函数名:ClearStack
参数列表:传进来一个栈的地址(类型是栈的指针)
返回值:无
*/
void ClearStack(SQStack* s)
{
if (s)
{
s->top = -1;
}
}
理解图
5、判断栈是否为空栈
/*
函数名:IsEmpty
参数列表:传进来一个栈的地址
返回值:1 栈为空
0 栈不为空
*/
int IsEmpty(SQStack* s)
{
if (s == NULL || s->top == -1)
{
return 1;
}
return 0;
}
6、获取栈的长度
/*
函数名:StackLength
参数列表:传进来一个指向栈的地址
返回值:返回栈的长度
*/
int StackLength(SQStack* s)
{
if (IsEmpty(s))
{
return 0;
}
return s->top + 1;
}
理解图
7、入栈
/*
函数名:Push
参数列表:传进去栈的地址,以及要入栈的数值
返回值:判断是否入栈成功
成功返回1
失败返回0
*/
int Push(SQStack* s, Mytype d)
{
//考虑不能入栈的情况:栈不存在或栈为空或栈空间已满(顺序栈有空间限制)
if (s == NULL || s->data == NULL || s->top == (MAX_LEN - 1))
{
return 0;
}
//入栈
++s->top;
s->data[s->top] = d;
return 1;
}
理解图
8、出栈
/*
函数名:Pop
参数列表:@s:传进来一个要操作的栈的地址
@d:保存要出栈的元素
返回值:失败返回0;
成功出栈返回1
*/
int Pop(SQStack* s, Mytype* d)
{
//判断出栈失败的原因
if (s == NULL || s->data == NULL || s->top == -1)
{
return 0;
}
//成功出栈的操作
*d = s->data[s->top];
s->top--;
return 1;
}
理解图
9、获取栈顶元素的值
/*
函数名:GetTop
参数列表:@s:指针,要操作的栈的地址
@d:指针,保存栈顶的元素,因为函数的返回值不是栈顶的元素,需要用指针来实现跨作用域的访问
不需要出栈
返回值:
成功返回1
失败返回0
*/
int GetTop(SQStack* s, Mytype* d)
{
if (s == NULL || s->data == NULL || s->top == -1)
{
return 0;
}
*d = s->data[s->top];
return 1;
}
理解图
10、顺序栈帮助理解总图
整体代码记录如下
cs
#include <stdlib.h>
#include <stdio.h>
#include "Sqstack.h" //这个是我定义的头文件
int main()
{
SQStack* s = lnitStack();
printf("长度是 %d \n", StackLength(s));
printf("判断是否为空链表(1是空,0不是空):%d\n", IsEmpty(s)); //巧妙利用我们定义的各个函数
int a, b;
//入栈
Push(s, 1);
Push(s, 2);
printf("往开辟的空间输入了两个int数值,现在长度是:%d \n", StackLength(s));
Pop(s, &a);
printf("输出了一个数值 a = %d \n", a);
printf("空间中最上面的位置往下移动一格,位置移动过第二个位置,现在空间中int长度个数是:%d \n", StackLength(s));
Push(s, 3);
Push(s, 4);
printf("往开辟的空间输入了两个int数值:3和4,现在空间中int长度个数是:%d \n", StackLength(s));
GetTop(s, &b);
printf("现在空间中最外面的元素是:%d \n", b);
int i;
printf("现在这个空间中的元素都有:");
for (i = 0; i <= s->top; i++)
{
printf("%d ", s->data[i]);
}
printf("\n");
return 0;
}
运行结果观察
这是用VS调试出来的效果
二、链式栈
这里的链式栈和上面的顺序栈不同的一点是:链式,哈哈哈,不仅是名字的区别哦~
回想我们学过的链表,最大的一个特点是能够管理整个数据,而且链表中的数据能够通过前驱后继可以访问前后的值;
那我们的栈能不能也添加这样的功能呢?而且通过上面的顺序栈,我们发现每次必须得提前开辟好空间,有的时候我们不知道需要写入多少个数据,所以可能会造成空间大小不够或者空间的浪费?能不能通过链式栈解决我们的顾虑呢?
我们可以和链表一起去理解:链式栈可以看作是一个"带头结点的双向链表",但在这个链表中只能加入和删除结点,只能进行尾插法或尾删法;
1、定义
链式栈中数据结点的定义:
typedef struct node {
Mytype data;
struct node* next;
struct node* prev;
}Dnode;
链式栈中头结点的定义:
typedef struct lianStack {
struct node* top; //相当于双向链表中的last
struct node* bottom; //相当于双向链表中的first
int num; //保存栈中的元素个数
}Lianstack;
2、链式栈的初始化
/*
函数名:Initstack
参数列表:无
返回值:返回一个初始化完毕的栈的首地址
*/
Lianstack* Initstack()
{
//创建一个管理层头结点并初始化
Lianstack* l = (Lianstack*)malloc(sizeof(Lianstack));
l->top = NULL;
l->bottom = NULL;
l->num = 0;
}
图解
3、入栈
/*
函数名:Push
参数列表:传进去一个创建好的栈的首地址,以及要往栈中加入的数值
返回值:除非栈开辟的空间为空,否则只要里面有一个数值之后,栈的最底端的地址永远不变,我们进行的是尾插尾删法
所以我们这里不返回新加入数据之后的栈的地址
这里我们返回入栈的结果,成功返回1,失败返回0
*/
int Push(Lianstack* l, Mytype d)
{
//这里其实是开了一个头结点,跟头结点是同一种定义,在这里我们把它叫栈
//如果这个栈没有被开辟,这个栈不存在
if (l == NULL)
{
return 0;
}
//申请空间,创建一个数据结点;
Dnode* pnew = (Dnode*)malloc(sizeof(Dnode));
pnew->data = d;
pnew->next = NULL;
pnew->prev = NULL;
//创建栈:从无到有
if (l->top == NULL)
{
l->top = pnew;
l->bottom = pnew;
}
else//从少到多:尾插法
{
l->top->next = pnew;
pnew->prev = l->top;
l->top = pnew;
}
l->num++;
return 1;
}
图解
4、出栈
/*
函数名:Pop
参数列表:@l:要操作的栈的地址
@d:Mytype类型的指针,用来存放跨作用域保存栈顶的值
返回值:
成功返回1
失败返回0
*/
int Pop(Lianstack* l, Mytype* d)
{
//出栈失败:栈为空或栈不存在
if (l == NULL || l->num == 0)
{
return 0;
}
//注意顺序:1、取数据
//出栈:这里说明一下,用返回值的形式把这个值传入主函数的方法也是可取的,但是这样节省了内存
*d = l->top->data;
//2、删除这个结点
//这里需要注意:需要新定义一个数据结点p去辅助删除栈顶元素(同时释放这块空间),让它先在top所指的空间上,真正的top往前移动一个位置,因为链式栈是一个带头结点的双向的栈,所以真正要释放栈上最外面那块空间的话,除了p->prev这个链子要断开,新的栈顶位置指向前面的那条链子l->top->next也要断开;
//但是这里要注意的是:每次l->top都一定存在吗?假如删除到最后一个数据结点时,l->top往前指(l->top->prev)的时候,已经是空,如果再执行之前的语句l->top->next=NULL的话,就会造成越界访问,这个时候只需要判断每次新变成的l->top是否存在,如果存在再继续执行这个语句;这里较好的一个办法是用if去判断,把新变成的语句l->top放在if的判断条件中,如果这个语句为真,不为空,那么就可以继续执行里面的语句了;
Dnode* p = l->top;
l->top = l->top->prev;
if (l->top)//如果没有if判断条件的话
{
//只有l->top->prev有值,不为空时才执行后面的这个可能会造成越界访问的语句,否则为空的话不执行,后面已经是空了
l->top->next = NULL;//这个语句可能会造成访问越界,段错误
}
p->prev = NULL;
//释放栈顶的这块空间,同时栈顶往下移
free(p);
//每删除一个数据栈中数目减一个
l->num--;
//假设都删除完了
if (l->num == 0)
{
//因为添加数据时bottom和top是管理层中同时进行指向的
//前面一直时对l->top进行操作,最后有对它置NULL操作
//bottom一直指向栈底,没有被置空,所以最后这里需要做后续处理
l->bottom = NULL;
}
//成功删除返回1
return 1;
}
图解
5、求栈的长度
/*
函数名:Stacklength
参数列表:传进来一个栈的首地址
返回值:返回栈中的数据个数
*/
int Stacklength(Lianstack* l)
{
//如果栈不存在或栈里面没有数据,则返回0
if (l == NULL || l->num == 0)
{
return 0;
}
//否则返回栈里面的数据个数
return l->num;
}
图解
6、获取栈顶元素,不删除栈顶元素
/*
函数名:Gettop
参数列表:传进来一个栈的地址
返回值:成功返回1
失败返回0
*/
int Gettop(Lianstack* l, Mytype* d)
{
if (l == NULL || l->num == 0)
{
return 0;
}
*d = l->top->data;
return 1;
}
7、判断栈是否为空栈
/*
函数名:Isempty
参数列表:传进来一个栈的地址
返回值:是空返回1
不是空返回0
*/
int Isempty(Lianstack* l)
{
if (l == NULL || l->num == 0)
{
return 1;
}
else
{
return 0;
}
}
8、清空栈
/*
函数名:Clearstack
参数列表:传进来一个栈的地址
返回值:无
*/
void Clearstack(Lianstack* l)
{
if (l == NULL || l->num == 0)
{
return;
}
//创建一个结点指针释放所有数据结点占的空间
Dnode* p = l->top;
while (p)
{
l->top = l->top->prev;
if (l->top)//如果后面一个还存在
{
l->top->next = NULL;
}
p->prev = NULL;
free(p);
p = l->top;
}
l->num = 0;
//所有都要清空
l->bottom = NULL;
}
图解
9、销毁栈
/*
函数名:Destroystack
参数列表:传进来一个栈的地址
返回值:无
*/
void Destroystack(Lianstack* l)
{
if (l == NULL || l->num == 0 || l->top == NULL)
{
return;
}
//清空栈
Clearstack(l);
//清空栈和销毁栈的不同是这里需要把栈的头结点释放掉
free(l);
}
在这里我们发现:
-
链式栈中随时加入数据随时申请空间,随时删除数据随时释放空间
-
栈无论是加入数据还是删除数据都只对一个数值进行操作
10、链式栈帮助理解总图
整体代码记录如下
cs
#include <stdlib.h>
#include <stdio.h>
#include "Sqstack.h" //这是我们可以自己定义的头文件
int main()
{
Lianstack* l = Initstack();
int d;
printf("第一次入栈一个数据6\n");
Push(l, 6);
printf("第二次入栈一个数据7\n");
Push(l, 7);
printf("第三次入栈一个数据8\n");
Push(l, 8);
printf("第一次出栈一个数据,保存在变量d中\n");
Pop(l, &d);
printf("这个时候,d的值是:%d\n",d);
printf("再入栈一个数据5\n");
Push(l, 5);
//这里定义一个数据结点p去辅助打印栈里面的数据
Dnode* p = l->bottom;
printf("这个时候,栈里面的数据有:\n");
while (p)
{
printf("%d ", p->data);
p = p->next;
}
printf("\n");
return 0;
}
代码运行结果观察
三、顺序栈和链式栈的相同之处
-
无论是顺序栈还是链式栈,每次入栈出栈,都只能对一个数据进行操作
-
无论是顺序栈还是链式栈,每次入栈出栈,都只能对栈顶元素操作(相当于只能是链表中的尾插尾删)
四、顺序栈和链式栈的区别
- 顺序栈:开辟一块固定大小的空间,相当于定义好一块大小已知的数组;每次输入一个数据就调用入栈函数一次,而且这里的出栈,只是把栈里面的数据取出来,栈的空间还是当时开辟的那样大小,没有被释放掉;
- 链式栈:参考带头结点的双向链表,这里比起顺序栈多定义了一个头结点管理整个栈,栈里面的每个数据(除了首数据结点和尾数据结点)都是可以指向前驱和后继;而且链式栈这里的每个数据都是随时用随时开辟,随时不用随时删除释放,不存在空间不够用的情况,也不存在空间被浪费的情况;
五、栈的应用初识
目前还没有明确接触到栈的应用,以致于我们觉得栈几乎没用,像是一个形式复杂、花里胡哨的数组;
哈哈哈,但肯定不是这样,一类方法被引用至今,定有其妙的点存在;我现在是对数据结构后面的内容有了些许期待,今天老师的作业是用栈和队列的特点去实现简易计算器的功能!
现在的接触仅限于通过后缀表达式和中缀表达式的互换,用栈去实现一个计算表达式的方法。
1、中后缀表达式互化
和我们平时C语言中写的运算式子差不多,这种形式就叫做中缀表达式
9+(3-1)*3+10/2
把上面的中缀表达式化成后缀表达式:
9 3 1 - 3 * + 10 2 / +
(1)中缀表达式转化为后缀表达式
通过栈的特点,让中缀表达式中的操作数和运算符按照特定的形式入栈和出栈,从而得到变化之后的后缀表达式;现在有一个中缀表达式:9+(3-1)*3+10/2 一个栈要得到一个后缀表达式!
以下是我们老师教的中缀化后缀的方法,转化过程一定会用到栈,例子还是沿用老师当时讲的例子,里面添了一些我自己的理解,供大家参考学习:
首先我们将中缀表达式中的值一一入栈,规则如下:
- 如果碰见操作数,即将操作数直接放到后缀表达式中;如果碰见运算符,就将运算符入栈;
- 如果在运算符在入栈过程中遇到了操作符,就要看当前运算符和栈顶运算符优先级;
- 如果栈顶的运算符优先级比外面的操作符优先级低,就入栈;如果栈里面的运算符的优先级高于或等于外面运算符优先级的话,就将栈里面的所有元素都出栈,再将外面的运算符入栈;
- 遇到括号:如果在入栈过程中遇到左括号,先入栈,之后的运算符和操作数的操作规则不变,在这其中如果出栈过程中遇到左括号时,停止出栈;之后再继续出栈入栈,直到遇见右括号之后,把右括号到左括号之间的元素全部出栈,并且括号抵消;
- 最后再将所有元素出栈
具体将9+(3-1)*3+10/2转化如下:
- 9 ---->操作数,直接输出到后缀表达式中:9
-
- ---->运算符入栈,栈中元素有:+ ,后缀表达式:9
- ( ----->左括号,直接入栈,栈中元素有:+( ,后缀表达式中有:9
- 3 ----->操作数,直接输出,栈中元素有:+( ,后缀表达式有:9 3
-
- ----->运算符,观察栈里面是( ,直接入栈,栈中元素有:+( - ,后缀表达式:9 3
- 1 ----->操作数,直接输出,栈里面是:+( - ,后缀表达式中有:9 3 1
- )------>右括号,将栈直到碰到左括号之前的元素全部输出,并且左右括号抵消,不显示栈里面元素:+ ,后缀表达式中:9 3 1 -
- * ----->乘号,与栈里面的符号+比较优先级,优先级比+高,直接入栈,栈里面元素:+ *, 后缀表达式:9 3 1 -
- 3 ------>操作数,直接输出,栈里面元素: + * ,后缀表达式:9 3 1 - 3
-
- ------>运算符,和栈顶的元素比较优先级,没有*优先级高,栈里面的元素先全部出栈,+再入栈,栈里面元素: + ,后缀表达式: 9 3 1 - 3 * +
- 10 ----->操作数,直接输出,栈里面元素: + ,后缀表达式: 9 3 1 - 3 * + 10
- / ----->运算符,和栈顶运算符+比较优先级 /优先级大,直接入栈,栈里面元素: + / ,后缀表达式: 9 3 1 - 3 * + 10
- 2 ------>操作数,直接输出,栈里面元素: + / ,后缀表达式: 9 3 1 - 3 * + 10 2
- 最后全部出栈,栈里面元素:无, 后缀表达式: 9 3 1 - 3 * + 10 2 / +
可以得到转换后的后缀表达式为:9 3 1 - 3 * + 10 2 / +
(2) 后缀表达式 计算为数学表达式, 通过栈来实现
具体方法:和后缀转中缀相反,遇见操作数就入栈,遇见运算符就将栈中两个操作数出栈(第一次出栈的操作数在运算符的右边)将计算后的数值再入栈,再计算
以 9 3 1 - 3 * + 10 2 / + 为例,一步一步转化:
- 9 ------>操作数,入栈,栈中元素:9
- 3 ------->操作数,入栈,栈中元素:9 3
- 1 ------->操作数,入栈,栈中元素:9 3 1
-
- ---->操作符,出栈栈中两个元素:3 1,出栈,第一次出栈的1在-的右边,所以是3-1=2,将2继续入栈,栈中元素:9 2
- 3 ------入栈,栈中元素:9 2 3
- * ------>操作符,出栈栈中两个元素 2 3出栈,第一次出栈的3在*的右边,所以是2*3=6,将6继续入栈,栈中元素:9 6
-
- ------>操作符,出栈栈中两个元素 9 6出栈,第一次出栈的6在+的右边,所以是9+6=15,将15继续入栈,栈中元素:15
- 10------->操作数,入栈,栈中元素:15 10
- 2 ------->操作数,入栈,栈中元素:15 10 2
- / ------>操作符,出栈栈中两个元素 10 2出栈,第一次出栈的2在/的右边,所以是10/2=5,将5继续入栈,栈中元素:15 5
-
- ------>操作符 出栈栈中两个元素 15 5出栈 第一次出栈的5在+的右边 所以是15+5=20 此时此刻,没有数值,所以这个计算的结果就是20
我们用数学方法计算9+(3-1)*3+10/2 结果也是20,计算成功;
2、利用栈去判断一串数学表达式中左括号和右括号是否匹配正确(对称)
(1)问题
100-[8+(2*3)] 像这样的式子中括号就是对称分布,像100-(9+[1*2)+10]-3 这样的式子中括号不是对称分布的
(2)具体思路
在字符串中遇到左括号就入栈,当遇到右括号就出栈,肯定是栈顶元素出栈,再分情况进行比较是不是匹配的括号,原理就是入栈出栈;
这个函数的目标是判断一个数学表达式中符号是不是对称,所以设置标志位看返回值判断是否括号是镜像对称;
在这个过程中,我要借助栈去帮我实现这个功能,所以我暂时开辟了栈的空间(链式栈顺式栈都可以,但注意顺式栈有空间限制),最后标志位帮我记录下来;
我知道括号具体是不是镜像对称之后,我还要销毁这个栈,否则你申请之后,不释放的话,它一直存在于内存中,而且你也不使用,就会造成内存泄漏。
(3)代码实现如下
/*
函数名:Kuo_pd
参数列表:传进去一个字符串,可以传进去一个字符数组的首元素地址
返回值:如果是对称的完整的,就返回1;
如果不对称,就返回0;
*/
int judge_kuo(char* str)
{
//定义一个标志位,判断表达式是否正确
int flag = 1;
//初始化一个栈
SQ* s = Initstack();
//遍历字符串
int i;
for (i = 0; i <= strlen(str); i++)
{
//如果是左括号 直接入栈
if (str[i] == '(' || str[i] == '[' || str[i] == '{')
{
Push(s, str[i]);
}
//当遇到右括号的时候开始判断
if (str[i] == ')')
{
//出栈之前一定要判断栈是否为空
if (Isempty(s) != 1)
{
Mytype d = 0;
Pop(s, &d);
if (d != '(')
{
flag = 0;
break;
}
}
else //栈为空
{
flag = 0;
break;
}
}
else if (str[i] == ']')
{
//出栈之前一定要判断栈是否为空
if (Isempty(s) != 1)
{
Mytype d = 0;
Pop(s, &d);
if (d != '[')
{
flag = 0;
break;
}
}
else //栈为空
{
flag = 0;
break;
}
}
else if (str[i] == '}')
{
//出栈之前一定要判断栈是否为空
if (Isempty(s) != 1)
{
Mytype d = 0;
Pop(s, &d);
if (d != '{')
{
flag = 0;
break;
}
else //栈为空
{
flag = 0;
break;
}
}
}
}
//遍历完成之后,如果栈不为空,左边括号多了
if (Isempty(s) != 1)
{
flag = 0;
}
//销毁栈
Destroystack(s);
return flag;
}
今天明白一定不能失去创造力,尤其是我现在即将要从事的并为之要做出一定贡献的事业。
写代码就是从无到有生成一件事物,尽我所能考虑全面。
以上是我在学习过程中通过老师讲解和与同学讨论,对我自己学习内容的梳理
文中内容若有说法不准确的地方,请各位大佬指正!本人的评论区欢迎大家讨论技术性问题!