当前位置: 首页 > news >正文

数据结构:链表栈的操作实现( Implementation os Stack using List)

目录

前提:我们已经确定的事实

创建栈 (CreateStack)

入栈 (Push)

出栈 (Pop)

查看栈顶 (Peek)

销毁栈 (DestroyStack)


前提:我们已经确定的事实

在推导操作之前,我们必须清晰地列出我们的出发点。这些是我们为自己设定的、不证自明的规则。

数据结构:栈(Stack)-CSDN博客

  1. 我们的目标:实现一个严格遵守“后进先出”(LIFO)规则的数据容器。

  2. 我们的工具:这次我们不用连续空间,而是用一个个独立的“节点”(Node)通过指针串联起来,也就是链表。

  3. 我们的设计图 (struct)

基本单元 StackNode:每个节点像一节火车车厢,包含两部分:

  • data:存放我们真正的数据(车厢里的货物)。

  • next:一个指针,指向下一节车厢(车厢之间的挂钩)。

typedef struct StackNode {int data;struct StackNode* next; // 这个“钩子”是指向另一个同类型结构体的指针
} StackNode;

管理者 LinkedStack:整个栈就像一个火车站的调度室,它只需要知道哪一节是火车头。

  • top:一个指针,永远指向链表的头节点,也就是我们的“栈顶”。

typedef struct {StackNode* top; // 指向“火车头”的指针
} LinkedStack;

我们的核心约定

  • 栈顶就是链表的头部。所有的“入栈”和“出栈”操作,都只在链表的头部进行。为什么?因为在链表头部增删节点,不需要遍历整个链条,操作非常快,时间复杂度是 O(1),完美契合栈的高效要求。

  • 栈空的状态是 top 指针为 NULL。当调度室里的 top 指针没有指向任何车厢时 (top == NULL),就说明站台上没有火车,栈是空的。

现在,我们拥有了设计图和核心规则,让我们开始推导具体的操作。


创建栈 (CreateStack)

在我们能对一个栈进行任何操作(如入栈、出栈)之前,最首要的一步是什么?

答案是:我们必须先拥有一个栈。一个变量不会凭空出现,我们需要一个明确的“创生”过程。

创建一个“链式栈”意味着什么?

根据我们的设计图,它是一个 LinkedStack 结构体。所以,我们需要在内存中为这个管理者结构体分配空间,创生的第一步是调用 malloc 分配 LinkedStack 大小的内存。

一个刚刚被创造出来的、崭新的栈,它应该是什么状态?

它应该是空的。根据我们的核心约定,空栈的 top 指针必须为 NULL

在分配内存后,必须将 top 指针显式地初始化为 NULL。这至关重要,它为后续所有操作(如判空)提供了基准。

 LinkedStack* CreateStack(void)
{
// 1. 为栈的管理者结构体分配内存
LinkedStack* s = (LinkedStack*)malloc(sizeof(LinkedStack));// 2. 检查内存是否分配成功 (健壮性)
if (s == NULL) {return NULL; // 如果失败,返回 NULL
}// 3. 初始化 top 指针,明确表示这是一个空栈
s->top = NULL;// 4. 返回这个初始化完成的栈的地址
return s;
}

入栈 (Push)

核心问题:我们想把一个新元素 value 放入这个链式栈中,具体要如何操作指针和内存?

这个过程,好比是给一列火车的车头位置,再挂接上一节新的车厢,让它成为新的“火车头”。

第一问:我们要添加的新东西从何而来?—— 创造新节点

  • 我们不能凭空添加数据,必须先有一个能承载数据的“容器”,也就是一个新的 StackNode 节点。

  • 在程序里,这意味着我们必须向操作系统申请一块新的内存,这块内存的大小要刚好能放下 StackNode 结构。

  • 推论 1:入栈的第一步,是调用 malloc 创建一个 StackNode 实例,并用一个临时指针(我们叫它 newNode)指向它。

  • 创建好节点后,我们要把 value 存放到它的 data 域中。

// 申请一块内存给新节点
StackNode* newNode = (StackNode*)malloc(sizeof(StackNode));
// 将数据放入新节点
newNode->data = value;

此时,这个 newNode 就像一节孤零零地停在备用轨道上的新车厢,它和我们的主列车(栈)还没有任何关系。它的 next 指针(挂钩)是悬空的,值是未知的。

