【数据结构入坑指南(五)】--《手把手实现栈结构:附带完整代码与注释,深度揭秘数组实现香在哪?》

【数据结构入坑指南(五)】--《手把手实现栈结构:附带完整代码与注释,深度揭秘数组实现香在哪?》

🔥@晨非辰Tong:个人主页 

👀专栏:《C语言》《数据结构与算法》《数据结构与算法刷题集》

💪学习阶段:C语言、数据结构与算法初学者

⏳“人理解迭代,神理解递归。”


引言:当熟练驾驭了结构复杂、指针纵横的双链表后,是否意味着需要更复杂的数据结构来挑战自己?恰恰相反。接下来登场的「栈」,它摒弃了链表的灵活多变,只在一端固守“后进先出”的准则。看似简单的约束,实则培养精准操作与算法思维的绝佳“内功心法”,更是平稳过渡到「队列」的坚实桥梁。

目录

一、栈的概念和结构

1.1  栈的定义

1.2  栈的基本结构

问题?--栈的底层实现是数组(顺序表)呢?还是链表呢?

二、栈的初始化、销毁

2.1  初始化

2.2  销毁

三、栈基本功能实现

3.1  入栈(在栈顶放入数据)

3.1.1  定义判空(准备工作)

3.2  出栈(栈顶)

3.3  取栈顶元素

3.4  获取有效数据个数

四、实现栈的完整代码

4.1  Stack.h

4.2  Stack.c

4.3  test.c


一、栈的概念和结构

1.1  栈的定义

--栈:一种特殊的线性表,只允许在固定的一端进行插入和删除操作。进行数据插入和删除的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守“后进先出”的原则。

--其中,插入、删除分别称为:

  • 压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶
  • 出栈:栈的删除操作叫做出栈,出数据也在栈顶
可以将栈的结构形象的看成水杯,在进行接水(进栈)在杯顶(栈顶),进行倒水也是(出栈)在杯顶(栈顶)。

1.2  栈的基本结构

问题?--栈的底层实现是数组(顺序表)呢?还是链表呢?

        :一般栈的实现,数组、链表都可以。但是对结构、复杂度的考虑:选择数组——>将数组的尾部作为栈顶,数组首部作为栈底,而且尾部操作时间复杂度都是O(1)

        --链表其实也可以实现,只不过是在头部进行操作,且时间复杂度:O(1)

--那为什么数组实现更好呢?

        :时间一样,那就看空间复杂度——>

  • 实现顺序表时,每次申请空间(比如int)虽然是呈2倍增加,但是申请的是4个字节的倍数。
  • 但链表则是每插入一个数据要申请一个节点大小的空间,空间较数组而言有点大。
Stack.h //定义栈的结构 typedef int STDataType; typedef struct Stack { STDataType* arr; int top; //指向栈顶的位置---刚好就是栈中有效数据个数 int capacity;//栈的空间大小 }ST;

--为什么底层实现与顺序表相同,不用size表示有效数据个数?

        :用top更能形象的表示这里栈顶,根据下标,则有效数据个数在top-1位。


二、栈的初始化、销毁

2.1  初始化

--在正式使用栈时,对栈结构进行初始化:

Stack.c #include"Stack.h" //初始化 void STInit(ST* ps) { ps->arr = NULL; ps->top = ps->capacity = 0; }
--因为要改变栈的结构(在定义结构时并没有进行任何赋值操作),所以要进行传址调用,用二级指针接收。

--初始时,指针指向空(还没有开辟空间),其余指向0(栈顶、空间根据下标在0的位置)。
test.c #include"Stack.h" void test01() { ST st; STInit(&st);//改变结构 } int main() { test01(); return 0; }

--定义变量不是指针变量?

        :这就是与链表的区别——>栈底层是数组,其存储数据的空间是连续的,因此不需要额外定义指针变量指向空间;而链表节点的空间不一定连续,就需要通过指针进行地址链接。

--传地址,形参的改变影响实参。

2.2  销毁

--销毁操作与初始化相同,只不过多加一步-->判空,如果数组 arr 为空,代表没有进行开辟空间。

Stack.h #include"Stack.h" //销毁 void STDesTroy(ST* ps) { if (ps->arr)//为空,没进行初始化 { free(ps->arr); } ps->arr = NULL; ps->top = ps->capacity = 0; }
--销毁就是将后续进行的入栈(放入数据)等操作重置:将空间释放,指针重新指向空,其余置零。

三、栈基本功能实现

--对于各种功能的实现,其算法原理与顺序表相关算法大同小异。 

3.1  入栈(在栈顶放入数据)

Stack.c #include"Stack.h" //入栈——栈顶 void STPush(ST* ps, STDataType x) { assert(ps); //判断空间是否足够 if (ps->top == ps->capacity) { //增容 int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity; //临时变量,防止扩容失败 STDataType* tmp = (STDataType*)realloc(ps->arr, newCapacity * sizeof(STDataType)); if (tmp == NULL) { perror("realloc fail!"); exit(1); } ps->arr = tmp; ps->capacity = newCapacity; } //空间足够 ps->arr[ps->top++] = x; }

