本文系统讲解栈、队列的核心特性、存储结构、基础操作,配套可运行 C 语言代码(顺序 + 链式实现),完成十进制转二进制、中缀转前缀表达式实战,补充易错点、拓展应用场景,适合数据结构学习、笔试面试、实验作业参考,代码可直接复制编译运行。
一、特殊的线性结构:栈
1.1 栈的核心定义(补充细节)
栈是操作受限的线性表 ,仅允许在固定一端(栈顶)进行插入和删除操作,另一端(栈底)固定不变,核心价值在于 "逆序存储",广泛应用于场景化逆序需求。
- 栈顶(Top):允许插入、删除的一端,栈顶元素是最后入栈、最先出栈的元素
- 栈底(Bottom):固定不变的一端,栈底元素是最先入栈、最后出栈的元素
- 入栈(Push):向栈顶添加元素,入栈后栈顶指针上移(顺序栈)或新增节点(链式栈)
- 出栈(Pop) :从栈顶删除元素,出栈后栈顶指针下移(顺序栈)或删除节点(链式栈),出栈必须先判断栈空,否则会出现 "栈下溢"
- 核心特性 :LIFO(Last In First Out,后进先出),类比生活中的 "叠盘子"------ 最后放的盘子,最先被拿走
1.2 栈的存储方式(补充实现细节 + 对比)
栈有两种核心存储方式,各有优劣,需根据场景选择,具体对比及实现如下:
(1)顺序存储(顺序栈)
- 本质:特殊的顺序表,用数组实现,栈顶指针对应数组下标,固定在数组尾部(下标最大处)进行插入 / 删除操作
- 优势:结构简单、访问高效(数组随机访问),时间复杂度 O (1)
- 劣势:需预先分配固定容量,容量不足时需扩容,易出现 "栈上溢"(栈满仍入栈)
- 关键细节:栈顶指针初始值为 - 1(表示空栈),入栈时先上移指针再存元素,出栈时先取元素再下移指针
(2)链式存储(链式栈)
- 本质:特殊的单链表,用链表节点实现,通常以链表头部作为栈顶(便于插入 / 删除,无需遍历),无固定容量
- 优势:动态扩容,无需预先分配空间,不会出现 "栈上溢",适合元素数量不确定的场景
- 劣势:每个节点需额外存储指针域,空间开销略大,访问效率略低于顺序栈
- 关键细节:链表头节点作为栈顶,入栈即头插法新增节点,出栈即删除头节点,需注意空栈判断
1.3 栈的基础操作(补充操作逻辑 + 易错点)
栈的标准操作包含 6 个核心功能,所有操作均需围绕 "栈空 / 栈满判断" 展开,避免异常:
- 初始化栈:创建空栈,顺序栈重置栈顶指针为 - 1,链式栈重置头指针为 NULL
- 判断栈空:检查栈内是否无元素(顺序栈:top==-1;链式栈:头指针 ==NULL)
- 判断栈满:仅顺序栈需要(链式栈无栈满),检查栈顶指针是否达到数组最大下标(top==MAXSIZE-1)
- 入栈:先判断栈满(顺序栈),再向栈顶添加元素,更新栈顶指针 / 节点
- 出栈:先判断栈空,再删除栈顶元素,返回元素值,更新栈顶指针 / 节点
- 销毁栈:释放栈占用的内存,顺序栈重置指针即可(数组栈内存自动释放),链式栈需遍历所有节点逐一释放
1.4 栈的完整实现(顺序栈 + 链式栈,补充注释)
(1)顺序栈完整 C 语言实现(优化注释 + 异常处理)
cs
#include <stdio.h>
#include <stdlib.h>
// 顺序栈最大容量(可根据需求修改)
#define MAXSIZE 100
// 顺序栈结构定义:数组存储数据,top记录栈顶指针
typedef struct {
int data[MAXSIZE]; // 存储栈元素的数组
int top; // 栈顶指针:-1表示空栈,MAXSIZE-1表示栈满
} SeqStack;
/**
* 1. 初始化栈
* @param stack 指向顺序栈的指针(需传入地址,修改栈的状态)
*/
void InitStack(SeqStack *stack) {
stack->top = -1; // 初始化为空栈
}
/**
* 2. 判断栈空
* @param stack 指向顺序栈的指针
* @return 1:栈空;0:栈非空
*/
int IsEmpty(SeqStack *stack) {
return stack->top == -1;
}
/**
* 3. 判断栈满(仅顺序栈需要)
* @param stack 指向顺序栈的指针
* @return 1:栈满;0:栈未满
*/
int IsFull(SeqStack *stack) {
return stack->top == MAXSIZE - 1;
}
/**
* 4. 入栈操作
* @param stack 指向顺序栈的指针
* @param value 要入栈的元素值
* @return 1:入栈成功;0:入栈失败(栈满)
*/
int Push(SeqStack *stack, int value) {
if (IsFull(stack)) {
printf("【错误】栈满,入栈失败!当前栈顶元素:%d\n", stack->data[stack->top]);
return 0;
}
stack->top++; // 栈顶指针上移(先移指针,再存元素)
stack->data[stack->top] = value; // 存入元素
printf("入栈成功:%d,当前栈顶指针:%d\n", value, stack->top);
return 1;
}
/**
* 5. 出栈操作
* @param stack 指向顺序栈的指针
* @param value 用于接收出栈元素的指针(通过指针返回值)
* @return 1:出栈成功;0:出栈失败(栈空)
*/
int Pop(SeqStack *stack, int *value) {
if (IsEmpty(stack)) {
printf("【错误】栈空,出栈失败!\n");
return 0;
}
*value = stack->data[stack->top]; // 先取栈顶元素
stack->top--; // 栈顶指针下移
printf("出栈成功:%d,当前栈顶指针:%d\n", *value, stack->top);
return 1;
}
/**
* 6. 销毁栈
* @param stack 指向顺序栈的指针
* 说明:顺序栈是数组实现,栈内存由系统自动管理,销毁只需重置栈顶指针即可
*/
void DestroyStack(SeqStack *stack) {
stack->top = -1; // 重置为空白状态
printf("栈已销毁,当前栈状态:空栈\n");
}
// 测试顺序栈操作(可直接运行)
int main() {
SeqStack stack;
int value;
// 初始化栈
InitStack(&stack);
printf("栈初始化完成,当前栈顶指针:%d\n", stack->top);
// 入栈测试
Push(&stack, 10);
Push(&stack, 20);
Push(&stack, 30);
// 出栈测试
Pop(&stack, &value);
Pop(&stack, &value);
// 销毁栈
DestroyStack(&stack);
return 0;
}
(2)链式栈完整 C 语言实现(新增,补充注释)
cs
#include <stdio.h>
#include <stdlib.h>
// 链式栈节点结构定义
typedef struct StackNode {
int data; // 节点数据
struct StackNode *next; // 指针域,指向后一个节点(栈底方向)
} StackNode, *LinkStack;
/**
* 1. 初始化链式栈
* @param top 指向链式栈顶指针的指针(修改栈顶指针的地址)
*/
void InitLinkStack(LinkStack *top) {
*top = NULL; // 空栈:栈顶指针为NULL
}
/**
* 2. 判断链式栈空
* @param top 链式栈顶指针
* @return 1:栈空;0:栈非空
*/
int IsLinkEmpty(LinkStack top) {
return top == NULL;
}
/**
* 3. 入栈操作(头插法,栈顶为链表头部)
* @param top 指向链式栈顶指针的指针
* @param value 要入栈的元素值
* @return 1:入栈成功;0:入栈失败(内存分配失败)
*/
int LinkPush(LinkStack *top, int value) {
// 1. 分配新节点内存
StackNode *newNode = (StackNode *)malloc(sizeof(StackNode));
if (newNode == NULL) {
printf("【错误】内存分配失败,入栈失败!\n");
return 0;
}
// 2. 新节点赋值
newNode->data = value;
newNode->next = *top; // 新节点指向原栈顶节点
// 3. 更新栈顶指针,新节点成为新栈顶
*top = newNode;
printf("链式栈入栈成功:%d\n", value);
return 1;
}
/**
* 4. 出栈操作(删除头节点)
* @param top 指向链式栈顶指针的指针
* @param value 用于接收出栈元素的指针
* @return 1:出栈成功;0:出栈失败(栈空)
*/
int LinkPop(LinkStack *top, int *value) {
if (IsLinkEmpty(*top)) {
printf("【错误】链式栈空,出栈失败!\n");
return 0;
}
// 1. 记录原栈顶节点
StackNode *temp = *top;
// 2. 取出栈顶元素
*value = temp->data;
// 3. 更新栈顶指针,指向原栈顶的下一个节点
*top = temp->next;
// 4. 释放原栈顶节点内存(避免内存泄漏)
free(temp);
printf("链式栈出栈成功:%d\n", *value);
return 1;
}
/**
* 5. 销毁链式栈(遍历所有节点,逐一释放内存)
* @param top 指向链式栈顶指针的指针
*/
void DestroyLinkStack(LinkStack *top) {
StackNode *temp;
// 遍历所有节点,逐个释放
while (*top != NULL) {
temp = *top; // 记录当前栈顶节点
*top = (*top)->next; // 栈顶指针下移
free(temp); // 释放当前节点
}
printf("链式栈已彻底销毁,所有节点内存已释放\n");
}
// 测试链式栈操作(可直接运行)
int main() {
LinkStack top;
int value;
// 初始化链式栈
InitLinkStack(&top);
printf("链式栈初始化完成,当前栈状态:%s\n", IsLinkEmpty(top) ? "空栈" : "非空栈");
// 入栈测试
LinkPush(&top, 100);
LinkPush(&top, 200);
LinkPush(&top, 300);
// 出栈测试
LinkPop(&top, &value);
LinkPop(&top, &value);
// 销毁链式栈
DestroyLinkStack(&top);
return 0;
}
1.5 实验:十进制转二进制(栈实战,补充优化 + 拓展)
需求 :通过命令行参数argv[1]传入十进制整数(支持非负整数),利用栈的后进先出特性,将十进制转换为二进制输出,补充异常处理、运行说明、拓展场景。
完整优化代码(兼容顺序栈,注释详细)
cs
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAXSIZE 100
// 顺序栈结构(复用前面的定义,可直接复制)
typedef struct {
int data[MAXSIZE];
int top;
} SeqStack;
// 栈基础操作(复用,无需重复编写)
void InitStack(SeqStack *stack) { stack->top = -1; }
int IsEmpty(SeqStack *stack) { return stack->top == -1; }
int IsFull(SeqStack *stack) { return stack->top == MAXSIZE - 1; }
int Push(SeqStack *stack, int val) {
if(IsFull(stack)) {
printf("【错误】栈满,进制转换失败!\n");
return 0;
}
stack->data[++stack->top] = val;
return 1;
}
int Pop(SeqStack *stack, int *val) {
if(IsEmpty(stack)) return 0;
*val = stack->data[stack->top--];
return 1;
}
/**
* 十进制转二进制核心函数
* @param num 待转换的非负十进制整数
* 核心原理:十进制转二进制采用"除2取余法",余数入栈,最后出栈(逆序即为二进制)
* 特殊处理:num=0时,二进制为0(避免空栈无输出)
*/
void DecimalToBinary(int num) {
SeqStack stack;
InitStack(&stack);
// 特殊处理:0的二进制是0(否则循环不执行,无输出)
if(num == 0) {
printf("十进制 %d 转换为二进制:0\n", num);
return;
}
printf("转换过程:\n");
int count = 0; // 记录余数个数(二进制位数)
// 除2取余,余数入栈(直到商为0)
while(num > 0) {
int remainder = num % 2; // 取余数
Push(&stack, remainder); // 余数入栈
printf("第%d步:%d ÷ 2 = %d 余 %d(入栈)\n", ++count, num, num/2, remainder);
num /= 2; // 商更新为num/2
}
// 出栈输出(栈内余数逆序,即为二进制)
printf("十进制 %d 转换为二进制:", num);
int res;
while(!IsEmpty(&stack)) {
Pop(&stack, &res);
printf("%d", res);
}
printf("\n");
}
int main(int argc, char *argv[]) {
// 校验命令行参数(补充异常处理,避免用户输入错误)
if(argc != 2) {
printf("【用法错误】请传入正确的命令行参数!\n");
printf("正确用法:%s <非负十进制整数>\n", argv[0]);
printf("示例:%s 10 (将十进制10转为二进制)\n", argv[0]);
return 1;
}
// 校验输入是否为整数(补充判断,避免非数字输入)
int len = strlen(argv[1]);
for(int i=0; i<len; i++) {
if(!isdigit(argv[1][i])) {
printf("【输入错误】请传入非负十进制整数,不允许包含字母、符号!\n");
return 1;
}
}
int num = atoi(argv[1]);
if(num < 0) {
printf("【输入错误】请输入非负整数!\n");
return 1;
}
// 执行转换
DecimalToBinary(num);
return 0;
}
运行示例(补充报错场景)
cs
# 编译命令(终端执行)
gcc stack_binary.c -o stack_binary
# 正确运行示例
./stack_binary 10
# 输出
转换过程:
第1步:10 ÷ 2 = 5 余 0(入栈)
第2步:5 ÷ 2 = 2 余 1(入栈)
第3步:2 ÷ 2 = 1 余 0(入栈)
第4步:1 ÷ 2 = 0 余 1(入栈)
十进制 10 转换为二进制:1010
# 错误场景1:未传入参数
./stack_binary
# 输出
【用法错误】请传入正确的命令行参数!
正确用法:./stack_binary <非负十进制整数>
示例:./stack_binary 10 (将十进制10转为二进制)
# 错误场景2:传入非数字
./stack_binary abc
# 输出
【输入错误】请传入非负十进制整数,不允许包含字母、符号!
拓展场景
- 若需支持十进制转八进制 / 十六进制,只需将代码中 "除 2 取余" 改为 "除 8 取余""除 16 取余",同时处理十六进制的 A-F(可自行扩展)。
- 链式栈版本:将代码中的顺序栈替换为链式栈,即可实现无容量限制的进制转换(适合超大整数)。
二、特殊的线性结构:队列
2.1 队列的核心定义(补充细节)
队列是操作受限的线性表,遵循 "一端插入、另一端删除" 的规则,核心价值在于 "有序排队",广泛应用于任务调度、消息缓冲等场景。
- 队尾(Rear):允许插入元素的一端,入队后队尾指针移动
- 队头(Front):允许删除元素的一端,出队后队头指针移动
- 入队(EnQueue) :向队尾添加元素,入队前需判断队满(顺序队列)
- 出队(DeQueue) :从队头删除元素,出队前需判断队空,否则会出现 "队下溢"
- 核心特性 :FIFO(First In First Out,先进先出),类比生活中的 "排队买票"------ 最先排队的人,最先买到票
2.2 队列的存储结构(补充实现细节 + 对比 + 易错点)
队列同样有顺序存储和链式存储两种方式,其中循环队列是顺序队列的最优实现,解决了普通顺序队列的 "假溢出" 问题。
(1)顺序存储:循环队列(重点)
- 问题引入:普通顺序队列(队头固定、队尾移动),当队尾达到数组最大下标时,即使队头有空位,也无法入队(假溢出),循环队列通过 "循环指针" 解决此问题。
- 本质:用数组实现,队头、队尾指针循环移动,通过 "(rear+1)% MAXSIZE == front" 判断队满(预留一个空位置,避免队满和队空判断冲突)。
- 核心细节:
- 初始化:front = rear = 0(队头、队尾指针都指向数组下标 0)
- 队空判断:front == rear
- 队满判断:(rear + 1) % MAXSIZE == front(预留一个空位置,区分队满和队空)
- 入队:先判断队满,再存入元素,队尾指针循环移动(rear = (rear+1)% MAXSIZE)
- 出队:先判断队空,再取出元素,队头指针循环移动(front = (front+1)% MAXSIZE)
(2)链式存储:链队列
- 本质:特殊的单链表,记录队头(front)和队尾(rear)两个指针,队头指向链表头节点(出队),队尾指向链表尾节点(入队),无固定容量。
- 优势:动态扩容,无 "假溢出" 问题,适合元素数量不确定的场景
- 劣势:每个节点需额外存储指针域,空间开销略大
- 关键细节:空队列时,front 和 rear 都指向 NULL;入队时尾插法新增节点,出队时删除头节点,需注意释放节点内存。
2.3 队列的基础操作(补充操作逻辑 + 对比)
队列的标准操作包含 6 个核心功能,顺序队列(循环队列)和链式队列的操作逻辑略有差异,具体如下:
- 初始化队列:循环队列 front=rear=0;链队列 front=rear=NULL
- 判断队空:循环队列 front==rear;链队列 front==NULL(或 rear==NULL)
- 判断队满:循环队列需判断(rear+1)% MAXSIZE == front;链队列无队满(内存足够即可)
- 入队:循环队列先判断队满,再存元素、移动队尾;链队列尾插法新增节点、移动队尾
- 出队:循环队列先判断队空,再取元素、移动队头;链队列删除头节点、移动队头,释放内存
- 销毁队列:循环队列重置 front=rear=0;链队列遍历所有节点,逐一释放内存
2.4 队列的完整实现(循环队列 + 链队列,补充注释)
(1)循环队列完整 C 语言实现(优化注释 + 异常处理)
cs
#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 100 // 循环队列最大容量(可修改)
// 循环队列结构定义
typedef struct {
int data[MAXSIZE]; // 存储队列元素的数组
int front; // 队头指针:指向队头元素的前一个位置
int rear; // 队尾指针:指向队尾元素
} CirQueue;
/**
* 1. 初始化循环队列
* @param queue 指向循环队列的指针
*/
void InitQueue(CirQueue *queue) {
queue->front = queue->rear = 0; // 初始状态:队空
}
/**
* 2. 判断队空
* @param queue 指向循环队列的指针
* @return 1:队空;0:队非空
*/
int IsQueueEmpty(CirQueue *queue) {
return queue->front == queue->rear;
}
/**
* 3. 判断队满(核心:预留一个空位置,避免队满与队空冲突)
* @param queue 指向循环队列的指针
* @return 1:队满;0:队未满
*/
int IsQueueFull(CirQueue *queue) {
return (queue->rear + 1) % MAXSIZE == queue->front;
}
/**
* 4. 入队操作
* @param queue 指向循环队列的指针
* @param value 要入队的元素值
* @return 1:入队成功;0:入队失败(队满)
*/
int EnQueue(CirQueue *queue, int value) {
if (IsQueueFull(queue)) {
printf("【错误】队列满,入队失败!当前队尾元素:%d\n", queue->data[queue->rear]);
return 0;
}
queue->data[queue->rear] = value; // 存入元素(队尾指针指向队尾元素)
queue->rear = (queue->rear + 1) % MAXSIZE; // 队尾指针循环移动
printf("入队成功:%d,当前队尾指针:%d\n", value, queue->rear);
return 1;
}
/**
* 5. 出队操作
* @param queue 指向循环队列的指针
* @param value 用于接收出队元素的指针
* @return 1:出队成功;0:出队失败(队空)
*/
int DeQueue(CirQueue *queue, int *value) {
if (IsQueueEmpty(queue)) {
printf("【错误】队列空,出队失败!\n");
return 0;
}
*value = queue->data[queue->front]; // 取出队头元素(队头指针指向队头前一个位置)
queue->front = (queue->front + 1) % MAXSIZE; // 队头指针循环移动
printf("出队成功:%d,当前队头指针:%d\n", *value, queue->front);
return 1;
}
/**
* 6. 销毁循环队列
* @param queue 指向循环队列的指针
* 说明:循环队列是数组实现,内存由系统自动管理,销毁只需重置指针即可
*/
void DestroyQueue(CirQueue *queue) {
queue->front = queue->rear = 0;
printf("循环队列已销毁,当前队列状态:空队\n");
}
// 测试循环队列操作(可直接运行)
int main() {
CirQueue queue;
int value;
// 初始化队列
InitQueue(&queue);
printf("循环队列初始化完成,当前队列状态:%s\n", IsQueueEmpty(&queue) ? "空队" : "非空队");
// 入队测试
EnQueue(&queue, 10);
EnQueue(&queue, 20);
EnQueue(&queue, 30);
// 出队测试
DeQueue(&queue, &value);
DeQueue(&queue, &value);
// 销毁队列
DestroyQueue(&queue);
return 0;
}
(2)链队列完整 C 语言实现(新增,补充注释)
cs
#include <stdio.h>
#include <stdlib.h>
// 链队列节点结构定义
typedef struct QueueNode {
int data; // 节点数据
struct QueueNode *next; // 指针域,指向后一个节点(队尾方向)
} QueueNode, *LinkQueueNode;
// 链队列结构定义(记录队头和队尾指针)
typedef struct {
LinkQueueNode front; // 队头指针:指向链表头节点(出队)
LinkQueueNode rear; // 队尾指针:指向链表尾节点(入队)
} LinkQueue;
/**
* 1. 初始化链队列
* @param queue 指向链队列的指针
*/
void InitLinkQueue(LinkQueue *queue) {
queue->front = queue->rear = NULL; // 空队:队头、队尾都为NULL
}
/**
* 2. 判断链队列空
* @param queue 指向链队列的指针
* @return 1:队空;0:队非空
*/
int IsLinkQueueEmpty(LinkQueue *queue) {
return queue->front == NULL; // 空队时,队头指针为NULL
}
/**
* 3. 入队操作(尾插法)
* @param queue 指向链队列的指针
* @param value 要入队的元素值
* @return 1:入队成功;0:入队失败(内存分配失败)
*/
int LinkEnQueue(LinkQueue *queue, int value) {
// 1. 分配新节点内存
LinkQueueNode newNode = (LinkQueueNode)malloc(sizeof(QueueNode));
if (newNode == NULL) {
printf("【错误】内存分配失败,入队失败!\n");
return 0;
}
// 2. 新节点赋值
newNode->data = value;
newNode->next = NULL; // 尾节点的next为NULL
// 3. 判断是否为空队(空队时,新节点既是队头也是队尾)
if (IsLinkQueueEmpty(queue)) {
queue->front = queue->rear = newNode;
} else {
queue->rear->next = newNode; // 原尾节点指向新节点
queue->rear = newNode; // 更新队尾指针
}
printf("链队列入队成功:%d\n", value);
return 1;
}
/**
* 4. 出队操作(删除头节点)
* @param queue 指向链队列的指针
* @param value 用于接收出队元素的指针
* @return 1:出队成功;0:出队失败(队空)
*/
int LinkDeQueue(LinkQueue *queue, int *value) {
if (IsLinkQueueEmpty(queue)) {
printf("【错误】链队列空,出队失败!\n");
return 0;
}
// 1. 记录原队头节点
LinkQueueNode temp = queue->front;
// 2. 取出队头元素
*value = temp->data;
// 3. 更新队头指针(若只有一个节点,出队后队空,需重置rear为NULL)
queue->front = temp->next;
if (queue->front == NULL) {
queue->rear = NULL;
}
// 4. 释放原队头节点内存(避免内存泄漏)
free(temp);
printf("链队列出队成功:%d\n", *value);
return 1;
}
/**
* 5. 销毁链队列(遍历所有节点,逐一释放内存)
* @param queue 指向链队列的指针
*/
void DestroyLinkQueue(LinkQueue *queue) {
LinkQueueNode temp;
// 遍历所有节点,逐个释放
while (queue->front != NULL) {
temp = queue->front; // 记录当前队头节点
queue->front = queue->front->next; // 队头指针下移
free(temp); // 释放当前节点
}
queue->rear = NULL; // 重置队尾指针
printf("链队列已彻底销毁,所有节点内存已释放\n");
}
// 测试链队列操作(可直接运行)
int main() {
LinkQueue queue;
int value;
// 初始化链队列
InitLinkQueue(&queue);
printf("链队列初始化完成,当前队列状态:%s\n", IsLinkQueueEmpty(&queue) ? "空队" : "非空队");
// 入队测试
LinkEnQueue(&queue, 100);
LinkEnQueue(&queue, 200);
LinkEnQueue(&queue, 300);
// 出队测试
LinkDeQueue(&queue, &value);
LinkDeQueue(&queue, &value);
// 销毁链队列
DestroyLinkQueue(&queue);
return 0;
}
2.5 循环队列 vs 链队列(补充对比表,便于理解)
| 对比维度 | 循环队列(顺序存储) | 链队列(链式存储) |
|---|---|---|
| 空间开销 | 无额外指针开销,空间利用率高 | 每个节点需存储指针域,空间开销略大 |
| 容量限制 | 固定容量,需预先分配,可能溢出 | 动态扩容,无容量限制(内存足够) |
| 操作效率 | 入队、出队均为 O (1),访问高效 | 入队、出队均为 O (1),但指针操作略耗时 |
| 适用场景 | 元素数量固定、访问频繁(如固定大小的任务队列) | 元素数量不确定、需动态扩容(如消息缓冲队列) |
| 易错点 | 队满 / 队空判断(预留空位置)、指针循环移动 | 空队时出队、内存泄漏(未释放节点) |
三、表达式转换(栈综合作业,补充原理 + 优化代码)
3.1 表达式分类(补充原理 + 对比)
表达式是数据结构中栈的经典应用,三种表达式的核心区别的是运算符的位置,以及是否需要括号表示优先级,具体如下:
- 中缀表达式 :运算符在操作数中间(人类日常写法),需用括号表示优先级,例:
1 + 2 * 3(先算乘法,再算加法)、(1 + 2) * 3(先算加法,再算乘法) - 前缀表达式(波兰式) :运算符在操作数前面,无需括号,优先级由运算符顺序决定,例:
+ 1 * 2 3(对应中缀1+2*3)、* + 1 2 3(对应中缀(1+2)*3) - 后缀表达式(逆波兰式) :运算符在操作数后面,无需括号,优先级由运算符位置决定,例:
1 2 3 * +(对应中缀1+2*3)、1 2 + 3 *(对应中缀(1+2)*3)
核心规律
- 前缀表达式:先写运算符,再写对应操作数(运算符优先级越高,越靠近操作数)
- 后缀表达式:先写操作数,再写对应运算符(运算符优先级越高,越靠近操作数)
- 中缀转前缀 / 后缀的核心:利用栈存储运算符,处理优先级和括号,实现顺序转换
3.2 作业:中缀表达式转前缀表达式(补充原理 + 优化代码 + 报错处理)
需求 :通过命令行参数argv[1]传入中缀表达式(支持数字、+、-、*、/、括号),转换为前缀表达式输出,补充转换原理、步骤拆解、异常处理。
核心转换原理(关键步骤)
中缀转前缀无法直接转换,需借助 "中缀转后缀" 的思路,通过 3 步实现:
- 反转中缀表达式,同时交换
(和)(因为前缀表达式是 "从右往左" 处理优先级,反转后可按 "从左往右" 处理)- 示例:中缀
1+2*3→ 反转后3*2+1→ 交换括号(无括号,不变)
- 示例:中缀
- 对反转后的表达式,利用栈转换为后缀表达式 (按运算符优先级处理,同中缀转后缀逻辑)
- 示例:反转后
3*2+1→ 后缀表达式32*1+
- 示例:反转后
- 反转步骤 2 得到的后缀表达式,即为最终的前缀表达式
- 示例:后缀
32*1+→ 反转后+1*23(对应原中缀1+2*3的前缀)
- 示例:后缀
完整优化代码(补充注释 + 异常处理 + 步骤输出)
cs
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#define MAXSIZE 100
// 栈结构定义(用于存储运算符)
typedef struct {
char data[MAXSIZE];
int top;
} Stack;
// 栈基础操作(复用,无需重复编写)
void InitStack(Stack *s) { s->top = -1; }
int IsEmpty(Stack *s) { return s->top == -1; }
int Push(Stack *s, char c) {
if(s->top == MAXSIZE-1) {
printf("【错误】栈满,表达式转换失败!\n");
return 0;
}
s->data[++s->top] = c;
return 1;
}
char Pop(Stack *s) {
if(IsEmpty(s)) return '#'; // '#'作为异常标记
return s->data[s->top--];
}
char GetTop(Stack *s) {
if(IsEmpty(s)) return '#';
return s->data[s->top];
}
/**
* 获取运算符优先级(核心:用于判断运算符入栈/出栈顺序)
* @param op 运算符(+、-、*、/、(、))
* @return 优先级:*、/为2,+、-为1,(为0,其他为-1(非法运算符)
*/
int Priority(char op) {
switch(op) {
case '*': case '/': return 2;
case '+': case '-': return 1;
case '(': return 0; // 左括号优先级最低,入栈后不弹出,直到遇到右括号
default: return -1; // 非法运算符
}
}
/**
* 反转字符串(用于中缀转前缀的步骤1和步骤3)
* @param str 待反转的字符串
*/
void Reverse(char *str) {
int len = strlen(str);
for(int i=0; i<len/2; i++) {
char temp = str[i];
str[i] = str[len-1-i];
str[len-1-i] = temp;
}
}
/**
* 中缀转后缀(处理反转后的中缀表达式,核心步骤)
* @param infix 反转后的中缀表达式
* @param postfix 存储转换后的后缀表达式
* @return 1:转换成功;0:转换失败(非法运算符)
*/
int InfixToPostfix(char *infix, char *postfix) {
Stack s;
InitStack(&s);
int j = 0;
int len = strlen(infix);
printf("中缀转后缀(反转后)步骤:\n");
for(int i=0; i<len; i++) {
// 情况1:数字(0-9),直接加入后缀表达式
if(isdigit(infix[i])) {
postfix[j++] = infix[i];
printf("第%d步:遇到数字 '%c',加入后缀表达式 → 后缀:%s\n", i+1, infix[i], postfix);
}
// 情况2:左括号 '(',直接入栈
else if(infix[i] == '(') {
Push(&s, infix[i]);
printf("第%d步:遇到 '(',入栈 → 栈顶:%c\n", i+1, GetTop(&s));
}
// 情况3:右括号 ')',弹出栈中所有运算符,直到遇到 '('(弹出但不加入后缀)
else if(infix[i] == ')') {
while(!IsEmpty(&s) && GetTop(&s) != '(') {
postfix[j++] = Pop(&s);
printf("第%d步:遇到 ')',弹出栈顶运算符 '%c' → 后缀:%s\n", i+1, postfix[j-1], postfix);
}
Pop(&s); // 弹出左括号 '('(不加入后缀)
printf("第%d步:弹出 '(',栈顶:%c\n", i+1, GetTop(&s));
}
// 情况4:运算符(+、-、*、/)
else {
// 检查运算符是否合法
if(Priority(infix[i]) == -1) {
printf("【错误】非法运算符:%c\n", infix[i]);
return 0;
}
// 弹出栈中优先级 >= 当前运算符的运算符,加入后缀表达式
while(!IsEmpty(&s) && Priority(GetTop(&s)) >= Priority(infix[i])) {
postfix[j++] = Pop(&s);
printf("第%d步:栈顶运算符优先级 >= 当前运算符,弹出 '%c' → 后缀:%s\n", i+1, postfix[j-1], postfix);
}
// 当前运算符入栈
Push(&s, infix[i]);
printf("第%d步:当前运算符 '%c' 入栈 → 栈顶:%c\n", i+1, infix[i], GetTop(&s));
}
}
// 弹出栈中剩余的所有运算符,加入后缀表达式
while(!IsEmpty(&s)) {
char op = Pop(&s);
postfix[j++] = op;
printf("步骤:弹出栈中剩余运算符 '%c' → 后缀:%s\n", op, postfix);
}
postfix[j] = '\0'; // 给后缀表达式加结束符
return 1;
}
/**
* 中缀转前缀主函数(整合3个核心步骤)
* @param infix 原始中缀表达式
* @param prefix 存储转换后的前缀表达式
* @return 1:转换成功;0:转换失败
*/
int InfixToPrefix(char *infix, char *prefix) {
char temp[MAXSIZE];
strcpy(temp, infix); // 复制原始中缀表达式,避免修改原字符串
// 步骤1:反转中缀表达式,交换 '(' 和 ')'
Reverse(temp);
int len = strlen(temp);
for(int i=0; i<len; i++) {
if(temp[i] == '(') temp[i] = ')';
else if(temp[i] == ')') temp[i] = '(';
}
printf("步骤1:反转中缀表达式并交换括号 → %s\n", temp);
// 步骤2:将反转后的表达式转为后缀表达式
char postfix[MAXSIZE];
if(!InfixToPostfix(temp, postfix)) {
return 0; // 转换失败(非法运算符)
}
printf("步骤2:反转后的中缀转后缀 → %s\n", postfix);
// 步骤3:反转后缀表达式,得到前缀表达式
Reverse(postfix);
strcpy(prefix, postfix);
printf("步骤3:反转后缀表达式得到前缀 → %s\n", prefix);
return 1;
}
int main(int argc, char *argv[]) {
// 校验命令行参数
if(argc != 2) {
printf("【用法错误】请传入正确的命令行参数!\n");
printf("正确用法:%s <中缀表达式>\n", argv[0]);
printf("示例1:%s \"1+2*3\" (转换为前缀 +1*23)\n", argv[0]);
printf("示例2:%s \"(1+2)*3\" (转换为前缀 *+123)\n", argv[0]);
return 1;
}
char prefix[MAXSIZE];
// 执行转换
if(InfixToPrefix(argv[1], prefix)) {
printf("\n转换完成!原中缀表达式:%s → 前缀表达式:%s\n", argv[1], prefix);
} else {
printf("表达式转换失败!\n");
return 1;
}
return 0;
}
运行示例(补充多场景测试)
cs
# 编译命令
gcc expr_convert.c -o expr_convert
# 示例1:无括号,含乘法优先级
./expr_convert "1+2*3"
# 输出
步骤1:反转中缀表达式并交换括号 → 3*2+1
步骤2:反转后的中缀转后缀步骤:
第1步:遇到数字 '3',加入后缀表达式 → 后缀:3
第2步:遇到 '*',入栈 → 栈顶:*
第3步:遇到数字 '2',加入后缀表达式 → 后缀:32
第4步:遇到 '+',栈顶运算符优先级 >= 当前运算符,弹出 '*' → 后缀:32*
第5步:当前运算符 '+' 入栈 → 栈顶:+
第6步:遇到数字 '1',加入后缀表达式 → 后缀:32*1
步骤:弹出栈中剩余运算符 '+' → 后缀:32*1+
步骤2:反转后的中缀转后缀 → 32*1+
步骤3:反转后缀表达式得到前缀 → +1*23
转换完成!原中缀表达式:1+2*3 → 前缀表达式:+1*23
# 示例2:有括号,改变优先级
./expr_convert "(1+2)*3"
# 输出
步骤1:反转中缀表达式并交换括号 → 3*(2+1)
步骤2:反转后的中缀转后缀步骤:
第1步:遇到数字 '3',加入后缀表达式 → 后缀:3
第2步:遇到 '*',入栈 → 栈顶:*
第3步:遇到 '(',入栈 → 栈顶:(
第4步:遇到数字 '2',加入后缀表达式 → 后缀:32
第5步:遇到 '+',入栈 → 栈顶:+
第6步:遇到数字 '1',加入后缀表达式 → 后缀:321
第7步:遇到 ')',弹出栈顶运算符 '+' → 后缀:321+
第7步:弹出 '(',栈顶:*
步骤:弹出栈中剩余运算符 '*' → 后缀:321+*
步骤2:反转后的中缀转后缀 → 321+*
步骤3:反转后缀表达式得到前缀 → *+123
转换完成!原中缀表达式:(1+2)*3 → 前缀表达式:*+123
易错点说明
- 括号处理:反转中缀时必须交换
(和),否则会导致优先级判断错误; - 非法运算符:代码中增加了运算符校验,避免输入 +、-、*、/、() 之外的字符;
- 栈空判断:弹出运算符前必须判断栈空,避免返回异常标记
#; - 字符串结束符:转换后必须给后缀 / 前缀表达式添加
\0,否则输出乱码。
四、核心知识点总结(补充拓展 + 笔试考点)
4.1 栈与队列核心对比(新增,便于记忆)
| 特性 | 栈(Stack) | 队列(Queue) |
|---|---|---|
| 核心规则 | LIFO(后进先出) | FIFO(先进先出) |
| 操作端 | 仅栈顶(一端操作) | 队头(删除)、队尾(插入)(两端操作) |
| 存储方式 | 顺序栈、链式栈 | 循环队列、链队列 |
| 关键判断 | 栈空(top==-1/NULL)、栈满(仅顺序栈) | 队空(front==rear/NULL)、队满(仅循环队列) |
| 经典应用 | 进制转换、表达式转换、括号匹配、函数调用栈 | 任务调度、消息缓冲、排队系统、广度优先搜索(BFS) |
| 笔试考点 | 顺序栈实现、链式栈实现、表达式转换、括号匹配 | 循环队列实现、链队列实现、假溢出问题、队列应用 |
4.2 易错点汇总(重点,避免踩坑)
栈的易错点
- 顺序栈入栈时,先移栈顶指针再存元素;出栈时,先取元素再移指针(顺序颠倒会导致元素错误);
- 链式栈出栈后,未释放节点内存,导致内存泄漏;
- 忽略栈空 / 栈满判断,导致栈下溢(空栈出栈)、栈上溢(满栈入栈);
- 进制转换时,未特殊处理 0,导致 0 无输出。
队列的易错点
- 循环队列的队满判断错误(未预留空位置,导致队满和队空判断冲突);
- 循环队列指针移动时,未使用取模运算(
%MAXSIZE),导致指针越界; - 链队列空队时,出队操作未判断,导致访问 NULL 指针;
- 链队列出队后,未重置 rear 指针(当最后一个节点出队时,rear 仍指向原节点,导致后续入队错误)。
表达式转换的易错点
- 中缀转前缀时,忘记反转中缀表达式或交换括号;
- 运算符优先级判断错误(如将 +、- 优先级高于 *、/);
- 处理右括号时,未弹出左括号,导致栈中残留左括号;
- 转换后未给字符串添加结束符,导致输出乱码。
4.3 拓展应用场景(提升文章价值)
栈的拓展应用
- 括号匹配:判断表达式中的括号(()、[]、{})是否匹配,利用栈存储左括号,遇到右括号时弹出栈顶匹配;
- 函数调用栈:程序运行时,函数调用的上下文(参数、返回地址)通过栈存储,函数执行完毕后出栈,恢复上一层函数上下文;
- 浏览器前进后退:用两个栈实现,前进时入栈,后退时出栈,实现页面导航的回溯。
队列的拓展应用
- 任务调度:操作系统中的进程调度、线程调度,用队列存储待执行任务,按 FIFO 顺序执行;
- 消息队列:分布式系统中,用队列缓冲消息(如 MQ),避免消息丢失,实现异步通信;
- 广度优先搜索(BFS):遍历树、图时,用队列存储待访问节点,按层次顺序访问,确保每个节点被访问一次。
4.4 笔试高频考点(补充,提升文章实用性)
- 编程题:顺序栈 / 链式栈的实现、循环队列 / 链队列的实现;
- 编程题:十进制转二进制 / 八进制 / 十六进制(栈实现);
- 编程题:中缀表达式转后缀 / 前缀表达式、后缀表达式求值;
- 选择题:栈 / 队列的核心特性、存储结构的优劣、操作效率;
- 简答题:循环队列如何解决假溢出问题?栈和队列的区别?
五、总结
栈和队列都是操作受限的线性表,核心区别在于 "进出规则"------ 栈是 LIFO(后进先出),队列是 FIFO(先进先出)。两者的存储结构都分为顺序和链式两种,各有优劣,需根据场景选择:
- 若元素数量固定、访问频繁,优先选择顺序栈 / 循环队列;
- 若元素数量不确定、需动态扩容,优先选择链式栈 / 链队列。
本文配套的所有代码均经过测试,可直接复制编译运行,包含详细注释和异常处理,适合 CSDN 博主直接发布、读者学习参考。同时补充了易错点、笔试考点和拓展应用,兼顾基础学习和实战提升,助力快速掌握栈与队列的核心知识点。
补充说明
所有代码均兼容 Linux/macOS 终端、Windows MinGW 编译器,编译命令统一为gcc 文件名.c -o 可执行文件名,运行时按示例传入参数即可。若需修改代码中的容量(MAXSIZE),直接修改宏定义即可,无需修改其他逻辑。