第二问:如何将这个新节点连接到现有的栈(链表)上?—— 建立连接

  • 我们的目标是让 newNode 成为新的栈顶(新的火车头)。这意味着,原来的老火车头(由 s->top 指向的节点)现在应该成为第二节车厢。

  • 如何实现这个连接?我们需要让 newNodenext 指针(它的尾部挂钩)连接到原来的火车头。

  • 原来的火车头在哪里?它的地址就保存在 s->top 指针里。

  • 推论 2:我们必须执行一步关键的连接操作:newNode->next = s->top;

  • 详细分析这一步:

    • 如果栈是空的 (s->topNULL):那么 newNode->next 就被赋值为 NULL。这完全正确!因为一个新的、唯一的节点入栈后,它的 next 就应该指向 NULL,表示它后面没有其他节点了。

    • 如果栈不是空的 (s->top 指向一个有效的节点):那么 newNode->next 就指向了原来的栈顶节点。这相当于把新车厢的尾部挂钩,精确地挂在了老火车头的头部。现在,newNode 和整个旧的链表已经串联起来了,形成了一个更长的链条。

第三问:连接建立后,如何“官宣”新的栈顶?—— 更新 top 指针

  • 虽然 newNode 已经连接上了旧的链表,但我们的“调度室”s->top 指针,此刻仍然指向那个旧的火车头。整个系统还认为旧节点是栈顶。

  • 我们必须更新 s->top,让它指向 newNode,正式承认 newNode 的新身份。

  • 推论 3:在连接完成后,执行 s->top = newNode;

  • 操作顺序的重要性:我们必须先执行推论 2 (newNode->next = s->top;),再执行推论 3 (s->top = newNode;)。如果顺序反了会怎么样?

    • s->top = newNode;         // s->top 指针立刻指向了新节点。

    • newNode->next = s->top;

接下来执行这句,s->top 已经是 newNode 了,这就变成了 newNode->next = newNode;,节点自己指向了自己!而原来的整个链表,因为 s->top 指针的移动而彻底丢失了,造成了严重的内存泄漏。

 void Push(LinkedStack* s, int value)
{
// 第1步: 创造一个独立的新节点,并填入数据
StackNode* newNode = (StackNode*)malloc(sizeof(StackNode));
// 健壮性检查:如果内存分配失败
if (newNode == NULL) {printf("错误:内存分配失败!\n");return;
}
newNode->data = value;// 第2步: "连线" - 将新节点的 next 指针指向当前的栈顶
newNode->next = s->top;// 第3步: "搬家" - 更新栈的 top 指针,让它指向新创建的节点
s->top = newNode;
}

出栈 (Pop)

核心问题:我们要如何从链式栈的顶部取出一个元素,并保证内存被正确释放?

这个过程,好比是将火车头从整列火车上拆卸下来,并让第二节车厢成为新的火车头。

  1. 第一问:真的有东西可“拿”吗?—— 思考边界条件

    • 和数组栈一样,我们不能从一个空栈里取东西。

    • 如何判断空?根据我们的“公理”,s->top == NULL 即为空。

    • 推论 1:操作的第一步,必须是检查栈是否为空。如果为空,则操作失败。

  2. 第二问:如果栈不空,如何让“第二节车厢”成为新的“火车头”?—— 更新 top 指针

    • 当前的火车头是 s->top 指向的节点。

    • 第二节车厢在哪里?它被第一节车厢的 next 指针指着,所以它的地址是 s->top->next

    • 要让第二节车厢成为新火车头,我们只需要让“调度室”的 s->top 指针指向它即可。

    • 推论 2:核心操作是 s->top = s->top->next;

  3. 第三问:被拆卸下来的旧火车头(旧栈顶)去哪了?—— 内存管理

    • 如果我们直接执行 s->top = s->top->next;top 指针是更新了,但原来那个旧的栈顶节点,没有任何指针指向它了。它占用的内存就无法被 free,变成了在宇宙中漂浮的“太空垃圾”,也就是内存泄漏。

    • 这个问题如何解决?在我们移动 s->top 指针 之前,我们必须用另一个临时指针,像一个“标记牌”,先挂在要被删除的旧栈顶上,记住它的位置。

    • 推论 3:在更新 s->top 之前,必须先用一个临时指针(如 nodeToFree)保存旧栈顶的地址:StackNode* nodeToFree = s->top;

  4. 第四问:如何将值返回并完成清理?—— 确定操作顺序

    • Pop 操作通常需要返回被移除的元素值。

    • 结合上面的推论,我们得出最严谨的操作序列: 

a. 用临时指针 nodeToFree 记住当前栈顶 s->top

b. 从 nodeToFree 中提前取出要返回的数据 value

c. 更新 s->top 指针,让它指向下一节点 (s->top = s->top->next;s->top = nodeToFree->next;)。至此,栈的逻辑结构已经正确更新。

d. 使用 nodeToFree 指针,释放它所指向的旧栈顶节点的内存 (free(nodeToFree);)。

e. 返回之前保存的 value