--空间的判断:与顺序表一致,当 top = capacity 时,代表后面没有位置了,采用 *2 倍扩容。

--断言ps:传的结构体变量指向这块结构体空间,不能传空地址。

test.c #include"Stack.h" void test01() { ST st; STInit(&st);//改变结构 STPush(&st, 1); STPush(&st, 2); STPush(&st, 3); STPush(&st, 4); } int main() { test01(); return 0; }

3.1.1  定义判空(准备工作)

--在实现出栈之前,我们先要实现一个判空操作的接口。

//栈是否为空 bool STEmpty(ST* ps) { assert(ps); return ps->top == 0; }

--top当处于下标0的位置,代表没有数据,栈为空。

3.2  出栈(栈顶)

--出栈与实现顺序表的删除算法一样,只用将栈顶缩减,后续的入栈就会将旧数据覆盖。

Stach.c #include"Stack.h" //出栈--栈顶 void STPop(ST* ps) { assert(!STEmpty); ps->top--;//栈顶缩减一位 }

3.3  取栈顶元素

--说是栈顶元素,看上面的图示,其实就是下标 top - 1 位的元素。

Stack.c #include"Stack.h" //取栈顶元素 STDataType STTop(ST* ps) { assert(!STEmpty(ps)); return ps->arr[ps->top - 1];//返回栈顶元素,实际是下标top-1位 }
test.c #include"stack.h" void test1() { ST ps; STInit(&ps); //入栈 STPush(&ps, 1); STPush(&ps, 2); STPush(&ps, 3); STPush(&ps, 4); while (!STEmpty(&ps)) { //取栈顶元素,打印 STDataType top = STTop(&ps); printf("%d ", top); //出栈 STPop(&ps); } printf("\n"); } int main() { test1(); }
 

代码中的循环就相当于循环出栈顺带将栈顶元素打印出来。

--根据图示,就体现了“先进后出”或“后进先出”的原则。

3.4  获取有效数据个数

--获取有效数据个数,只需要在入栈操作完成后,返回top值就行了。

Stack.c #include"Stack.h" //获取栈中有效元素个数 int STSize(ST* ps) { assert(ps); return ps->top; }

四、实现栈的完整代码

--至此,栈的实现已经全部完成,部分实现的思路与顺序表大差不差,感兴趣的可以看看博主这几篇有关顺序表的的博文:

4.1  Stack.h

#include<stdio.h> #include<stdlib.h> #include<stdbool.h> #include<assert.h> //定义栈的结构 typedef int STDataType; typedef struct Stack { STDataType* arr; int top; //指向栈顶的位置---刚好就是栈中有效数据个数 int capacity;//栈的空间大小 }ST; //初始化 void STInit(ST* ps); //销毁 void STDesTroy(ST* ps); //入栈 void STPush(ST* ps, STDataType x); //出栈--栈顶 void STPop(ST* ps); //判空 bool STEmpty(ST* ps); //取栈顶元素 STDataType STTop(ST* ps); //获取栈中有效元素个数 int STSize(ST* ps); 

4.2  Stack.c

#include"Stack.h" //初始化 void STInit(ST* ps) { ps->arr = NULL; ps->top = ps->capacity = 0; } //销毁 void STDesTroy(ST* ps) { if (ps->arr)//为空,没进行初始化 { free(ps->arr); } ps->arr = NULL; ps->top = ps->capacity = 0; } //入栈--栈顶 void STPush(ST* ps, STDataType x) { assert(ps); //判断空间是否足够 if (ps->top == ps->capacity) { //增容 int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity; STDataType* tmp = (STDataType*)realloc(ps->arr, newCapacity * sizeof(STDataType)); if (tmp == NULL) { perror("realloc fail!"); exit(1); } ps->arr = tmp; ps->capacity = newCapacity; } //空间足够 ps->arr[ps->top++] = x; } //栈是否为空 bool STEmpty(ST* ps) { assert(ps); return ps->top == 0; } //出栈--栈顶 void STPop(ST* ps) { assert(!STEmpty(ps)); ps->top--;//栈顶缩减一位 } //取栈顶元素 STDataType STTop(ST* ps) { assert(!STEmpty(ps)); return ps->arr[ps->top - 1];//返回栈顶元素,实际是下标top-1位 } //获取栈中有效元素个数 int STSize(ST* ps) { assert(ps); return ps->top; }

4.3  test.c

#include"stack.h" void test1() { ST ps; STInit(&ps); //入栈 STPush(&ps, 1); STPush(&ps, 2); STPush(&ps, 3); STPush(&ps, 4); //打印有效数据个数 printf(":%d\n", STSize(&ps)); while (!STEmpty(&ps)) { //取栈顶元素,打印 STDataType top = STTop(&ps); printf("%d ", top); //出栈 STPop(&ps); } printf("\n"); } int main() { test1(); }

