首页 > 其他分享 >复习数据结构的第五天(栈)

复习数据结构的第五天(栈)

时间:2024-05-25 18:59:27浏览次数:13  
标签:return 复习 int top 第五天 数据结构 data stack 入栈

上次我们复习了静态链表,它需要系统提前分配一定大小的内存空间,同时它和单链表一样有一个指针(游标)指向下一个节点的数组下标索引。它不具有顺序表随机访问的功能,但它可以像单链表一样插入删除时不需要移动其他元素,只需要改变游标就可以实现。

栈的概念

栈是一种只能在一端进行插入或删除的线性表,通常这一端叫做栈顶,另一端通常固定不变叫做栈底,它的主要特点就是后进先出(FILO),按照存储结构可以分为顺序栈和链栈。

这张图就简单表示了栈的结构,先放入的元素会放在栈底,依次叠加,当要取出元素时只能先从最上面的元素开始,而我们如何知道这个栈的大小以及对其进行操作呢,那就得用到栈底和栈顶了。前面说过栈底是保持不变的,所以我们只需要通过移动栈顶就可以完成所需要的功能了。在数据结构中,栈顶通常用一个栈顶指针top进行表示。

这里顺带还要提一下栈的数学性质,大家可以记一下:

当n个元素以某种顺序入栈时,且可在任意时刻出栈,所获得的出栈序列的数目N为

N = \frac{1}{n+1}C_{2n}^{n}

顺序栈的初始化

首先来讲一下顺序栈,顺序栈就跟顺序表一样,一开始要定义栈的空间大小,定义一个数组用来表示栈中存储的元素,定义一个top指向此时栈顶的位置。最后用一个结构体进行封装。

#define Maxsize 10            //定义栈的大小
typedef struct 
{
    int data[Maxsize];        //存储数据
    int top;                  //指向栈顶对栈进行操作
}stack;    

 一开始栈顶和栈底相等,整个栈内没有任何元素。

通常来讲,我们会把栈顶指针指向-1这个位置,当然有些题会让它指向0这个位置,这时候就需要注意审题。

这里我先讲栈顶指针指向-1时。

void initstack(stack *s)
{
    s->top = -1;
}

顺序栈入栈

当栈顶指针指向-1,因为我们存入的数据要从下标为0开始存,所以我们入栈的操作是先让指针移动到下一个位置,再进行插入数据。当然,在正式插入数据还需要判断栈是否还有空间,否则会出现溢出的情况。栈满的条件应该是栈顶指针指向maxsize-1的位置,因为每次入栈之后此时的栈顶指针指向的位置和数组下标是一致的,当数据存满之后,也就是push函数执行了maxsize次时,栈顶指针指向a[maxsize-1]。

bool push(stack *s,int data)
{
    if(s->top == Maxsize-1)            //如果栈空间已满
        return false;
    s->data[++s->top] = data;        /*等同于s->top = s->top + 1;
                                            s->data[s->top]=data;*/
    return true;
}

顺序栈出栈

出栈一样要先判断栈是否已经为空,栈为空的话栈顶指针是指向-1这个位置的,然后将弹出的元素先赋值给data,指针再向下移动。

bool pop(stack *s,int *data)
{
    if(s->top == -1)            //如果栈为空
        return false;
    *data = s->data[s->top--];   /*等同于data = s->data[s->top];
                                         s->top = s->top - 1;*/
    return true;
}

如果是初始化是top指针指向0,入栈出栈和判断栈满栈空的条件都会不一样。

由于初始化top指针为0,我们从数组下标为0开始插入,所以需要先插入数据再移动栈顶指针。另外,每次入栈后top指向的位置是数组下标的下一位,所以当栈满时top应指向Maxsize这个位置。

bool push(stack *s,int data)
{
    if(s->top == Maxsize)
        return false;
    s->data[s->top++] = data;        //先赋值再移动指针
    return true;
}

