数据结构:链表栈的操作实现( Implementation os Stack using List)
目录
前提:我们已经确定的事实
创建栈 (CreateStack)
入栈 (Push)
出栈 (Pop)
查看栈顶 (Peek)
销毁栈 (DestroyStack)
前提:我们已经确定的事实
在推导操作之前,我们必须清晰地列出我们的出发点。这些是我们为自己设定的、不证自明的规则。
数据结构:栈(Stack)-CSDN博客
-
我们的目标:实现一个严格遵守“后进先出”(LIFO)规则的数据容器。
-
我们的工具:这次我们不用连续空间,而是用一个个独立的“节点”(Node)通过指针串联起来,也就是链表。
-
我们的设计图 (
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
指向的节点)现在应该成为第二节车厢。 -
如何实现这个连接?我们需要让
newNode
的next
指针(它的尾部挂钩)连接到原来的火车头。 -
原来的火车头在哪里?它的地址就保存在
s->top
指针里。 -
推论 2:我们必须执行一步关键的连接操作:
newNode->next = s->top;
。 -
详细分析这一步:
-
如果栈是空的 (
s->top
是NULL
):那么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)
核心问题:我们要如何从链式栈的顶部取出一个元素,并保证内存被正确释放?
这个过程,好比是将火车头从整列火车上拆卸下来,并让第二节车厢成为新的火车头。
-
第一问:真的有东西可“拿”吗?—— 思考边界条件
-
和数组栈一样,我们不能从一个空栈里取东西。
-
如何判断空?根据我们的“公理”,
s->top == NULL
即为空。 -
推论 1:操作的第一步,必须是检查栈是否为空。如果为空,则操作失败。
-
-
第二问:如果栈不空,如何让“第二节车厢”成为新的“火车头”?—— 更新
top
指针-
当前的火车头是
s->top
指向的节点。 -
第二节车厢在哪里?它被第一节车厢的
next
指针指着,所以它的地址是s->top->next
。 -
要让第二节车厢成为新火车头,我们只需要让“调度室”的
s->top
指针指向它即可。 -
推论 2:核心操作是
s->top = s->top->next;
。
-
-
第三问:被拆卸下来的旧火车头(旧栈顶)去哪了?—— 内存管理
-
如果我们直接执行
s->top = s->top->next;
,top
指针是更新了,但原来那个旧的栈顶节点,没有任何指针指向它了。它占用的内存就无法被free
,变成了在宇宙中漂浮的“太空垃圾”,也就是内存泄漏。 -
这个问题如何解决?在我们移动
s->top
指针 之前,我们必须用另一个临时指针,像一个“标记牌”,先挂在要被删除的旧栈顶上,记住它的位置。 -
推论 3:在更新
s->top
之前,必须先用一个临时指针(如nodeToFree
)保存旧栈顶的地址:StackNode* nodeToFree = s->top;
。
-
-
第四问:如何将值返回并完成清理?—— 确定操作顺序
-
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)
核心问题:如何在不破坏任何结构和连接的情况下,仅仅读取栈顶元素的值?
这个操作就像调度员只通过监视器看一眼火车头的编号,而不做任何调度。
-
第一问:有东西可“看”吗?
-
同样,先检查栈是否为空 (
s->top == NULL
)。 -
推论 1:必须先进行空栈检查。
-
-
第二问:看哪里?
-
s->top
指向的就是栈顶节点。 -
该节点的数据存放在
data
域中。 -
推论 2:要查看的值就是
s->top->data
。
-
-
第三问:需要改变什么吗?
-
绝对不需要。
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);
}