【数据结构】励志大厂版·初阶(复习+刷题):栈与队列


前引:本篇将由小编与大家一起复习 栈 、队列 的知识点,栈、队列的顺序、链式结构各个缺点好处,如何实现、对于一般的增删查找此篇文章一定再详细不过!对代码的注释、何时需要判断、特殊情况,白话文版一解到底,彻底了解栈与队列。文章末尾还精心选择了几道例题,小编同样会精心讲解,如果伙伴们被感动了的话!可否一键三连~好了,正文开始~
目录
知识点速览
栈
何为栈?栈是一种线性结构,只允许在一端进行插入和拿出元素的数据结构。
压栈(也叫入栈):插入元素
出栈:拿出元素
栈顶:进行插入和拿出元素的一端
栈底:固定的,不允许进行插入和拿出元素

存储特点:先进后出、后入先出。想象成一个一端开口的容器!

栈的存储结构分析
首先栈可以用两种结构来实现,一种是顺序栈,也就是用数组实现;另一种是链式栈,用链表实现。这两种结构栈我们应该选择哪一种来实现好?综合考虑,我们优先选择顺序栈
首先是顺序栈:
顺序栈出栈、入栈都很方便,栈顶指针也方便指向,有一个小缺点:扩容可能导致空间浪费

其次是链式栈:
链式栈虽然扩容很方便,无浪费情况,但是出栈时,不好控制栈顶指针, 设置稍微复杂一点。比如单链表,需要将链表倒置过来,否则栈的存储特定就与其不符,出栈时需要知道倒数第二个链表节点。还有双链表结构,虽然不用去找倒数第二个节点,但是写起来没有那么的简单、快捷

