链栈的实现——2027考研408数据结构全攻略
适用范围:2027年全国硕士研究生招生考试·计算机学科专业基础(408)
考纲章节:第三章 栈和队列 → 3.1 栈 → 栈的链式存储结构
难度等级:⭐⭐⭐(基础核心,与单链表对比命题多)
目录
一、知识点概念
1.1 链栈的定义
链栈(Linked Stack) 是栈的链式存储结构,本质上是只允许在表头进行插入和删除操作的单链表。链栈用链表存储栈中的元素,栈顶通常设在链表的头部。
链栈的核心特征:
- 本质是一个单链表,只能在栈顶(链表头部)操作
- 无需担心栈满(可无限扩容,除非内存耗尽)
- 入栈 = 头插;出栈 = 头删
- 无需预设容量,内存按需分配
1.2 链栈 vs 单链表
单链表(一般操作):
[头] → [a1] → [a2] → [a3] → NULL
↑ ↑
可在任意位置插入 可在任意位置删除
链栈(受限操作):
[头] → [a1] → [a2] → [a3] → NULL
↑
栈顶(top)
只能: 栈顶插入(a) → [a] → [a1] → [a2] → [a3]
只能: 栈顶删除(a1) → [a2] → [a3] → NULL
📌 考研重点:链栈是单链表的特例,入栈操作即头插法创建链表,出栈操作即头删法。
1.3 链栈的存储结构
链栈结构:
┌─────────┐ ┌────┬────┐ ┌────┬────┐ ┌────┬────┐
│ top ────┼────────►│data│next│────►│data│next│────►│data│next│ NULL
└─────────┘ └────┴────┘ └────┴────┘ └────┴────┘
栈顶 中间结点 栈底(尾结点)
空栈: top = NULL
1.4 链栈与顺序栈的核心区别
| 对比维度 | 链栈 | 顺序栈 |
|---|---|---|
| 存储方式 | 链式存储(离散) | 顺序存储(连续) |
| 栈满问题 | 无栈满(动态分配) | 有栈满(需预设容量) |
| 入栈出栈 | O(1) | O(1) |
| 存储密度 | 较低(含指针域) | 高(纯数据) |
| 扩容 | 无需扩容 | 需扩容(有开销) |
| 实现方式 | 链表头作为栈顶 | 数组+top指针 |
二、链栈的结构定义
2.1 C语言结构体定义
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
typedef int ElemType;
/* 链栈结点定义 */
typedef struct StackNode {
ElemType data; // 数据域
struct StackNode *next; // 指针域(指向下一个结点)
} StackNode, *LinkStack;
⚠️ 注意:
LinkStack是指向栈顶结点的指针(相当于StackNode *top),不是整个栈的描述符。
2.2 空栈的表示
// 链栈空栈
LinkStack top = NULL; // 栈顶指针为NULL表示空栈
三、链栈的实现(基本操作)
3.1 初始化
/* 初始化链栈(空栈) */
void InitStack(LinkStack *S) {
*S = NULL; // 栈顶指针置空,无头结点
}
📌 注意:链栈通常不带头结点,栈顶就是链表的第一个数据结点。
3.2 判空
/* 判断链栈是否为空 */
bool StackEmpty(LinkStack S) {
return S == NULL; // 栈顶指针为NULL则空栈
}
3.3 入栈(Push)
/* 入栈操作:将元素e压入栈顶
* 链栈的入栈本质是单链表的头插法
*/
bool Push(LinkStack *S, ElemType e) {
StackNode *p = (StackNode *)malloc(sizeof(StackNode));
if (p == NULL) return false; // 内存分配失败
p->data = e; // 将数据存入新结点
p->next = *S; // 新结点指向原栈顶
*S = p; // 栈顶指针指向新结点
return true;
}
入栈图解:
入栈前: top → [a1] → [a2] → [a3] → NULL
Push(e=99):
1. 创建新结点 p,p->data = 99
2. p->next = top (p指向a1)
3. top = p (top指向p)
入栈后: top → [99] → [a1] → [a2] → [a3] → NULL
↑
新栈顶
3.4 出栈(Pop)
/* 出栈操作:删除栈顶元素,并用e返回其值
* 链栈的出栈本质是单链表的头删法
*/
bool Pop(LinkStack *S, ElemType *e) {
if (StackEmpty(*S)) // 栈空,无法出栈
return false;
StackNode *p = *S; // p指向栈顶结点
*e = p->data; // 用e返回栈顶元素
*S = p->next; // 栈顶指针下移一位
free(p); // 释放原栈顶结点
return true;
}
出栈图解:
出栈前: top → [a1] → [a2] → [a3] → NULL
↑
p(待删除)
Pop(e):
1. *e = p->data = a1
2. top = p->next (top指向a2)
3. free(p)
出栈后: top → [a2] → [a3] → NULL
3.5 取栈顶元素(GetTop)
/* 取栈顶元素:返回栈顶元素但不删除 */
bool GetTop(LinkStack S, ElemType *e) {
if (StackEmpty(S)) // 栈空
return false;
*e = S->data; // 取出栈顶元素
return true;
}
3.6 求栈长
/* 求链栈的长度 */
int StackLength(LinkStack S) {
int len = 0;
StackNode *p = S; // p从栈顶开始
while (p != NULL) { // 遍历至栈底
len++;
p = p->next;
}
return len;
}
3.7 销毁链栈
/* 销毁链栈(释放所有结点) */
void DestroyStack(LinkStack *S) {
StackNode *p;
while (*S != NULL) { // 依次释放每个结点
p = *S;
*S = (*S)->next;
free(p);
}
}
3.8 完整示例代码
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
typedef int ElemType;
typedef struct StackNode {
ElemType data;
struct StackNode *next;
} StackNode, *LinkStack;
/* 初始化 */
void InitStack(LinkStack *S) {
*S = NULL;
}
/* 判空 */
bool StackEmpty(LinkStack S) {
return S == NULL;
}
/* 入栈 */
bool Push(LinkStack *S, ElemType e) {
StackNode *p = (StackNode *)malloc(sizeof(StackNode));
if (p == NULL) return false;
p->data = e;
p->next = *S;
*S = p;
return true;
}
/* 出栈 */
bool Pop(LinkStack *S, ElemType *e) {
if (StackEmpty(*S)) return false;
StackNode *p = *S;
*e = p->data;
*S = p->next;
free(p);
return true;
}
/* 取栈顶 */
bool GetTop(LinkStack S, ElemType *e) {
if (StackEmpty(S)) return false;
*e = S->data;
return true;
}
/* 求栈长 */
int StackLength(LinkStack S) {
int len = 0;
StackNode *p = S;
while (p != NULL) {
len++;
p = p->next;
}
return len;
}
/* 打印栈(从栈顶到栈底) */
void PrintStack(LinkStack S) {
printf("链栈(从顶到底,长度=%d): ", StackLength(S));
StackNode *p = S;
while (p != NULL) {
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
/* 主函数测试 */
int main() {
LinkStack S;
ElemType e;
InitStack(&S);
// 入栈: 10 20 30 40 50
for (int i = 1; i <= 5; i++)
Push(&S, i * 10);
PrintStack(S); // 输出: 50 40 30 20 10(头插,逆序)
// 取栈顶
GetTop(S, &e);
printf("栈顶元素: %d\n", e); // 输出: 50
// 出栈
Pop(&S, &e);
printf("出栈元素: %d\n", e); // 输出: 50
PrintStack(S); // 输出: 40 30 20 10
// 再出栈
Pop(&S, &e);
printf("出栈元素: %d\n", e); // 输出: 40
PrintStack(S); // 输出: 30 20 10
printf("栈长度: %d\n", StackLength(S)); // 输出: 3
printf("栈是否为空: %s\n", StackEmpty(S) ? "是" : "否");
DestroyStack(&S);
return 0;
}
四、链栈的进阶操作
4.1 链栈的逆置
/* 逆置链栈
* 思路:依次弹出元素,再依次压入(利用栈的特性)
* 或直接反转链表
*/
void Reverse(LinkStack *S) {
LinkStack tmp;
InitStack(&tmp);
ElemType e;
// 弹出所有元素并压入tmp栈
while (!StackEmpty(*S)) {
Pop(S, &e);
Push(&tmp, e);
}
// 再弹回原栈
while (!StackEmpty(tmp)) {
Pop(&tmp, &e);
Push(S, e);
}
}
4.2 链栈的复制
/* 复制链栈(深拷贝) */
LinkStack CopyStack(LinkStack S) {
LinkStack newS;
InitStack(&newS);
if (S == NULL) return newS;
// 第一遍:逆序复制到新栈
StackNode *p = S;
while (p != NULL) {
Push(&newS, p->data);
p = p->next;
}
// 第二遍:逆序弹出再压回,得到正序
LinkStack tmp;
InitStack(&tmp);
ElemType e;
while (!StackEmpty(newS)) {
Pop(&newS, &e);
Push(&tmp, e);
}
// 交换指针
newS = tmp;
return newS;
}
4.3 判断链栈是否对称
/* 判断链栈中的元素是否构成回文
* 思路:将前半部分元素入辅助栈,然后比较后半部分
*/
bool IsSymmetric(LinkStack S) {
if (S == NULL) return true; // 空栈是对称的
int len = StackLength(S);
int half = len / 2;
// 将前半部分压入辅助栈
LinkStack aux;
InitStack(&aux);
StackNode *p = S;
for (int i = 0; i < half; i++) {
Push(&aux, p->data);
p = p->next;
}
// 如果是奇数个,跳过中间结点
if (len % 2 == 1)
p = p->next;
// 比较后半部分和辅助栈
ElemType e;
while (p != NULL) {
Pop(&aux, &e);
if (e != p->data) {
DestroyStack(&aux);
return false;
}
p = p->next;
}
DestroyStack(&aux);
return true;
}
4.4 C++ STL stack 实现(了解)
#include <iostream>
#include <stack>
using namespace std;
int main() {
stack<int> S; // STL中的stack默认基于deque实现
// 入栈
S.push(10);
S.push(20);
S.push(30);
// 取栈顶
cout << "栈顶: " << S.top() << endl; // 输出: 30
// 出栈
S.pop();
// 判空
cout << "是否为空: " << S.empty() << endl; // 输出: 0
// 大小
cout << "大小: " << S.size() << endl; // 输出: 2
return 0;
}
五、时间与空间复杂度分析
5.1 时间复杂度
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| 初始化 InitStack | O(1) | 只需置空指针 |
| 判空 StackEmpty | O(1) | 只需比较 |
| 入栈 Push | O(1) | 只需申请结点并修改指针 |
| 出栈 Pop | O(1) | 只需修改指针并释放 |
| 取栈顶 GetTop | O(1) | 直接访问 |
| 求栈长 StackLength | O(n) | 需遍历所有结点 |
| 销毁 DestroyStack | O(n) | 需释放所有结点 |
📌 考研重点:链栈的入栈和出栈操作都是 O(1) 时间复杂度!
5.2 空间复杂度
| 类型 | 空间复杂度 | 说明 |
|---|---|---|
| 链栈本身 | O(n) | n为当前元素个数 |
| 每个结点 | O(1) | 仅含数据域和指针域 |
| 各操作辅助空间 | O(1)(入栈/出栈) O(n)(求长/销毁) | — |
六、优缺点对比
6.1 链栈优点
- 无栈满问题:动态分配内存,只要内存足够就能入栈
- 空间按需分配:不预设容量,按需申请和释放
- 入栈出栈高效:只需 O(1) 时间修改指针
- 适合数据量不确定:无需担心溢出问题
- 插入删除灵活:不受固定容量限制
6.2 链栈缺点
- 存储密度低:每个结点额外存储一个指针(32位4字节,64位8字节)
- 缓存不友好:结点可能存储在不连续的内存位置
- 内存管理开销:每次入栈需 malloc,每次出栈需 free
- 无法随机访问:只能访问栈顶,无法直接访问栈底或中间元素
- 可能出现内存泄漏:忘记 free 会导致内存泄漏
七、链栈与顺序栈的对比
7.1 完整对比表
| 对比维度 | 链栈 | 顺序栈 |
|---|---|---|
| 存储方式 | 链式(离散) | 顺序(连续) |
| 栈满问题 | 无栈满 | 有栈满(需预设容量) |
| 空间预分配 | 无需 | 需要 |
| 空间灵活性 | 高(按需分配) | 低(固定容量) |
| 入栈时间 | O(1) | O(1) |
| 出栈时间 | O(1) | O(1) |
| 取栈顶时间 | O(1) | O(1) |
| 求栈长时间 | O(n) | O(1) |
| 存储密度 | 较低(含指针) | 高(纯数据) |
| 缓存命中 | 低(不连续) | 高(连续) |
| 实现复杂度 | 较低 | 简单 |
| 内存开销 | malloc/free开销 | 无 |
7.2 选择建议
| 场景 | 推荐 |
|---|---|
| 数据量固定、可预估 | 顺序栈 |
| 数据量不确定、变化大 | 链栈 |
| 内存受限 | 顺序栈(存储密度高) |
| 入栈出栈频繁 | 两者皆可 |
| 需要求栈长 | 顺序栈(O(1)) |
八、应用场景
8.1 适合使用链栈的场景
- 数据量无法预知:如表达式求值、函数调用
- 内存受限但需动态扩展:嵌入式系统
- 频繁入栈出栈操作:如撤销操作、历史记录
- 与顺序栈互补使用:根据实际需求选择
8.2 实际应用举例
| 应用 | 使用原因 |
|---|---|
| 函数调用 | 链栈存储返回地址和局部变量 |
| 表达式求值 | 操作数栈和运算符栈 |
| 括号匹配 | 左括号入栈,右括号出栈匹配 |
| 深度优先搜索 | 用栈保存搜索路径 |
| 编译器语法分析 | 符号表管理 |
| 内存管理 | 栈帧管理 |
九、高频考点与易错点
9.1 高频考点
考点1:入栈 = 头插法,出栈 = 头删法 ⭐⭐⭐⭐
链栈的操作与单链表的头部操作完全一致:
// 链栈入栈 = 单链表头插法
bool Push(LinkStack *S, ElemType e) {
StackNode *p = (StackNode *)malloc(sizeof(StackNode));
p->data = e;
p->next = *S; // 新结点指向原栈顶
*S = p; // 栈顶指向新结点
return true;
}
// 链栈出栈 = 单链表头删法
bool Pop(LinkStack *S, ElemType *e) {
if (*S == NULL) return false;
StackNode *p = *S;
*e = p->data;
*S = p->next; // 栈顶下移
free(p);
return true;
}
考点2:链栈无栈满,顺序栈有栈满 ⭐⭐⭐
// 链栈判满
// 不需要判满!动态分配内存,只要malloc成功就能入栈
// 顺序栈判满
if (S->top == MaxSize - 1) return false; // 数组下标越界
考点3:链栈无头结点 ⭐⭐⭐
// 链栈通常不带头结点
LinkStack S = NULL; // 直接指向第一个数据结点
// 单链表通常带头结点
LNode *L = (LNode *)malloc(sizeof(LNode));
L->next = NULL; // 头结点
考点4:malloc/free 配对 ⭐⭐⭐
// 入栈时malloc
StackNode *p = (StackNode *)malloc(sizeof(StackNode));
// 出栈时free
free(p); // 必须配对!
9.2 易错点汇总
易错点1:出栈前忘记判空
// ❌ 错误:直接出栈
Pop(&S, &e); // 可能栈空!
// ✅ 正确:先判空
if (!StackEmpty(S)) {
Pop(&S, &e);
} else {
printf("栈空\n");
}
易错点2:入栈时malloc失败未处理
// ❌ 错误:未检查malloc返回值
StackNode *p = (StackNode *)malloc(sizeof(StackNode));
p->data = e; // 若malloc失败,p为NULL,程序崩溃!
// ✅ 正确:检查malloc返回值
StackNode *p = (StackNode *)malloc(sizeof(StackNode));
if (p == NULL) return false;
p->data = e;
易错点3:出栈后忘记释放内存
// ❌ 错误:未释放内存,造成内存泄漏
*S = p->next; // 栈顶下移,但p未释放
// ✅ 正确:释放内存
*S = p->next;
free(p);
易错点4:混淆链栈和单链表
// 链栈的栈顶 = 单链表的头部
// 链栈的栈底 = 单链表的尾部(next为NULL)
// ❌ 错误:链栈不支持在栈底操作
// 链栈只能在栈顶(链表头部)进行插入删除!
// ✅ 正确理解:链栈 = 只能在头部操作的特殊单链表
易错点5:求栈长复杂度误判
// 链栈求栈长需要遍历,O(n)时间
int StackLength(LinkStack S) {
int len = 0;
while (S != NULL) {
len++;
S = S->next;
}
return len;
}
// ❌ 错误:认为链栈求栈长是O(1)
// ✅ 正确:链栈求栈长是O(n),顺序栈是O(1)
易错点6:销毁链栈时未置空
// ❌ 错误:销毁后未置空,可能导致悬空指针
DestroyStack(&S);
// S此时仍指向已释放的内存!
// ✅ 正确:销毁后置空
DestroyStack(&S);
S = NULL;
9.3 大题常见考法
- 选择题:链栈 vs 顺序栈的选择
- 选择题:链栈入栈出栈操作的时间复杂度
- 选择题:链栈无栈满 vs 顺序栈有栈满
- 算法设计题:链栈的基本操作实现
- 填空题:链栈入栈出栈的指针操作
十、真题演练
题目1(选择题)
题目:下列关于栈的叙述中,正确的是( )。
选项:
A. 栈是一种先进先出的线性表
B. 栈的链式存储结构中,仅能使用单链表实现
C. 链栈的入栈和出栈操作的时间复杂度都是 O(1)
D. 链栈不存在栈满的情况,但存在栈空的情况
答案:C、D
解析:
- A错误:栈是后进先出(LIFO),不是先进先出(FIFO)
- B错误:链栈可以用单链表实现,但也可使用其他链表
- C正确:链栈入栈出栈都只需 O(1) 时间修改指针
- D正确:链栈无栈满(动态分配),但栈空时 top = NULL
题目2(选择题)
题目:与顺序栈相比,链栈的主要优点是( )。
选项:
A. 入栈操作更容易实现
B. 支持随机访问
C. 入栈操作不会溢出
D. 出栈操作更快
答案:C
解析:
- A错误:两者入栈都是 O(1),难度相当
- B错误:栈不支持随机访问,只能访问栈顶
- C正确:链栈动态分配内存,不存在栈满溢出问题
- D错误:两者出栈都是 O(1)
题目3(算法设计)
题目:设计一个算法,判断链栈是否为空栈。
bool StackEmpty(LinkStack S) {
return S == NULL;
}
题目4(算法设计)
题目:设计一个算法,返回链栈中第 k 个元素(从栈顶开始)。
bool GetKth(LinkStack S, int k, ElemType *e) {
StackNode *p = S;
int count = 1;
while (p != NULL && count < k) {
p = p->next;
count++;
}
if (p == NULL) return false; // 不存在第k个元素
*e = p->data;
return true;
}
十一、总结速记
🔑 核心操作速记
链栈 = 单链表(只在头部操作)
入栈(头插):
p->next = *S
*S = p
出栈(头删):
*e = (*S)->data
*S = (*S)->next
free(p)
空栈: S == NULL
🔑 链栈 vs 顺序栈一句话区别
链栈:无栈满(动态分配),但存储密度低,malloc/free 开销
顺序栈:有栈满(预设容量),但存储密度高,无额外指针开销
🔑 时间复杂度速记表
| 操作 | 链栈 | 顺序栈 |
|---|---|---|
| 入栈 | O(1) | O(1) |
| 出栈 | O(1) | O(1) |
| 取栈顶 | O(1) | O(1) |
| 求栈长 | O(n) | O(1) |
| 判空 | O(1) | O(1) |
| 销毁 | O(n) | O(1) |
🔑 考试答题模板
写链栈算法时,必须包含:
- ✅ 判空操作(出栈前必须判断)
- ✅ malloc 返回值检查(入栈时)
- ✅ free 释放内存(出栈时)
- ✅ 指针正确更新(栈顶移动)
📚 参考资料
- 王道考研《数据结构》第3章 栈和队列
- 严蔚敏《数据结构(C语言版)》第3章
- 2027年408考试大纲·数据结构部分
本文涵盖2027年408考研数据结构链栈全部考点,建议结合真题反复练习代码实现。
如有疑问,欢迎在评论区交流!

370

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