回顾:

《面试高频数据结构:从单链到双链的进阶,读懂“双向奔赴”的算法之美与效率权衡》

《超越单链表的局限:双链表“哨兵位”设计模式,如何让边界处理代码既优雅又健壮?》
结语:至此,我们对「栈」的探索已告一段落。它用“后进先出”的简单规则,为我们展示了数据结构中“约束”的魅力。从双链表的灵活到栈的专一,我们不仅学会了一种新结构,更学会了一种聚焦核心问题的思维。当栈的操作已内化于心,我们便做好了准备,去迎接下一个遵循“先进先出”规则的伙伴——队列。届时,一个更宏大的数据结构世界将在我们面前展开。

Read more

Flutter 三方库 objectbox_generator — 自动化构建鸿蒙极速 NoSQL 数据库映射(适配鸿蒙 HarmonyOS Next ohos)

Flutter 三方库 objectbox_generator — 自动化构建鸿蒙极速 NoSQL 数据库映射(适配鸿蒙 HarmonyOS Next ohos)

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net。 Flutter 三方库 objectbox_generator — 自动化构建鸿蒙极速 NoSQL 数据库映射(适配鸿蒙 HarmonyOS Next ohos) 在高性能移动应用开发中,本地数据的持久化存储效率往往是决定用户感知流畅度的木桶短板。传统的 SQLite 虽然结构化程度高,但在处理大规模对象关系映射(ORM)时,复杂的 SQL 拼接和反射解析往往会成为性能瓶颈。 ObjectBox 作为一个专为移动设备打造的、跨平台的超高速 NoSQL 数据库,已经成为了许多追求极致体验开发者的首选。而在 Flutter for OpenHarmony 开发中,配合 objectbox_generator,我们可以通过注解驱动的自动化流程,掌握这套高性能数据库的核心用法。 ⚠️ 鸿蒙适配现状提示:截至本文撰写时,ObjectBox 的 Dart 插件尚未提供官方的 OpenHarmony

By Ne0inhk
【MySQL基础】(1):MySQL的安装

【MySQL基础】(1):MySQL的安装

✅ 适用人群:刚接触 Linux 和数据库的新手 ✅ 目标:快速装好 MySQL,用 root 用户练习 SQL,无需复杂权限配置 ✅ 系统要求:Ubuntu 20.04 / 22.04 / 24.04 LTS(阿里云、腾讯云、AWS EC2 等均可) 🔧 第一步:登录你的云服务器 1. 使用 SSH 工具(如 Xshell、FinalShell、或 macOS/Linux 的终端)连接到你的 Ubuntu 服务器。 2. 先确认你是普通用户(不是 root),但拥有 sudo 权限(大多数云服务器默认如此)

By Ne0inhk
Rust异步编程高级模式:并发控制、超时机制与实战架构

Rust异步编程高级模式:并发控制、超时机制与实战架构

Rust异步编程高级模式:并发控制、超时机制与实战架构 一、异步并发控制:Semaphore、Mutex、RwLock的异步版本 1.1 为什么需要异步同步原语? 💡在同步编程中,我们使用std::sync::Mutex、std::sync::RwLock、std::sync::Semaphore等同步原语来控制并发访问。这些原语在多线程场景下非常有效,但在异步编程中,它们会导致任务阻塞,影响性能。 异步同步原语通过await关键字暂停任务,而不是阻塞线程,从而提高了CPU利用率。Tokio提供了一系列异步同步原语,如tokio::sync::Mutex、tokio::sync::RwLock、tokio::sync::Semaphore。 1.2 异步Mutex(互斥锁) 异步Mutex的使用方式与标准库的类似,但需要使用await来获取锁。 usetokio::sync::Mutex;usestd::sync::Arc;

By Ne0inhk
告别手写SQL?Cursor智能生成实战指南与避坑技巧

告别手写SQL?Cursor智能生成实战指南与避坑技巧

文章目录 * 前言 * 一、 原理揭秘:Cursor 为什么比 ChatGPT 更懂你的数据库? * 1. 核心架构组件 * 2. 架构流程图解 * 二、 实战教学:从自然语言到高质量 SQL * 场景一:自然语言生成 SQL(Text-to-SQL) * 场景二:复杂 SQL 生成(窗口函数、CTE) * 场景三:SQL 转自然语言(代码解释与优化建议) * 三、 支持范围与边界:用实例说话 * 案例 1:ClickHouse 物化视图生成的“陷阱” * 案例 2:MongoDB 聚合管道的缺失阶段 * 小结 * 四、 避坑指南:如何让生成准确率达到 99%?(附真实案例) * 技巧一:拒绝“

By Ne0inhk