栈的基本操作
初始化栈 判断栈空 入栈 读取栈元素 出栈 销毁栈
结构体定义
顺序栈只需要一个栈顶指针、一个空间指针即可。在初始化时给空间指针开辟空间,这里小编建议可以额外设置一个空间上限,方便以后扩容
typedef struct Olderstack { //栈顶指针 int top; //存储空间指针 int* data; //存储上限 int max; }Olderstack;初始化栈
栈的初始状态应该是栈顶指针在栈底的位置,再给栈开辟空间

//初始化栈 void preliminary(Olderstack* Stack) { //初始化栈顶指针、空间上限 Stack->max = MAX; Stack->top = 0; //开辟存储空间 Stack->data = (int*)malloc(sizeof(int) * MAX); //判断空间有效性 if (Stack->data == NULL) { printf("开辟失败\n"); return; } //(可以反馈一下) printf("空间开辟成功\n"); } 判断栈空
栈空的标志就是栈顶指针指向栈底。这里直接根据栈顶指针指向来判。这里解释一下为何判断选择传址?因为我们后面在打印栈元素、出栈的时候需要用到判断,为了接口的一致选择传址,同时防止代码被修改,可以用 const 修饰

//判断栈空 void Judgment(const Olderstack* Stack) { //直接判断栈顶指针 if (Stack->top == 0) { printf("栈为空\n"); return; } } 入栈
顺序栈的入栈就像将元素存进数组一样,只是需要控制栈顶指针的更新,考虑栈满需要扩容的情况

//入栈 void Enter(Olderstack* Stack, int data) { //先判断存储空间是否已满 if (Stack->top == Stack->max) { //已满需要扩容 int* pc = (int*)realloc(Stack->data, sizeof(int) * (Stack->max) * 2); //判断空间有效性 if (pc == NULL) { printf("扩容失败\n"); return; } Stack->data = pc; //更新空间上限 Stack->max *= 2; } //更新栈顶指针 Stack->top++; //入栈 Stack->data[Stack->top - 1] = data; printf("入栈成功\n"); }读取栈元素
因为栈的存储特性,我们只能读取栈顶元素,同时读取栈元素并不会改变栈顶指针,需要额外一个变量去代替栈顶指针的移动来不断读取栈顶元素。这里需要判断栈空,为了与前面的接口适用,我们选择取地址,同时注意加 const 修饰,防止误触!注意:前置减减 后置减减的区别

//读取栈元素 void Read(const Olderstack* Stack) { //判断栈空 Judgment(Stack); //设置变量代替栈顶指针 int size = Stack->top; //读取栈元素 printf("栈元素:"); while (size) { printf("%d ", Stack->data[--size]); } }下面我们通过不断入栈来试一下打印效果,注意栈的存储特点:先进后出

出栈
出栈按照理论逻辑:栈顶指针每次指向靠近栈顶元素的末尾,因此最好先出栈,再改变栈顶指针
注意:前置减减、后置减减的区别 【前置是先减再使用,后置是先使用再减减】

//出栈 void Out(Olderstack* Stack) { //判断栈空 Judgment(Stack); //出栈顶元素 Stack->data[--Stack->top]; printf("出栈成功\n"); }销毁栈
按照逻辑,我们先清理栈空间存储的数据,再释放栈。下面我们释放之后再来测试一下:


//销毁栈 void Undermine(Olderstack* Stack) { //出栈存储的元素 while (Stack->top) { Out(Stack); } //释放栈存储空间 free(Stack->data); Stack->data = NULL; printf("栈空间释放成功\n"); }队列
队列也是一种线性数据结构,允许在一端存入数据、一端拿出数据
队头:允许拿出(删除)数据的一端,也称队首
队尾:允许存入数据的一端
结构特点:拥有两个指针指向队头与队尾的元素,随着元素的变化发生改变
存储特点:先进先出

队列的结构分析
队列的实现也可以选择顺序结构、链式结构,但是总体考虑,以链式结构最佳
首先是队列的顺序结构:
我们知道顺序结构移除某个元素是很方便的,但是队列有两个指针分别随着元素的变化发生改变,这导致出现了以下的情况:随着元素逐渐出去,队头指针head会不断移动,当不断地进数据出数据会导致空间浪费越来越大

其次是链式结构:
队列更好的实现方式是链式结构,因为较于顺序结构,虽然是采用节点来存储数据,但是它的出队列、入队列就是链表的头删、尾插,使用起来比顺序结构更效率,且没有空间的浪费,如下图


队列的基本操作
初始化队列 入队 出队 获取队尾元素 获取队头元素 判断对空 销毁队列
结构体定义
首先咱们采用的是链表来定义的,因此肯定需要一个链表结构、其次需要两个指针指向队尾队头的元素,为了不与链表发生混乱,我们将这两个指针单独放在一个结构体里面。
注意:队列指针的类型应该是链表类型的,因为它是指向链表的,后面通过队列指针来维护节点
//链表节点 typedef struct List { struct List* next; int data; }List; //队列空间 typedef struct Queqe { struct List* head; struct List* real; //当前队列元素个数 int size; }Queqe;初始化队列
重点:咱们的初始化不是先初始化一个链表开辟头指针,而是开辟队列空间,在入队列的时候再开辟链表节点

//初始化队列 Queqe* preliminary() { //开辟队列空间 Queqe* Space = (Queqe*)malloc(sizeof(Queqe)); //判断空间有效性 if (Space == NULL) { printf("队列空间开辟失败\n"); return NULL; } //初始化队列成员 Space->head = NULL; Space->real = NULL; Space->size = 0; printf("队列空间开辟成功\n"); return Space; }入队列
重点:咱们得数据是放在链表里面的,所以应该先开辟一个链表节点用来放数据,然后队列指针指向之前需要判断,如果这是第一个存入的数据,那么队列指针指向这个节点;如果是第 N 个数据,那么就需要通过链表节点的 next 指针来进行连接节点
如何通过队列空间来找到链表节点存储数据?
因为队列指针类型是链表类型,通过队列指针找链表
例如:Space->real->next=newnode Space->real=newnode
为何队列空间的判断最好用元素个数?
这里存储的时候根据队列指针是否为空也可进行判断,因为链表末尾为空,队列指针如果为空,则表示指到了链表的末尾,没有节点。但是直观上根据元素个数判断更加直观,没有难度

//入队列 void Enter(Queqe** Space, int data) { //开辟节点空间 List* newnode = (int*)malloc(sizeof(List)); //判断空间有效性 if (newnode == NULL) { printf("节点空间开辟失败\n"); return; } //设置链表空间 newnode->data = data; newnode->next = NULL; //如果是第一个元素 if ((*Space)->size == 0) { (*Space)->real = newnode; (*Space)->head = newnode; (*Space)->size++; } else { //连接节点 (*Space)->real->next = newnode; //改变队列指针 (*Space)->real = newnode; (*Space)->size++; } printf("入队成功\n"); }出队列
先判断是否有元素可以出队,如果有则释放该链表节点后,再改变队列指针指向
//出队列 void Out(Queqe** Space) { //判断队列是否为空 if ((*Space)->size == 0) { printf("队列为空,无法出队\n"); return; } //释放对应的链表节点 List* cur = (*Space)->head; //改变队列指针指向 (*Space)->head = (*Space)->head->next; free(cur); cur = NULL; //队列元素个数减减 (*Space)->size--; printf("出队成功\n"); }获取队尾元素
先判断队列是否为空,否则直接打印对应队尾指针指向节点的元素即可

//获取队尾元素 void Tail_team(Queqe* Space) { //如果队列元素为空 if (Space->size == 0) { printf("队列为空,无法获取\n"); return; } List* cur = Space->real; printf("队尾元素:%d\n", cur->data); }获取队头元素
先判断队列是否为空,然后打印对应队列指针所指向节点的元素

//获取队头元素 void Head_team(Queqe* Space) { //如果队列元素为空 if (Space->size == 0) { printf("队列为空,无法获取\n"); return; } printf("队尾元素:%d\n", Space->head->data); }判断队空
这里咱们就不多说了,前面咱们一直都有这个判断,这里只是单独封装成一个函数
//判断队空 void Empty_team(Queqe* Space) { //如果队列元素为空 if (Space->size == 0) { printf("队列为空\n"); return; } }销毁队列
咱们的队列是由链表完成的,应该先释放链表,再释放队列空间,否则就找不到链表位置了

注意:队列里链表应该由队头销向队尾,因为链表的尾部在队尾这端,下面小编进行了详细注释
同时注意二级指针指向的是一级指针的地址,对二级指针解引用一次,就拿到一级指针地址
例如:

//销毁队列 void Undermine(Queqe** Space) { //如果队列元素为空 if ((*Space)->size == 0) { printf("队列为空,无法销毁\n"); return; } //销毁链表 List* cur = (*Space)->head; while ( (*Space)->head ) { (*Space)->head = (*Space)->head->next; //释放链表节点 free(cur); cur = NULL; //重新指向下一个节点 cur = (*Space)->head; } //销毁队列空间 free(*Space); *Space = NULL; printf("销毁成功\n"); }栈和队列OJ题(典型)

题目分析:
有一个字符数组,里面的内容是几个括号,根据数组内容判断括号是否是完整对应来返回不同的值。这里就是根据字符的位数来判断对应位置的字符与其是否相匹配的问题

实例讲解:
比如现在有一个字符串“ ()”
先建立一个堆,第一个字符是左括号“(”入栈,下一次进入循环的字符是“)”,则出栈顶元素与其进行匹配,此时匹配成功,再次进入循环遇到循环结束条件,最后栈也为空,返回 true
比如现在有一个字符串“)[”
先建立一个堆,此时第一个字符是右括号“)”,选择出栈,但是栈为空,则直接返回 false
思维讲解:
我们可以建议一个堆,如果当前字符是“(”“{”“[”,就存入堆里面,如果是“)”“}”“]”就将堆顶的元素拿出来与之对比,如果匹配成功,就继续,直到数组内容到达“\0”,否则返回 false,听到这里肯定还不是很清楚,没事(小编当时也是这样的!哈哈!下面有画图演示,千万别担心!)大家接着往下看,跟着思路走,整体就疏通了!我们主要是通过堆的存储与取出堆顶元素来实现这道题!
(1)先实现\拷贝对应的堆功能过来,这题需要初始化堆、入堆、出堆顶元素、判断栈空、销毁堆
注意:改变相应存储的元素、空间指针类型为 char 类型
这个判断栈空的函数我们可以简写,直接判断栈的元素个数,这对新手小白很友好
下面是将之前实现堆的函数直接拷贝过来!注意判断栈空的函数小编直接简写了哦
#define MAX 10 typedef char Datatype; typedef struct Olderstack { //栈顶指针 int top; //空间上限 int max; //存储空间指针 Datatype* data; }Olderstack; //初始化栈 void preliminary(Olderstack* Stack) { //初始化栈顶指针、空间上限 Stack->max = MAX; Stack->top = 0; //开辟存储空间 Stack->data = (Datatype*)malloc(sizeof(Datatype) * MAX); //判断空间有效性 if(Stack->data==NULL) { return; } } //入栈 void Enter(Olderstack* Stack, Datatype data) { //先判断存储空间是否已满 if (Stack->top == Stack->max) { //已满需要扩容 Datatype* pc = (Datatype*)realloc(Stack->data, sizeof(Datatype) * (Stack->max) * 2); //判断空间有效性 if (pc == NULL) { return; } Stack->data = pc; //更新空间上限 Stack->max *= 2; } //更新栈顶指针 Stack->top++; //入栈 Stack->data[Stack->top - 1] = data; } //出栈 char Out(Olderstack* Stack) { //出栈顶元素 return Stack->data[--Stack->top]; } //销毁栈 void Undermine(Olderstack* Stack) { //出栈存储的元素 while (Stack->top) { Out(Stack); } //释放栈存储空间 free(Stack->data); Stack->data = NULL; } (2)其次我们创建一个栈,再初始化(因为此题是利用栈完成的)
//创建栈 Olderstack Stack; //初始化栈 preliminary(&Stack);(3)进入循环判断,直到遇到 \0 退出循环
如果是左括号就入栈,然后字符指针后移一位
如果是右括号就先判断是不是栈空,否则出栈拿栈顶元素与与其匹配,不匹配则返回
while(*s) { //如果是前括号就入栈 if(*s=='(' || *s=='[' || *s=='{') { Enter(&Stack, *s); s++; } else { //如果直接是右括号且栈为空就判错 if(Stack.top==0) { //销毁栈 Undermine(&Stack); return false; } //否则出栈,与当前的元素进行匹配 Datatype cur = Out(&Stack); //如果不匹配直接返回,否则继续 if(cur=='(' && *s!=')' || cur=='{' && *s!='}' || cur=='[' && *s!=']' ) { //销毁栈 Undermine(&Stack); return false; } else { s++; } } }(4)如果经过了上面的循环判断,也就是没有返回 false ,可能是下面这种情况:
存在匹配成功的部分,但是字符串走到 \0 了,比如:“()[” 或者“{”栈不为空,返回 false
//判断空栈 bool result=(Stack.top==0); //此时数组判断完,全部符合条件,先销毁栈再返回 Undermine(&Stack); return result;测试用例是不用判断数组没有元素的情况的,也就是说至少一个括号,如图:
