


在数据结构中,栈的 “后进先出” 特性广为人知,上一篇我们聊了基于数组的顺序栈,它虽高效但容量固定,容易出现栈溢出或内存浪费的问题。今天就来介绍一种更灵活的实现方式 ——栈的链式结构(简称链栈),它像一串可自由增减的 “珠子”,能按需分配内存,彻底解决顺序栈的容量困扰,用通俗的例子和代码带你轻松搞懂。
栈的链式结构,本质是用单链表来实现栈的功能。单链表由一个个 “节点” 组成,每个节点包含存储的数据和指向 next 节点的指针,节点之间通过指针串联,无需连续的内存空间。
你可以把链栈想象成一串 “串珠”:
和顺序栈相比,链栈最大的优势是容量不固定,需要存多少元素就创建多少节点,不用提前预留内存,也不会出现栈满溢出的情况。
要实现链栈,首先得定义 “节点” 和 “栈” 的结构,这是链栈的基础。用 C 语言实现如下:
typedef int elemtype; // 栈中元素类型(这里以int为例)
// 定义链栈的节点结构
typedef struct StackNode {
elemtype data; // 节点存储的数据
struct StackNode *next; // 指向next节点的指针
} StackNode;
// 定义链栈结构(用表头指针标记栈顶)
typedef struct {
StackNode *top; // 栈顶指针,指向链表的表头节点
int size; // 栈的当前元素个数(可选,方便统计长度)
} LinkStack;
关键说明:
链栈的核心操作和顺序栈一致(初始化、入栈、出栈、取栈顶、判空),但实现方式换成了链表的节点操作,核心是 “操作栈顶指针和节点的连接关系”,我们结合例子逐一拆解。
初始化的目的是让栈处于 “空状态”,此时没有任何节点,栈顶指针指向 NULL,元素个数为 0。
// 链栈初始化
void InitLinkStack(LinkStack *L) {
L->top = NULL; // 栈顶指针置空,无节点
L->size = 0; // 元素个数初始为0
}
这就像准备一串空的 “串珠”,还没穿任何珠子,丝线的起点(对应栈顶指针)是空的。
入栈就是在链表表头(栈顶)新增一个节点,让新节点成为新的栈顶,步骤是 “创建新节点→连接旧栈顶→更新栈顶指针”。
// 链栈入栈(e为要入栈的元素)
int Push(LinkStack *L, elemtype e) {
// 1. 创建新节点,分配内存
StackNode *newNode = (StackNode *)malloc(sizeof(StackNode));
if (newNode == NULL) { // 内存分配失败(比如内存不足)
printf("入栈失败,内存不足!\n");
return 0;
}
// 2. 新节点存储数据,next指针指向旧栈顶
newNode->data = e;
newNode->next = L->top; // 新节点连在旧栈顶前面
// 3. 更新栈顶指针,新节点成为新栈顶
L->top = newNode;
L->size++; // 元素个数+1
return 1;
}
举例:往空栈中依次入栈 10、20、30
此时栈的结构是:30(栈顶)→20→10(栈底),最后入栈的 30 处在最顶端。
出栈是从链表表头(栈顶)移除节点,取出节点数据,步骤是 “判断栈空→保存栈顶数据→更新栈顶指针→释放旧节点内存”。
// 链栈出栈(e用于存储出栈的元素)
int Pop(LinkStack *L, elemtype *e) {
// 先判断栈是否为空
if (L->top == NULL) {
printf("栈为空,无法出栈!\n");
return 0;
}
// 1. 保存栈顶节点和数据
StackNode *temp = L->top; // temp指向当前栈顶节点
*e = temp->data; // 取出栈顶元素存入e
// 2. 更新栈顶指针,指向原栈顶的next节点
L->top = L->top->next;
// 3. 释放旧栈顶节点的内存(避免内存泄漏)
free(temp);
L->size--; // 元素个数-1
return 1;
}
举例:对上面存了 10、20、30 的链栈执行出栈
完美体现 “后进先出”—— 最后入栈的 30 最先出栈,再是 20。
和顺序栈一样,有时我们只想查看栈顶元素,不删除它,只需直接访问栈顶节点的数据即可,无需修改指针。
// 获取栈顶元素(e存储栈顶数据)
int GetTop(LinkStack *L, elemtype *e) {
if (L->top == NULL) {
printf("栈为空,无栈顶元素!\n");
return 0;
}
*e = L->top->data; // 直接取栈顶节点的数据
return 1;
}
比如上面出栈两次后,栈顶是 10,调用这个函数就能取出 10,栈的结构和 size 都不会改变。
判断链栈是否为空,只需看栈顶指针是否为 NULL(没有节点就是空栈)。
// 判断链栈是否为空
int IsEmpty(LinkStack *L) {
return L->top == NULL ? 1 : 0; // 空栈返回1,非空返回0
}
把上面的操作串联起来,看一个完整的运行过程,代码如下:
#include <stdio.h>
#include <stdlib.h>
// (此处省略前面定义的节点、栈结构及所有操作函数)
int main() {
LinkStack L;
InitLinkStack(&L); // 初始化空栈
// 入栈操作
Push(&L, 10);
Push(&L, 20);
Push(&L, 30);
printf("入栈后栈的大小:%d\n", L.size); // 输出:3
// 出栈操作
elemtype e;
Pop(&L, &e);
printf("出栈元素:%d\n", e); // 输出:30(最后入栈,最先出栈)
printf("出栈后栈的大小:%d\n", L.size); // 输出:2
// 取栈顶元素
GetTop(&L, &e);
printf("当前栈顶元素:%d\n", e); // 输出:20(出栈30后,20成为新栈顶)
// 判断栈是否为空
if (IsEmpty(&L)) {
printf("栈为空\n");
} else {
printf("栈非空\n"); // 输出:栈非空
}
return 0;
}
运行结果完全符合预期,链栈的操作逻辑和顺序栈一致,但没有容量限制,灵活度更高。
链栈适合以下场景:
栈的链式结构是顺序栈的 “灵活升级版”,核心是用单链表实现 “后进先出”,通过栈顶指针操作节点,实现高效的入栈和出栈。关键要点总结:
如果你是编程新手,建议结合本文代码动手敲一遍,重点关注节点的创建和指针的移动,实践后就能轻松掌握。下一篇我们可以对比顺序栈和链栈的具体使用场景,帮你精准选择合适的栈结构,感兴趣的小伙伴可以持续关注哦!