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

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

前引:本篇将由小编与大家一起复习 、队列 的知识点,栈、队列的顺序、链式结构各个缺点好处,如何实现、对于一般的增删查找此篇文章一定再详细不过!对代码的注释、何时需要判断、特殊情况,白话文版一解到底,彻底了解栈与队列。文章末尾还精心选择了几道例题,小编同样会精心讲解,如果伙伴们被感动了的话!可否一键三连~好了,正文开始~

目录

 知识点速览

栈的存储结构分析

 栈的基本操作

结构体定义 

初始化栈

 判断栈空

 入栈

 读取栈元素

出栈

 销毁栈

队列

队列的结构分析

 队列的基本操作

结构体定义

初始化队列

入队列

出队列

获取队尾元素

获取队头元素

判断队空

销毁队列 

栈和队列OJ题(典型)


 知识点速览

何为栈?栈是一种线性结构,只允许在一端进行插入和拿出元素的数据结构。

压栈(也叫入栈):插入元素

出栈:拿出元素

栈顶:进行插入和拿出元素的一端

栈底:固定的,不允许进行插入和拿出元素

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

栈的存储结构分析

首先栈可以用两种结构来实现,一种是顺序栈,也就是用数组实现;另一种是链式栈,用链表实现。这两种结构栈我们应该选择哪一种来实现好?综合考虑,我们优先选择顺序栈

首先是顺序栈:

顺序栈出栈、入栈都很方便,栈顶指针也方便指向,有一个小缺点:扩容可能导致空间浪费

其次是链式栈:

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

 栈的基本操作

初始化栈    判断栈空   入栈   读取栈元素   出栈   销毁栈 

结构体定义 

顺序栈只需要一个栈顶指针、一个空间指针即可。在初始化时给空间指针开辟空间,这里小编建议可以额外设置一个空间上限,方便以后扩容

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;

         测试用例是不用判断数组没有元素的情况的,也就是说至少一个括号,如图:

Read more

面向数据工程的 SQL 与 Python 代码自动生成:6 款大模型深度评测

面向数据工程的 SQL 与 Python 代码自动生成:6 款大模型深度评测

面向数据工程的 AI 代码助手:6 款 SQL 与 Python 工具深度评测 摘要:本文对 GitHub Copilot、Cursor、Claude、ChatGPT、Gemini Code Assist 和 Amazon CodeWhisperer 六款主流 AI 代码助手进行了深度评测,重点考察它们在数据工程工作流(如 SQL 转换、Python ETL、dbt 模型生成等)中的表现。作者详细对比了各工具的优缺点、适用场景及成本效益,为个人开发者和数据团队提供了切实可行的选型建议,强调了“混合使用”策略的优势。 免责声明:本评测反映了 2026 年 1 月时的工具能力。AI 代码助手发展迅速,功能、定价和模型能力频繁变化。

By Ne0inhk

嵌入式软件代码架构详解,超清晰图解为什么需要软件架构,以及告诉你怎么实现软件架构

我希望你能够带着几个问题进入到下面的文章中,我会用生动的例子告诉你为什么需要软件架构,以及一个简单的软件架构是什么样子的。在看文章的过程中,你要有意识的思考这几个问题,希望看完这篇文章,你就能回答出下面几个问题了。 1.为什么需要软件架构? 2.好的软件架构有哪些标准,能够解决掉什么问题? 3.软件架构长什么样子?文章看完了你能够画出来嘛? 一、 告别“面条代码”,嵌入式为何更需要软件架构? 1.1 从两个场景说起 当你拿到一块新的开发板,兴致勃勃地开始你的嵌入式项目时,是不是经常这样开始你的main.c? 场景A(新手期):功能堆砌的“面条代码” 这就是经典的“面条代码”(Spaghetti Code)——所有逻辑像一碗面条一样缠绕在一起。 它有些什么样的问题呢? * main函数无限膨胀: 所有功能都堆在while(1)循环里,代码越来越长,越来越难阅读。 * “牵一发而动全身”: 你想修改按键的逻辑,可能会影响到ADC采样;想移除蜂鸣器功能,得小心翼翼地从一大坨代码里找出所有相关行。 * 高度耦合: 业务逻辑(按键控制LED)

By Ne0inhk
【Kafka进阶篇】深入Kafka内部:日志存储的设计思路,藏着中间件高性能的真相

【Kafka进阶篇】深入Kafka内部:日志存储的设计思路,藏着中间件高性能的真相

🍃 予枫:个人主页 📚 个人专栏: 《Java 从入门到起飞》《读研码农的干货日常》 💻 Debug 这个世界,Return 更好的自己! 引言 做分布式开发的同学,几乎都用过Kafka,但多数人只停留在“生产者发消息、消费者收消息”的表层使用,很少深究:百万级消息并发下,Kafka如何快速定位目标消息?底层的.log、.index、.timeindex文件各司其职,又是如何配合实现高效读写的?今天就从物理层面拆解Kafka日志存储与索引机制,吃透这部分,不仅能搞定面试难点,更能在生产环境中精准优化Kafka性能。 文章目录 * 引言 * 一、前言:为什么要搞懂Kafka日志与索引机制? * 二、核心前提:Kafka日志的“分段存储”设计 * 三、深度拆解:三种核心文件的作用与结构 * 3.1 .log文件:消息的“真正存储载体” * 关键细节: * 3.2

By Ne0inhk