数据结构——栈

要点:

  1. 先进后出,后进先出;

  2. 只能对栈顶元素操作;

一、顺序栈

通过老师讲解,我理解到,顺序栈好像是一个特殊的数组。

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. 无论是顺序栈还是链式栈,每次入栈出栈,都只能对一个数据进行操作

  2. 无论是顺序栈还是链式栈,每次入栈出栈,都只能对栈顶元素操作(相当于只能是链表中的尾插尾删)

四、顺序栈和链式栈的区别

  1. 顺序栈:开辟一块固定大小的空间,相当于定义好一块大小已知的数组;每次输入一个数据就调用入栈函数一次,而且这里的出栈,只是把栈里面的数据取出来,栈的空间还是当时开辟的那样大小,没有被释放掉;
  2. 链式栈:参考带头结点的双向链表,这里比起顺序栈多定义了一个头结点管理整个栈,栈里面的每个数据(除了首数据结点和尾数据结点)都是可以指向前驱和后继;而且链式栈这里的每个数据都是随时用随时开辟,随时不用随时删除释放,不存在空间不够用的情况,也不存在空间被浪费的情况;

五、栈的应用初识

目前还没有明确接触到栈的应用,以致于我们觉得栈几乎没用,像是一个形式复杂、花里胡哨的数组;

哈哈哈,但肯定不是这样,一类方法被引用至今,定有其妙的点存在;我现在是对数据结构后面的内容有了些许期待,今天老师的作业是用栈和队列的特点去实现简易计算器的功能!

现在的接触仅限于通过后缀表达式和中缀表达式的互换,用栈去实现一个计算表达式的方法。

1、中后缀表达式互化

和我们平时C语言中写的运算式子差不多,这种形式就叫做中缀表达式

9+(3-1)*3+10/2

把上面的中缀表达式化成后缀表达式:

9 3 1 - 3 * + 10 2 / +

(1)中缀表达式转化为后缀表达式

通过栈的特点,让中缀表达式中的操作数和运算符按照特定的形式入栈和出栈,从而得到变化之后的后缀表达式;现在有一个中缀表达式:9+(3-1)*3+10/2 一个栈要得到一个后缀表达式!

以下是我们老师教的中缀化后缀的方法,转化过程一定会用到栈,例子还是沿用老师当时讲的例子,里面添了一些我自己的理解,供大家参考学习:

首先我们将中缀表达式中的值一一入栈,规则如下:

  1. 如果碰见操作数,即将操作数直接放到后缀表达式中;如果碰见运算符,就将运算符入栈;
  2. 如果在运算符在入栈过程中遇到了操作符,就要看当前运算符和栈顶运算符优先级;
  3. 如果栈顶的运算符优先级比外面的操作符优先级低,就入栈;如果栈里面的运算符的优先级高于或等于外面运算符优先级的话,就将栈里面的所有元素都出栈,再将外面的运算符入栈;
  4. 遇到括号:如果在入栈过程中遇到左括号,先入栈,之后的运算符和操作数的操作规则不变,在这其中如果出栈过程中遇到左括号时,停止出栈;之后再继续出栈入栈,直到遇见右括号之后,把右括号到左括号之间的元素全部出栈,并且括号抵消;
  5. 最后再将所有元素出栈

