链栈的实现_2027考研408

链栈的实现——2027考研408数据结构全攻略

适用范围:2027年全国硕士研究生招生考试·计算机学科专业基础(408)
考纲章节:第三章 栈和队列 → 3.1 栈 → 栈的链式存储结构
难度等级:⭐⭐⭐(基础核心,与单链表对比命题多)


目录

  1. 知识点概念
  2. 链栈的结构定义
  3. 链栈的实现(基本操作)
  4. 链栈的进阶操作
  5. 时间与空间复杂度分析
  6. 优缺点对比
  7. 链栈与顺序栈的对比
  8. 应用场景
  9. 高频考点与易错点
  10. 真题演练
  11. 总结速记

一、知识点概念

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 时间复杂度

操作时间复杂度说明
初始化 InitStackO(1)只需置空指针
判空 StackEmptyO(1)只需比较
入栈 PushO(1)只需申请结点并修改指针
出栈 PopO(1)只需修改指针并释放
取栈顶 GetTopO(1)直接访问
求栈长 StackLengthO(n)需遍历所有结点
销毁 DestroyStackO(n)需释放所有结点

📌 考研重点:链栈的入栈和出栈操作都是 O(1) 时间复杂度!

5.2 空间复杂度

类型空间复杂度说明
链栈本身O(n)n为当前元素个数
每个结点O(1)仅含数据域和指针域
各操作辅助空间O(1)(入栈/出栈)
O(n)(求长/销毁)

六、优缺点对比

6.1 链栈优点

  1. 无栈满问题:动态分配内存,只要内存足够就能入栈
  2. 空间按需分配:不预设容量,按需申请和释放
  3. 入栈出栈高效:只需 O(1) 时间修改指针
  4. 适合数据量不确定:无需担心溢出问题
  5. 插入删除灵活:不受固定容量限制

6.2 链栈缺点

  1. 存储密度低:每个结点额外存储一个指针(32位4字节,64位8字节)
  2. 缓存不友好:结点可能存储在不连续的内存位置
  3. 内存管理开销:每次入栈需 malloc,每次出栈需 free
  4. 无法随机访问:只能访问栈顶,无法直接访问栈底或中间元素
  5. 可能出现内存泄漏:忘记 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 大题常见考法

  1. 选择题:链栈 vs 顺序栈的选择
  2. 选择题:链栈入栈出栈操作的时间复杂度
  3. 选择题:链栈无栈满 vs 顺序栈有栈满
  4. 算法设计题:链栈的基本操作实现
  5. 填空题:链栈入栈出栈的指针操作

十、真题演练

题目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)

🔑 考试答题模板

写链栈算法时,必须包含:

  1. ✅ 判空操作(出栈前必须判断)
  2. ✅ malloc 返回值检查(入栈时)
  3. ✅ free 释放内存(出栈时)
  4. ✅ 指针正确更新(栈顶移动)

📚 参考资料

  • 王道考研《数据结构》第3章 栈和队列
  • 严蔚敏《数据结构(C语言版)》第3章
  • 2027年408考试大纲·数据结构部分

本文涵盖2027年408考研数据结构链栈全部考点,建议结合真题反复练习代码实现。
如有疑问,欢迎在评论区交流!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值