引言
栈是一种线性数据结构,遵循“后进先出”(LIFO)原则,即最后添加的元素最先被移除。栈的实现方式有多种,其中基于数组和基于链表是两种常见方法。本文将详细介绍基于链表的栈实现,并与基于数组的栈进行对比,帮助初学者理解两种实现方式的区别和适用场景。
栈的操作
在深入实现之前,先定义栈的关键操作:
-
Push(入栈):在栈顶添加元素。
-
Pop(出栈):移除栈顶元素。
-
Peek(查看栈顶):查看栈顶元素但不移除。
-
IsEmpty(是否为空):检查栈是否为空。
-
IsFull(是否已满):检查栈是否已达到最大容量(对固定大小的栈相关)。
基于数组的栈实现
基于数组的栈使用固定大小的数组存储元素,top 变量跟踪栈顶元素的索引,初始为 -1 表示空栈。
#define MAXSIZE 1024 // 栈的最大容量
typedef struct {
int data[MAXSIZE]; // 存储元素的数组
int top; // 栈顶元素的索引,初始为-1表示空栈
} Stack;
// 初始化栈
void initStack(Stack* s) {
s->top = -1; // 设置top为-1,表示栈为空
}
// 入栈
int push(Stack* s, int element) {
if (s->top + 1 >= MAXSIZE) {
return 0; // 栈已满,返回0表示失败
}
s->top++; // top加1,指向下一个位置
s->data[s->top] = element; // 在top位置放元素
return 1; // 成功返回1
}
// 出栈
int pop(Stack* s) {
if (s->top == -1) {
return -1; // 栈为空,返回-1表示失败
}
int element = s->data[s->top]; // 获取栈顶元素
s->top--; // top减1,移除元素
return element; // 返回移除的元素
}
// 查看栈顶
int peek(Stack* s) {
if (s->top == -1) {
return -1; // 栈为空,返回-1
}
return s->data[s->top]; // 返回栈顶元素
}
// 是否为空
int isEmpty(Stack* s) {
return s->top == -1; // 如果top为-1,返回真
}
// 是否已满
int isFull(Stack* s) {
return s->top + 1 >= MAXSIZE; // 如果top+1等于最大容量,返回真
}
为什么 last = -1 表示栈为空?
last 跟踪最后一个元素的索引。当 last = -1 时,说明没有元素,栈为空。-1 是一个特殊值,因为数组索引从 0 开始,-1 表示“没有元素”,与标准实践一致。
p->last 是否是栈的长度?
不是,p->last 是最后一个元素的索引,栈的长度是 p->last + 1。比如:
-
last = -1,长度 = 0(空栈)。 -
last = 0,长度 = 1(一个元素)。 -
last = 1,长度 = 2(两个元素)。
return -1 是什么意思?
-
在
get函数中,return -1表示没找到元素。 -
在
find函数中,return -1表示位置无效。 -
-1 是一个特殊值,表示操作失败,因为索引不能为负。
基于链表的栈实现
基于链表的栈使用节点集合,每个节点包含数据和指向下一个节点的指针,链表的头部(栈顶)由 top 指针管理。这种方法允许动态大小,因为节点按需分配。
#include <stdio.h>
#include <stdlib.h>
// 定义节点结构
typedef struct Node {
int data; // 存储数据
struct Node* next; // 指向下一个节点的指针
} Node;
// 定义栈结构
typedef struct {
Node* top; // 栈顶指针,初始为 NULL
} LinkedListStack;
// 初始化栈
void initLinkedListStack(LinkedListStack* s) {
s->top = NULL; // 栈为空时,栈顶指针为 NULL
}
// 入栈操作
int push(LinkedListStack* s, int element) {
Node* newNode = (Node*)malloc(sizeof(Node)); // 分配新节点内存
if (newNode == NULL) {
return 0; // 内存分配失败,返回 0 表示失败
}
newNode->data = element; // 设置新节点的数据
newNode->next = s->top; // 新节点的 next 指向当前栈顶
s->top = newNode; // 更新栈顶指针为新节点
return 1; // 成功返回 1
}
// 出栈操作
int pop(LinkedListStack* s) {
if (s->top == NULL) {
return -1; // 栈为空,返回 -1 表示失败
}
Node* temp = s->top; // 保存当前栈顶节点
int element = temp->data; // 获取栈顶数据
s->top = s->top->next; // 更新栈顶指针为下一个节点
free(temp); // 释放原栈顶节点的内存
return element; // 返回出栈的元素
}
// 查看栈顶元素
int peek(LinkedListStack* s) {
if (s->top == NULL) {
return -1; // 栈为空,返回 -1
}
return s->top->data; // 返回栈顶元素的数据
}
// 检查栈是否为空
int isEmpty(LinkedListStack* s) {
return s->top == NULL; // 如果栈顶指针为 NULL,栈为空
}
-
Node结构体:
-
int data:存储节点中的数据,这里是整数类型,但可以根据需求定义其他类型。 -
struct Node* next:一个指针,指向下一个Node结构体。如果是链表的最后一个节点,next指向NULL,表示链表结束。
-
-
LinkedListStack结构体:
-
Node* top:一个指针,指向栈顶的节点(即链表的头节点)。当栈为空时,top为NULL。
-
详细解释:
-
每个
Node就像一个书架上的书,data是书的内容,next是书的标签,写着“下一本书在哪里”。 -
LinkedListStack是整个书架的管理者,top是当前你拿的书的入口,指向最上面的书。
变量的含义
以下是代码中变量的详细含义:
-
data:Node结构体中的成员,存储节点的数据,比如一个整数。这是节点的核心内容,代表栈中存储的元素。 -
next:Node结构体中的成员,是一个指针,指向下一个Node。如果当前节点是最后一个,next为NULL,表示链表结束。 -
top:LinkedListStack结构体中的成员,是一个指针,指向栈顶的Node。当栈为空时,top为NULL。
通俗解释:
-
data是书的内容,next是书的“下一页”标签,top是你当前拿的书的入口。
NULL的含义
NULL是C语言中的一个宏,表示空指针(Null Pointer),不指向任何有效内存地址。它用于表示“没有东西”或“不存在”。
在链表栈中,NULL有以下具体用途:
-
栈为空:当栈中没有元素时,
LinkedListStack的top被设置为NULL,表示栈顶不存在。 -
链表结束:每个
Node的next指针指向下一个节点,当到达最后一个节点时,next被设置为NULL,表示没有更多节点。
详细解释:
-
当
top == NULL时,说明栈里没有节点,相当于书架上没有书。 -
当某个节点的
next == NULL时,说明这是最后一个节点,后面没有书了。
->操作符的解释
->操作符是C语言中用于通过指针访问结构体成员的运算符。其语法为:如果有一个指针ptr指向结构体S,ptr->member表示通过ptr访问S中的member成员。
在链表栈中,->操作符频繁用于访问节点的成员,因为节点是通过指针连接的。
单个->的例子:
-
s->top:s是一个指向LinkedListStack的指针,s->top访问的是LinkedListStack中的top成员(即栈顶指针)。 -
node->data:如果node是一个指向Node的指针,node->data访问的是该节点中的数据。
两个->的解释:
-
当你看到像
s->top->data这样的表达式时,它表示:-
s->top:先通过s访问LinkedListStack中的top指针(它是一个指向Node的指针)。 -
(s->top)->data:再通过top指针访问Node结构体中的data成员。
-
-
因此,
s->top->data的意思是“获取栈顶节点中的数据”。 -
另一个例子:
s->top->next-
s->top:获取栈顶指针。 -
s->top->next:通过栈顶指针访问Node结构体中的next成员(即下一个节点的指针)。 -
这在出栈操作中常用,例如:
-
s->top = s->top->next;这里,
s->top->next获取栈顶节点的下一个节点,然后将栈顶指针更新为这个下一个节点,从而移除原来的栈顶节点。
-
通俗解释:
-
两个
->就像是“先找到一个东西,再从这个东西里找另一个东西”。比如s->top->data是“先找到栈顶(小助手),然后看栈顶上的数据(机器人的手)”。
操作示例与两个->的实际使用
为了让你更清楚地理解,让我们看一下链表栈的基本操作(入栈和出栈),特别关注两个->的用法:
-
入栈(Push):
-
int push(LinkedListStack* s, int element) { Node* newNode = (Node*)malloc(sizeof(Node)); // 创建新节点 if (newNode == NULL) return 0; // 内存分配失败 newNode->data = element; // 设置新节点的数据 newNode->next = s->top; // 新节点的next指向当前栈顶 s->top = newNode; // 更新栈顶为新节点 return 1; // 成功 } - 这里没有两个
->,但可以看到s->top和newNode->next的用法。 -
出栈(Pop):
-
int pop(LinkedListStack* s) { if (s->top == NULL) return -1; // 栈为空 Node* temp = s->top; // 保存当前栈顶 int element = temp->data; // 获取栈顶数据 s->top = s->top->next; // 更新栈顶为下一个节点,这里用了两个`->` free(temp); // 释放内存 return element; // 返回出栈的元素 }-
在
s->top = s->top->next;中,s->top->next是一个两个->的例子:-
s->top:获取栈顶指针。 -
s->top->next:通过栈顶指针访问next成员,获取下一个节点的指针。
-
-
性能与限制
-
基于链表的栈:
-
优点:动态大小,无浪费空间,适合未知大小。
-
缺点:动态内存分配可能稍慢,需手动管理内存。
-
-
两个
->的频率:在栈操作中,两个->主要出现在访问栈顶节点的成员(如s->top->next),通常用于更新栈顶或遍历。
890

被折叠的 条评论
为什么被折叠?