具体将9+(3-1)*3+10/2转化如下:

  1. 9 ---->操作数,直接输出到后缀表达式中:9
    • ---->运算符入栈,栈中元素有:+ ,后缀表达式:9
  2. ( ----->左括号,直接入栈,栈中元素有:+( ,后缀表达式中有:9
  3. 3 ----->操作数,直接输出,栈中元素有:+( ,后缀表达式有:9 3
    • ----->运算符,观察栈里面是( ,直接入栈,栈中元素有:+( - ,后缀表达式:9 3
  4. 1 ----->操作数,直接输出,栈里面是:+( - ,后缀表达式中有:9 3 1
  5. )------>右括号,将栈直到碰到左括号之前的元素全部输出,并且左右括号抵消,不显示栈里面元素:+ ,后缀表达式中:9 3 1 -
  6. * ----->乘号,与栈里面的符号+比较优先级,优先级比+高,直接入栈,栈里面元素:+ *, 后缀表达式:9 3 1 -
  7. 3 ------>操作数,直接输出,栈里面元素: + * ,后缀表达式:9 3 1 - 3
    • ------>运算符,和栈顶的元素比较优先级,没有*优先级高,栈里面的元素先全部出栈,+再入栈,栈里面元素: + ,后缀表达式: 9 3 1 - 3 * +
  8. 10 ----->操作数,直接输出,栈里面元素: + ,后缀表达式: 9 3 1 - 3 * + 10
  9. / ----->运算符,和栈顶运算符+比较优先级 /优先级大,直接入栈,栈里面元素: + / ,后缀表达式: 9 3 1 - 3 * + 10
  10. 2 ------>操作数,直接输出,栈里面元素: + / ,后缀表达式: 9 3 1 - 3 * + 10 2
  11. 最后全部出栈,栈里面元素:无, 后缀表达式: 9 3 1 - 3 * + 10 2 / +

可以得到转换后的后缀表达式为:9 3 1 - 3 * + 10 2 / +

(2) 后缀表达式 计算为数学表达式, 通过来实现

具体方法:和后缀转中缀相反,遇见操作数就入栈,遇见运算符就将栈中两个操作数出栈(第一次出栈的操作数在运算符的右边)将计算后的数值再入栈,再计算

以 9 3 1 - 3 * + 10 2 / + 为例,一步一步转化:

  1. 9 ------>操作数,入栈,栈中元素:9
  2. 3 ------->操作数,入栈,栈中元素:9 3
  3. 1 ------->操作数,入栈,栈中元素:9 3 1
    • ---->操作符,出栈栈中两个元素:3 1,出栈,第一次出栈的1在-的右边,所以是3-1=2,将2继续入栈,栈中元素:9 2
  4. 3 ------入栈,栈中元素:9 2 3
  5. * ------>操作符,出栈栈中两个元素 2 3出栈,第一次出栈的3在*的右边,所以是2*3=6,将6继续入栈,栈中元素:9 6
    • ------>操作符,出栈栈中两个元素 9 6出栈,第一次出栈的6在+的右边,所以是9+6=15,将15继续入栈,栈中元素:15
  6. 10------->操作数,入栈,栈中元素:15 10
  7. 2 ------->操作数,入栈,栈中元素:15 10 2
  8. / ------>操作符,出栈栈中两个元素 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;

}

今天明白一定不能失去创造力,尤其是我现在即将要从事的并为之要做出一定贡献的事业。

写代码就是从无到有生成一件事物,尽我所能考虑全面。


以上是我在学习过程中通过老师讲解和与同学讨论,对我自己学习内容的梳理

文中内容若有说法不准确的地方,请各位大佬指正!本人的评论区欢迎大家讨论技术性问题!

相关推荐
jingling55514 分钟前
后端开发刷题 | 兑换零钱(动态规划)
java·开发语言·数据结构·算法·动态规划
搁浅小泽30 分钟前
数据结构绪论
数据结构·算法
流殇25835 分钟前
数据结构(6)哈希表和算法
数据结构·哈希算法·散列表
摆烂小白敲代码43 分钟前
大一新生以此篇开启你的算法之路
c语言·数据结构·c++·人工智能·经验分享·算法
Cyan_RA93 小时前
C 408—《数据结构》算法题基础篇—链表(上)
java·数据结构·算法·链表·c·408·计算机考研
youyiketing3 小时前
排序链表(归并排序)
数据结构·链表
程序猿阿伟4 小时前
《C++位域:在复杂数据结构中的精准驾驭与风险规避》
开发语言·数据结构·c++
独領风萧4 小时前
数据结构之栈(数组实现)
c语言·数据结构
一遍再一遍4 小时前
数据结构——初识数据结构
数据结构
18你磊哥5 小时前
java重点学习-集合(List)
java·数据结构·学习·list