int Pop(LinkedStack* s)
{
// 第1步: 检查边界条件
if (s->top == NULL) {printf("错误:栈为空,无法出栈!\n");return -1; // 约定返回一个特殊值代表错误
}// 第2步: 使用临时指针标记要被删除的栈顶节点
StackNode* nodeToFree = s->top;// 第3步: 提前保存要返回的数据
int value_to_return = nodeToFree->data;// 第4步: "断开连接" - 更新栈的 top 指针,使其指向下一个节点
s->top = nodeToFree->next; // 或者 s->top = s->top->next;// 第5步: "回收车厢" - 释放旧栈顶节点的内存
free(nodeToFree);// 第6步: 返回保存的数据
return value_to_return;
}

查看栈顶 (Peek)

核心问题:如何在不破坏任何结构和连接的情况下,仅仅读取栈顶元素的值?

这个操作就像调度员只通过监视器看一眼火车头的编号,而不做任何调度。

  1. 第一问:有东西可“看”吗?

    • 同样,先检查栈是否为空 (s->top == NULL)。

    • 推论 1:必须先进行空栈检查。

  2. 第二问:看哪里?

    • s->top 指向的就是栈顶节点。

    • 该节点的数据存放在 data 域中。

    • 推论 2:要查看的值就是 s->top->data

  3. 第三问:需要改变什么吗?

    • 绝对不需要。Peek 的定义就是非破坏性的。我们不分配内存,不释放内存,也不改变任何指针的指向。

    • 推论 3:Peek 操作仅包含一次读取,无任何写操作。

int Peek(LinkedStack* s)
{
// 第1步: 检查边界条件
if (s->top == NULL) {printf("错误:栈为空!\n");return -1; // 约定返回一个特殊值代表错误
}// 第2步: 直接返回栈顶节点的数据,不进行任何修改
return s->top->data;
}

销毁栈 (DestroyStack)

当一个栈完成了它的历史使命,我们想把它彻底从内存中清除,应该怎么做?

  • 销毁一个链式栈意味着什么?我们需要释放所有由 malloc 创建的内存。这包括:所有 StackNode 节点,以及最初的那个 LinkedStack 管理者结构体。

  • 如何安全地释放所有节点?我们可以一个接一个地释放。这个过程听起来很熟悉... 这不就是不断地执行 Pop 操作吗?Pop 正好负责移除一个节点并释放其内存。

  • 一个优雅的清理所有节点的方法是,循环调用 Pop 直到栈为空。

  • 当所有节点都被 Pop 掉之后,s->top 会变成 NULL。此时还剩下什么?还剩下最初由 CreateStack 创建的、现在已经空了的 LinkedStack 结构体 s 本身。

  • 最后一步,是 free(s),释放管理者结构体占用的内存。

void DestroyStack(LinkedStack* s)
{
// 1. 通过不断出栈来清空并释放所有节点
while (!IsEmpty(s)) {Pop(s); // Pop内部会处理free
}// 2. 释放栈管理者结构体本身
free(s);
}
http://www.dtcms.com/a/327756.html

相关文章:

  • LDAP 登录配置参数填写指南
  • 文件io ,缓冲区
  • 【智慧城市】2025年湖北大学暑期实训优秀作品(3):基于WebGIS的南京市古遗迹旅游管理系统
  • 简单的双向循环链表实现与使用指南
  • 小黑课堂计算机一级Office题库安装包2.93_Win中文_计算机二级考试_安装教程
  • 使用shell脚本执行需要root权限操作,解决APK只有系统权限问题
  • mysql参数调优之 sync_binlog (二)
  • 计算机网络摘星题库800题笔记 第2章 物理层
  • 防御保护11
  • Flutter GridView的基本使用
  • 17、CryptoMamba论文笔记
  • 基于大数据的在线教育评估系统 Python+Django+Vue.js
  • scikit-learn/sklearn学习|岭回归python代码解读
  • CVPR 2025丨机器人如何做看懂世界
  • 全面解析远程桌面:功能实现、性能优化与安全防护全攻略
  • 第十篇:3D模型性能优化:从入门到实践
  • AWT与Swing深度对比:架构差异、迁移实战与性能优化
  • 自己动手造个球平衡机器人
  • 基于 gRPC 的接口设计、性能优化与生产实践
  • open Euler--单master部署集群k8s
  • 【能耗监控数据聚合处理策略应用】
  • IIS 多用户环境中判断服务器是否为开发用电脑,数据状态比较
  • GeoScene 空间大数据产品使用入门(2)数据资源
  • 英伟达被约谈?国产替代迎来新机遇
  • 中国网络安全处罚综合研究报告(2020-2025)
  • 项目部署总结
  • iceberg FlinkSQL 特性
  • 什么是分布式,它有哪些功能和应用场景
  • 如何在idea中导入外来文件
  • 呼吸道病原体检测需求激增,呼吸道快检试纸条诊断试剂生产厂家推荐,默克全链解决方案助IVD企业把握百亿风口