数据结构——栈

要点:

  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、顺序栈帮助理解总图

整体代码记录如下 

#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、链式栈帮助理解总图 

整体代码记录如下

#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
  2. + ---->运算符入栈,栈中元素有:+ ,后缀表达式:9
  3. ( ----->左括号,直接入栈,栈中元素有:+( ,后缀表达式中有:9
  4. 3 ----->操作数,直接输出,栈中元素有:+( ,后缀表达式有:9 3
  5. - ----->运算符,观察栈里面是( ,直接入栈,栈中元素有:+( - ,后缀表达式:9 3
  6. 1 ----->操作数,直接输出,栈里面是:+( - ,后缀表达式中有:9 3 1
  7. )------>右括号,将栈直到碰到左括号之前的元素全部输出,并且左右括号抵消,不显示栈里面元素:+ ,后缀表达式中:9 3 1 -
  8. * ----->乘号,与栈里面的符号+比较优先级,优先级比+高,直接入栈,栈里面元素:+ *, 后缀表达式:9 3 1 -
  9. 3 ------>操作数,直接输出,栈里面元素: + * ,后缀表达式:9 3 1 - 3
  10. + ------>运算符,和栈顶的元素比较优先级,没有*优先级高,栈里面的元素先全部出栈,+再入栈,栈里面元素: +  ,后缀表达式: 9 3 1 - 3 * +
  11. 10 ----->操作数,直接输出,栈里面元素: + ,后缀表达式: 9 3 1 - 3 * + 10
  12. / ----->运算符,和栈顶运算符+比较优先级 /优先级大,直接入栈,栈里面元素: + / ,后缀表达式: 9 3 1 - 3 * + 10
  13. 2 ------>操作数,直接输出,栈里面元素: + / ,后缀表达式: 9 3 1 - 3 * + 10 2
  14. 最后全部出栈,栈里面元素:无, 后缀表达式: 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
  4. - ---->操作符,出栈栈中两个元素:3 1,出栈,第一次出栈的1在-的右边,所以是3-1=2,将2继续入栈,栈中元素:9 2
  5. 3 ------入栈,栈中元素:9 2 3
  6. * ------>操作符,出栈栈中两个元素 2 3出栈,第一次出栈的3在*的右边,所以是2*3=6,将6继续入栈,栈中元素:9 6
  7. + ------>操作符,出栈栈中两个元素 9 6出栈,第一次出栈的6在+的右边,所以是9+6=15,将15继续入栈,栈中元素:15
  8. 10------->操作数,入栈,栈中元素:15 10
  9. 2 ------->操作数,入栈,栈中元素:15 10 2
  10. / ------>操作符,出栈栈中两个元素 10 2出栈,第一次出栈的2在/的右边,所以是10/2=5,将5继续入栈,栈中元素:15 5
  11. + ------>操作符 出栈栈中两个元素 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;
}

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

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


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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值