一、栈的基础认知(补充核心资料内容)
1. 栈的核心定义
栈是一种仅允许在一端(栈顶)进行插入和删除操作的线性表,遵循 "后进先出(Last In First Out,LIFO)" 原则:
- 入栈(Push):在栈顶添加元素;
- 出栈(Pop):从栈顶删除元素;
- 栈空 / 栈满:栈中无元素为栈空,链式栈无 "栈满" 概念(动态扩容);
- 栈顶(Top):唯一可操作的一端,栈底固定不可直接访问。
2. 栈的核心分类(资料核心补充)
根据栈顶指针(top)的变化规则,栈可分为四类:
| 栈类型 | 核心特征 |
|---|---|
| 空增栈 | 栈空时,新增元素后top指向的内存地址变大 |
| 空减栈 | 栈空时,新增元素后top指向的内存地址变小 |
| 满增栈 | 栈满时,新增元素后top指向的内存地址变大 |
| 满减栈 | 栈满时,新增元素后top指向的内存地址变小 |
同时需明确top的核心含义:
- 空栈 :
top指向新元素待插入的位置; - 满栈 :
top指向最后入栈的元素的位置。
3. 数据结构栈 vs 系统栈(资料核心补充)
很多学习者易混淆 "数据结构栈" 和 "系统栈",两者核心区别如下:
| 对比维度 | 数据结构栈 | 系统栈 |
|---|---|---|
| 工作原理 | 先进后出 | 先进后出 |
| 内存位置 | 堆空间(动态开辟) | 0-3G 用户内存段(约 8M 固定空间) |
| 核心功能 | 通用数据结构,适配括号匹配、表达式求值等场景 | 存储函数调用关系、局部变量、函数参数、返回地址 |
| 管理方式 | 开发者手动创建 / 销毁(如链式栈的Create/Destroy) |
系统自动管理(函数调用时入栈,返回时出栈) |
4. 链式栈的优势
栈的实现分为 "顺序栈(数组实现)" 和 "链式栈(链表实现)",链式栈的核心优势:
- 动态扩容:无需预先指定容量,避免顺序栈的 "溢出" 问题;
- 内存高效:仅在需要时分配节点内存,无空间浪费;
- 操作灵活:入栈 / 出栈无需移动元素,时间复杂度均为 O (1)。
二、链式栈的核心结构定义
在实现链式栈前,需先定义核心数据结构(通常放在linkstack.h头文件中):
#ifndef LINKSTACK_H
#define LINKSTACK_H
// 栈节点数据类型(可自定义,适配不同场景)
typedef union {
char c; // 字符型(用于括号匹配)
int num; // 数值型(用于表达式求值)
char op; // 运算符型(用于表达式求值)
// 扩展字段:存储括号的行列号(括号匹配场景)
struct {
int row;
int col;
};
} DATATYPE;
// 链式栈节点结构
typedef struct stacknode {
DATATYPE data; // 数据域:存储栈元素
struct stacknode *next; // 指针域:指向栈顶方向的下一个节点
} LinkStackNode;
// 链式栈管理结构(简化栈操作,无需遍历链表统计长度)
typedef struct {
LinkStackNode *top; // 栈顶指针(核心操作入口)
int clen; // 栈中元素个数(避免遍历统计)
} LinkStack;
// 链式栈核心操作声明
LinkStack* CreateLinkStack(); // 创建栈
int PushLinkStack(LinkStack* ls, DATATYPE* newdata); // 入栈
int PopLinkStack(LinkStack* ls); // 出栈
DATATYPE* GetTopLinkStack(LinkStack* ls); // 获取栈顶元素
int GetSizeLinkStack(LinkStack* ls); // 获取栈大小
int IsEmptyLinkStack(LinkStack* ls); // 判断栈空
int DestroyLinkStack(LinkStack *ls); // 销毁栈
#endif
三、链式栈核心操作实现(重点)
以下是链式栈核心操作的完整代码及逐行解析,也是栈学习的核心内容:
#include "linkstack.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 1. 创建链式栈:初始化栈管理结构
LinkStack* CreateLinkStack()
{
// 为栈管理结构分配内存
LinkStack* ls = (LinkStack*)malloc(sizeof(LinkStack));
// 内存分配失败处理(必做:避免野指针)
if (NULL == ls)
{
printf("CreateLinkStack malloc failed\n");
return NULL;
}
// 初始化栈状态:栈顶为空,元素个数为0
ls->top = NULL;
ls->clen = 0;
return ls;
}
// 2. 入栈操作:在栈顶添加新元素
int PushLinkStack(LinkStack* ls, DATATYPE* newdata)
{
// 健壮性判断:栈管理结构/数据为空则返回失败
if (ls == NULL || newdata == NULL) {
printf("PushLinkStack: invalid param\n");
return 1;
}
// 为新栈节点分配内存
LinkStackNode* newnode = (LinkStackNode*)malloc(sizeof(LinkStackNode));
if (NULL == newnode)
{
printf("PushLinkStack malloc failed\n");
return 1; // 返回非0表示失败
}
// 拷贝数据到新节点(通用方式,适配自定义DATATYPE)
memcpy(&newnode->data, newdata, sizeof(DATATYPE));
newnode->next = NULL; // 初始化指针(规范操作)
// 核心:新节点指向原栈顶(头插法)
newnode->next = ls->top;
// 栈顶指针指向新节点(完成入栈)
ls->top = newnode;
// 更新栈元素个数
ls->clen++;
return 0; // 返回0表示成功
}
// 3. 获取栈大小:快速返回元素个数
int GetSizeLinkStack(LinkStack* ls)
{
// 健壮性判断:栈为空则返回0
return ls ? ls->clen : 0;
}
// 4. 判断栈空:检查是否无元素
int IsEmptyLinkStack(LinkStack* ls)
{
// 栈为空 或 元素个数为0 均判定为栈空
return (ls == NULL) || (0 == ls->clen);
}
// 5. 出栈操作:删除栈顶元素并释放内存
int PopLinkStack(LinkStack* ls)
{
// 前置检查:栈空则无法出栈
if(IsEmptyLinkStack(ls))
{
printf("PopLinkStack: stack is empty\n");
return 1; // 栈空返回失败
}
// 保存栈顶节点地址(避免释放后丢失指针)
LinkStackNode* tmp = ls->top;
// 栈顶指针指向原栈顶的下一个节点
ls->top = ls->top->next;
// 释放原栈顶节点内存(避免内存泄漏)
free(tmp);
// 更新栈元素个数
ls->clen--;
return 0;
}
// 6. 获取栈顶元素:返回栈顶数据地址(不删除)
DATATYPE* GetTopLinkStack(LinkStack* ls)
{
// 栈空则返回NULL
if(IsEmptyLinkStack(ls))
{
return NULL;
}
// 返回栈顶节点数据的地址
return &ls->top->data;
}
// 7. 销毁链式栈:释放所有内存
int DestroyLinkStack(LinkStack *ls)
{
if (ls == NULL) {
return 1;
}
// 循环出栈释放所有节点
int len = GetSizeLinkStack(ls);
for(int i=0; i<len; i++)
{
PopLinkStack(ls); // 复用出栈操作,自动释放节点内存
}
// 释放栈管理结构
free(ls);
return 0;
}
核心操作关键要点
- 内存管理:每个
malloc必须对应free,入栈分配节点内存、出栈释放节点内存、销毁栈释放管理结构; - 鲁棒性:所有操作均增加
ls非空判断,避免空指针访问; - 效率:借助
clen字段实现 O (1) 时间复杂度的大小统计 / 判空,优于遍历链表(O (n)); - 通用性:通过
memcpy拷贝DATATYPE,适配字符、数值、结构体等任意自定义数据类型。
四、实战练习:链式栈的典型应用
以下练习基于上述链式栈实现,帮助理解栈的实际应用场景,所有代码可直接编译运行。
练习 1:括号匹配(栈的嵌套场景)
功能说明
检查代码文件中的括号(()/[]/{})是否匹配,若不匹配则定位错误的行列位置。
完整代码
#include <stdio.h>
#include "linkstack.h"
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
// 读取文件内容到缓冲区
int readfile(char *filename, char* data, int max_len)
{
if (filename == NULL || data == NULL) {
return 1;
}
int fd = open(filename, O_RDONLY);
if(-1 == fd)
{
printf("readfile: open %s failed\n", filename);
return 1;
}
// 读取文件内容(最多max_len字节)
read(fd, data, max_len);
close(fd);
return 0;
}
// 检查括号匹配核心逻辑
int check_bracket(char *str, LinkStack* ls)
{
if (str == NULL || ls == NULL) return 1;
DATATYPE data = {0};
DATATYPE* tmp = NULL;
int row = 1; // 行号(从1开始)
int col = 1; // 列号(从1开始)
while(*str != '\0')
{
memset(&data, 0, sizeof(data));
switch(*str)
{
// 左括号入栈,记录字符+行列号
case '(':
case '{':
case '[':
data.c = *str;
data.row = row;
data.col = col;
PushLinkStack(ls, &data);
break;
// 右括号匹配检查
case ')':
tmp = GetTopLinkStack(ls);
if (tmp != NULL && '(' == tmp->c) {
PopLinkStack(ls); // 匹配成功,出栈
} else if (tmp == NULL) {
printf("错误:多余的右括号 '%c',位置:行%d 列%d\n", *str, row, col);
return 1;
} else {
printf("错误:括号不匹配,栈顶左括号 '%c'(行%d 列%d),当前右括号 '%c'(行%d 列%d)\n",
tmp->c, tmp->row, tmp->col, *str, row, col);
return 1;
}
break;
case ']':
tmp = GetTopLinkStack(ls);
if (tmp != NULL && '[' == tmp->c) {
PopLinkStack(ls);
} else if (tmp == NULL) {
printf("错误:多余的右括号 '%c',位置:行%d 列%d\n", *str, row, col);
return 1;
} else {
printf("错误:括号不匹配,栈顶左括号 '%c'(行%d 列%d),当前右括号 '%c'(行%d 列%d)\n",
tmp->c, tmp->row, tmp->col, *str, row, col);
return 1;
}
break;
case '}':
tmp = GetTopLinkStack(ls);
if (tmp != NULL && '{' == tmp->c) {
PopLinkStack(ls);
} else if (tmp == NULL) {
printf("错误:多余的右括号 '%c',位置:行%d 列%d\n", *str, row, col);
return 1;
} else {
printf("错误:括号不匹配,栈顶左括号 '%c'(行%d 列%d),当前右括号 '%c'(行%d 列%d)\n",
tmp->c, tmp->row, tmp->col, *str, row, col);
return 1;
}
break;
}
// 更新行列号
if('\n' == *str)
{
col = 0;
row++;
}
str++;
col++;
}
// 遍历结束后检查是否有未匹配的左括号
if (!IsEmptyLinkStack(ls)) {
tmp = GetTopLinkStack(ls);
printf("错误:未匹配的左括号 '%c',位置:行%d 列%d\n", tmp->c, tmp->row, tmp->col);
return 1;
}
return 0;
}
int main(int argc, char** argv)
{
char data[4096] = {0};
// 读取待检查的文件(替换为自己的文件路径)
if (readfile("./test_bracket.c", data, 4095) != 0) {
return 1;
}
// 创建链式栈
LinkStack* ls = CreateLinkStack();
if (ls == NULL) {
return 1;
}
// 检查括号匹配
int ret = check_bracket(data, ls);
if (ret == 0) {
printf("括号匹配检查通过!\n");
}
// 销毁栈
DestroyLinkStack(ls);
ls = NULL;
return ret;
}
测试说明
-
创建
test_bracket.c文件,写入包含错误括号的代码(如{[)];); -
编译运行程序,会输出:
错误:括号不匹配,栈顶左括号 '['(行1 列2),当前右括号 ')'(行1 列3)
练习 2:中缀表达式求值(栈的优先级场景)
功能说明
利用两个链式栈(操作数栈 + 运算符栈)实现中缀表达式(如20*3+2)求值,支持+/-/*//运算。
完整代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "linkstack.h"
int temp_num = 0; // 临时存储拼接的多位数
// 拼接数字(处理多位数,如"20"由'2'和'0'拼接)
void concat_num(char c)
{
temp_num = temp_num * 10 + (c - '0');
}
// 获取运算符优先级(*、/优先级高于+、-)
int get_priority(char op)
{
switch (op)
{
case '+':
case '-':
return 1;
case '*':
case '/':
return 2;
default:
return 0; // 非法运算符
}
}
// 计算两个数的运算结果
int calc_result(int num1, int num2, char op)
{
switch (op)
{
case '+': return num1 + num2;
case '-': return num1 - num2;
case '*': return num1 * num2;
case '/':
if (num2 == 0) {
printf("calc_result: divide by zero\n");
exit(1);
}
return num1 / num2;
default: return 0;
}
}
int main(int argc, char** argv)
{
char express[] = "20*3+25/5-8"; // 待计算的中缀表达式
DATATYPE data = {0};
// 创建操作数栈和运算符栈
LinkStack* num_stack = CreateLinkStack();
LinkStack* op_stack = CreateLinkStack();
if (num_stack == NULL || op_stack == NULL) {
printf("main: create stack failed\n");
return 1;
}
DATATYPE* top_op = NULL;
char* tmp = express;
// 遍历表达式
while (*tmp != '\0')
{
memset(&data, 0, sizeof(data));
// 处理数字(拼接多位数)
if (*tmp >= '0' && *tmp <= '9')
{
concat_num(*tmp);
tmp++;
continue;
}
// 数字入操作数栈
data.num = temp_num;
temp_num = 0;
PushLinkStack(num_stack, &data);
// 处理运算符:按优先级入栈/计算
top_op = GetTopLinkStack(op_stack);
while (1)
{
// 运算符入栈条件:栈空 或 当前运算符优先级更高
if (IsEmptyLinkStack(op_stack) ||
get_priority(*tmp) > get_priority(top_op ? top_op->op : ' '))
{
data.op = *tmp;
PushLinkStack(op_stack, &data);
break;
}
else
{
// 弹出操作数和运算符计算
int num2 = GetTopLinkStack(num_stack)->num;
PopLinkStack(num_stack);
int num1 = GetTopLinkStack(num_stack)->num;
PopLinkStack(num_stack);
char op = GetTopLinkStack(op_stack)->op;
PopLinkStack(op_stack);
// 计算结果入操作数栈
int result = calc_result(num1, num2, op);
data.num = result;
PushLinkStack(num_stack, &data);
top_op = GetTopLinkStack(op_stack); // 更新栈顶运算符
}
}
tmp++;
}
// 处理最后一个数字
data.num = temp_num;
temp_num = 0;
PushLinkStack(num_stack, &data);
// 处理剩余运算符
while (!IsEmptyLinkStack(op_stack))
{
int num2 = GetTopLinkStack(num_stack)->num;
PopLinkStack(num_stack);
int num1 = GetTopLinkStack(num_stack)->num;
PopLinkStack(num_stack);
char op = GetTopLinkStack(op_stack)->op;
PopLinkStack(op_stack);
int result = calc_result(num1, num2, op);
data.num = result;
PushLinkStack(num_stack, &data);
}
// 输出最终结果
top_op = GetTopLinkStack(num_stack);
int final_result = top_op->num;
printf("中缀表达式:%s\n计算结果:%d\n", express, final_result);
// 销毁栈
DestroyLinkStack(num_stack);
DestroyLinkStack(op_stack);
return 0;
}
运行结果
编译运行后输出:
中缀表达式:20*3+25/5-8
计算结果:57
五、学习总结与建议
1. 核心重点
- 链式栈的核心是 "头插法入栈、头删法出栈",所有操作围绕栈顶指针
top展开; - 内存管理是链式栈的关键,必须保证
malloc和free成对出现,避免内存泄漏; clen字段是优化点,可将栈大小统计、判空的时间复杂度从 O (n) 降至 O (1);- 栈的分类(空增 / 空减 / 满增 / 满减)核心是
top指针的变化规则,需结合实际场景选择。
2. 学习建议
- 基础巩固:手动模拟入栈、出栈过程(画链表图),理解指针变化;同时对比顺序栈(数组实现)的
top指针规则,区分数据结构栈与系统栈的差异; - 调试练习:在核心操作中增加打印(如入栈后打印栈顶值、栈大小),观察栈状态;
- 扩展优化:
- 为括号匹配增加 "忽略注释 / 字符串中括号" 的逻辑;
- 为表达式求值增加括号嵌套支持(如
(20+5)*3); - 实现链式栈的 "清空""遍历" 操作;
- 尝试基于 "空增栈 / 空减栈" 规则改造链式栈的
top指针逻辑。