同理,出栈时要先移动指针再进行赋值,栈空时栈顶指针是指向0的。

bool pop(stack *s,int *data)
{
    if(s->top == 0)
        return false;
    *data = s->data[--s->top];        //先移动指针再赋值
    return true;
}

 共享栈

共享栈其实就是两个栈共享同一片内存空间,它与普通的栈相比更有效地利用了存储空间,并且时间复杂度与普通的栈一样入栈出栈都是O(1)。

两个栈的栈底一开始分别指向共享空间的两端。

入栈时第一步还是得先判断栈空间是否溢出。以这张图为例就是top[0]+1=top[1]。然后判断元素进入的是哪个栈,然后进行相应的入栈操作。

出栈时先判断是哪一个栈要出栈,然后判断它是否为空。

#define Maxsize 10
typedef struct 
{
    int data[Maxsize];
    int top0;            //第一个栈的头指针
    int top1;            //第二个栈的头指针
}stack;
void initstack(stack *s)
{
    s->top0 == -1;
    s->top1 == Maxsize;
}
bool push(stack *s,int i,int data)    //    多了一个i判断是入哪个栈
{
    if(s->top0+1 == s->top1||i<0||i>1)        //栈已满或者输入不合法数据
        return false;
    if(i == 0)
        s->data[++s->top0] = data;
    else
        s->data[--s->top1] = data;
    return true;
}
bool pop(stack *s,int i,int *data)
{
    if(s->top0 == -1 && i==0 || s->top1==Maxsize && i==1 || i<0 || i>1)
        return false;
    if(i == 0)
        *data = s->data[s->top0--];
    else
        *data = s->data[s->top1--];
    return true;
}

所以顺序栈只需要记住两个状态和两个操作就可以,两个状态就是栈空和栈满的状态,两个操作就是先移动指针还是先赋值。


链栈初始化

链栈就和单链表类似,初始化基本可以照搬。

typedef struct node
{
    struct node *next;
    int data;
}node,*stack;

 同样也需要分为有头节点和无头结点的情况。

这里我先讲有头结点的情况:

void initstack(stack *s)
{
    *s = (node *)malloc(sizeof(node));
    (*s)->next = NULL;
}

链栈入栈

链栈入栈其实就跟单链表的头插法一样,栈顶指针只需要指向头节点的下一节点就可以了。重新回顾一下头插法的过程。

代码基本也是一样的。大家可以自己手敲一下就当复习了。

void push(stack *s,int data)
{
    node *p = *s;
    node *q = (node *)malloc(sizeof(node));
    q->data = data;
    q->next = p->next;
    p->next = q;
}

链栈出栈

出栈其实就等于将单链表第一个带数据的节点删除。

bool pop(stack *s,int *data)
{
    if((*s)->next==NULL)    //如果栈为空
    {
        return false;
    }
    node *p = (*s)->next;
    *data = p->data;
    (*s)->next = p->next;
    free(p);
    return true;
}  

接下来是没有头节点的情况(代码过于简单不再赘述,下面仅作参考):

void initstack(stack *s)
{
    *s = NULL;
}
void push(stack *s,int data)
{
    node *p = (node *)malloc(sizeof(node));
    p->data = data;
    p->next = *s;
    *s = p;
}
bool pop(stack *s,int *data)
{
    if(s == NULL)
        return false;
    node *p = *s;
    *data = p->data;
    *s = (*s)->next;
    free(p);
    return true;
}

栈的应用

这里我就讲一下考研当中容易考的栈的一个应用——中缀表达式转换为后缀表达式以及表达式求值。

在讲之前简单讲一下什么是表达式吧。

表达式分为三种,在生活中我们常用的算数表达式就是中缀表达式,例如3*(4+2)-5等等。但是计算机是怎么实现这些算术操作的呢,一般以我们的角度而言肯定会先进行括号里的运算,但是计算机怎么知道要这么干呢总不可能给它设置很多分支吧,所以不知道哪位计算机大神想出来可以利用一个古老的波兰数学家卢卡西维奇提出来的后缀表达式或前缀表达式,这么做的好处就是不需要知道算数运算符的优先级顺序,简化了逻辑,适宜计算机操作。

此时我的心情就是:

说了一些废话,还是直接上干货(用栈转换的方法,其实还可以用二叉树后面再讲):

  1. 先初始化一个栈和一个数组,栈用来存放运算符,数组用来存放表达式。
  2. 从左到右依次遍历中序表达式。如果是字符直接放进数组。
  3. 如果是运算符,就得分类讨论:
  • 如果是加减乘除符号,首先要判断栈里是否有元素,栈里没有元素直接入栈。若有比较栈顶元素与该符号的优先级,若栈顶元素优先级低于该符号,该符号入栈。若栈顶元素优先级等于或高于该符号,弹出栈顶元素至数组中直至栈为空或者出现栈顶元素优先级低于该符号,然后该符号再入栈。
  • 如果是左括号,则直接入栈。如果是右括号,则弹出栈顶元素至数组中直至遇见左括号,然后将左右括号抛弃。

最后得到的数组就是后缀表达式。

以a+b-a*((c+d)/e-f)+g为例:

1、指向a,不是运算符进入数组

2、指向+,运算符,此时栈为空入栈

3、指向b,不是运算符进入数组

4、指向-,运算符,优先级与+相等,+出栈进入数组,栈空,-入栈

5、指向a,不是运算符进入数组

6、指向*,运算符,优先级比-高,入栈

7、指向(,直接入栈

8、指向(,直接入栈

9、指向c,不是运算符进入数组

10、指向+,优先级比(高,入栈

11、指向d,不是运算符进入数组

12、指向),弹出栈元素进入数组直至出现(

13、指向/,运算符,优先级比(高,入栈

14、指向e,不是运算符进入数组

15、指向-,运算符,优先级比/低,/出栈进入数组,优先级比(高,入栈

16、指向f,不是运算符进入数组

17、指向),弹出栈元素进入数组直至出现(

18、指向+,运算符,优先级比*低,优先级与-相同,依次弹出*,-,栈空入栈

19、指向g,不是运算符进入数组

20、遍历完成依次弹出栈顶元素至数组。

用代码实现如下:

#include<stdio.h>
#include<stdbool.h>
int priority(char a,char b)        //判断优先级,a>b返回1,否则返回0
{
    //a只可能是+,-,*,/
    if(a == b)          //两个字符相同不可以入栈
    {
        return 0;
    }
    else                //两个字符不相同
    {
        if(b == '(' )     //左括号优先级最小,任何元素都可入栈
        {
           return 1;
        }
        else if (b == '*'|| b == '/')
        {
            return 0;
        }
        else            //当b是+或者-
        {
            return (a == '*' || a == '/') ? 1 : 0;
        }
    }
   
}
void convert(char a[],char b[])
{
    int i,count = 0,top = -1;
    char stack[20];      //定义一个栈
    for(i=0;a[i]!='\0';i++)         //从左向右遍历
    {
        if(a[i] == '+'|| a[i] == '-' || a[i] == '*' || a[i] == '/')
        {
            if(top == -1 || priority(a[i],stack[top]) == 1) //如果栈为空或者栈顶元素优先级小于该运算符
            {
                stack[++top] = a[i];        //入栈
            }
            else
            {
                                            //出栈直至遇见优先级小于该运算符或栈为空
                while(top!=-1&&priority(a[i],stack[top]) == 0)
                {
                    b[count] = stack[top--];
                    count++;
                }
                stack[++top] = a[i];
            }
        }
        else if(a[i] == '(')
        {
            stack[++top] = a[i];            //直接入栈
        }
        else if(a[i] == ')')
        {
            while(stack[top]!='(' && top!=-1 )              //一直出栈直至遇见左括号或栈空
            {
                b[count++] = stack[top--];
            }
            if(top!=-1)
            {
                top--;          //去除左括号
            }
        }
        else
        {
            b[count] = a[i];
            count++;
        }
    }
    while (top != -1) {             // 如果还有元素在栈中,则全部出栈  
        b[count++] = stack[top--];  
    }  
    b[count] = '\0';
}
int main()
{
    char a[20],b[20];     //a数组存储中缀表达式
                          //b数组存储后缀表达式
    int i=0;
    while( (a[i]=getchar()) !='\n')         //输入表达式到数组中
    {
        i++;
    }
    a[i] = '\0';
    convert(a,b);
    for (i = 0; b[i] != '\0'; i++) {  
        printf("%c", b[i]);  
    }  
}

如果要化为前缀表达式的话,其实原理差不多,我这里就不多解释了,简单介绍一下方法就行了,有愿意了解的可以自己动手实现代码。

  1. 先初始化一个栈和一个数组,栈用来存放运算符,数组用来存放表达式。
  2. 从右到左依次遍历中序表达式。如果是字符直接放进数组。
  3. 如果是运算符,就得分类讨论:
  •  如果是加减乘除符号,首先要判断栈里是否有元素,栈里没有元素直接入栈。若有比较栈顶元素与该符号的优先级,若栈顶元素优先级高于或等于该符号,该符号入栈。若栈顶元素优先级低于该符号,弹出栈顶元素至数组中直至栈为空或者出现栈顶元素优先级高于或等于该符号,然后该符号再入栈。       
  • 如果是右括号,则直接入栈。如果是左括号,则弹出栈顶元素至数组中直至遇见右括号,然后将左右括号抛弃。

当化成前缀或后缀表达式之后,就可以进行运算了。

运算规则就是(这里以后缀表达式为主)

  • 将数组内容从左到右依次存入栈中
  • 若存入的是字符则直接入栈
  • 若存入的是运算符则连续出栈两次,出栈的第一个元素放在右边,出栈的第二个元素放在左边进行相应的运算,得到的结果再存入栈

假设我们输入的都是合法输入,这里的数字只能是0~9,且除法得到的结果都是整数。

#include<stdio.h>
int op(char a,char b,char c)
{
    switch (b) {  
        case '+':  
            return a + c;  
        case '-':  
            return a - c;  
        case '*':  
            return a * c;  
        case '/':  
            if (c == 0) {  
                // 返回一个表示错误的特殊值  
                return -65535;  
            } else {  
                return a / c;  
            }  
        default:  
            // 如果运算符不是 +、-、* 或 /
            return -65535;  
    }  
}
int carculate(char array[])
{
    int stack[20];
    int top = -1;   //定义一个栈
    int result;     //得到一次运算的结果
    for(int i=0;array[i]!='\0';i++) //从左到右遍历
    {
        if(array[i] == '+'||array[i] == '-'||array[i] == '*'||array[i] == '/')
        {
            //如果是运算符
            int c = stack[top--];   //第一个栈顶元素放右边
            int a = stack[top--];   //第二个栈顶元素放左边
            result = op(a,array[i],c);
            stack[++top] = result;
        }
        else{
            array[i] = array[i] - '0';  //  将字符型数字改为数字值
            stack[++top] = array[i];
        }
    }
    return stack[top];
}
int main()
{
    char data[20];      //键入你想要输入的后缀表达式
    int i=0;
    while((data[i]=getchar())!='\n')
    {
        i++;
    }
    data[i] = '\0';
    printf("%d",carculate(data));
}

标签:return,复习,int,top,第五天,数据结构,data,stack,入栈
From: https://blog.csdn.net/qq_62880031/article/details/139074300

相关文章

  • C语言数据结构栈的概念及结构、栈的实现、栈的初始化、销毁栈、入栈、出栈、检查是否
    文章目录前言栈的概念及结构栈的实现一、栈结构创建二、初始化结构三、销毁栈四、入栈五、出栈六、检查是否为空七、获取栈顶元素八、获取有效元素的个数九、测试1十、测试2总结前言C语言数据结构栈的概念及结构、栈的实现、栈的初始化、销毁栈、入栈、出栈、检......
  • 第二周第五天
    今天是跟随视频学习vue2的一些相关知识,并跟随视频学习和写程序。主要是了解一些有关渲染子类的知识,同时也练习了一些静态页面。总体感受前面的比较简单,后面的越来越复杂,有点看不懂了,还记不住。加油!我相信多练习一点也就会了。以下今天写的一些程序。视频实在看不懂也没关系......
  • Redis基本数据结构
    String数据结构如果存储的是整型,直接把值存储在RedisObject里面,数据类型为int。如果存储的数据量不大(早期版本,32字节),采用动态字符串SDS存储,存储类型是embstr。超过32字节,采用动态字符串SDS进行存储,存储类型是raw。embstr和raw类型的区别在于,RedisObject和embstr是连续存......
  • Java数据结构与算法(平衡二叉树)
    前言平衡二叉树是为了提高二叉树的查询速度,通过满足特定的条件来保持其平衡性。平衡二叉树具有以下特点:左子树和右子树的高度差不会大于1,这是为了确保树的高度不会过大,从而减少查询时的磁盘I/O开销,提高查询速度。平衡二叉树上的所有结点的平衡因子(左子树深度减去右子树深度的......
  • 数据结构(栈)
    1.栈的概念和结构概念:栈是一种线性表,它只能从固定的一端进行数据的插入和删除,这一端称为栈顶,另一端称为栈底。栈遵循先入后出的原则。压栈:栈的插入操作可以称为进栈/压栈/入栈,入数据在栈的顶部。出栈:栈的删除操作叫出栈,出的数据也是在栈顶。进栈:出栈:2.栈的实现栈的实......
  • 数据结构(树)
    1.树的概念和结构树,顾名思义,它看起来像一棵树,是由n个结点组成的非线性的数据结构。下面就是一颗树:树的一些基本概念:结点的度:一个结点含有的子树的个数称为该结点的度;如上图:A的为6叶结点或终端结点:度为0的结点称为叶结点;如上图:B、C、H、I...等结点为叶结点非终端结点或......
  • 山大软件23年下半年计算机网络复习
     PS:结合老师屁屁踢复习吧,图片丢失太多懒得改了 point单播:只有一个发送方和一个接收方的点到点传输。组播:将一个数据包发送给一组机器,即所有机器的一个子集。广播:将一个数据包发送给所有的目标机器。面向连接的服务:按照电话系统建模,服务用户首先必须建立一个连接,然......
  • 山大软件24年上半年人机交互复习
    考试内容(回忆版)2024-2025山大软件人机交互考试(回忆版)-CSDN博客绪论1什么是人机交互人机交互定义人机交互是关于设计、评价和实现供人们使用的交互式计算机系统,且围绕这些方面的主要现象进行研究的学科(技术上)狭义:人机交互从技术上讲(狭义的),主要是研究人与计算机之间......
  • 数据结构顺序表实现通讯录
    目录1.前言:2.通讯录项目的创建3.通讯录的实现3.1通讯录的初始化3.2通讯录的销毁3.3通讯录添加数据3.4通讯录查找数据3.5通讯录展示数据  3.6通讯录删除数据 3.7通讯录修改数据 4.通讯录完整代码4.1test.c4.2SeqList.h4.3SeqList.c 4.4Contac......
  • 【免费Web系列】大家好 ,今天是Web课程的第五天点赞收藏关注,持续更新作品 !
     这是Web第一天的课程大家可以传送过去学习 http://t.csdnimg.cn/K547r课程内容SpringBootWeb入门HTTP协议Web服务器-Tomcat1.Tomcat1.1简介Tomcat服务器软件是一个免费的开源的web应用服务器。是Apache软件基金会的一个核心项目。由Apache,Sun和其他一些公司......