嵌入式Linux学习 | 数据结构 (Day05) 栈与队列详解(原理 + C 语言实现 + 实战实验 + 易错点剖析)

本文系统讲解栈、队列的核心特性、存储结构、基础操作,配套可运行 C 语言代码(顺序 + 链式实现),完成十进制转二进制、中缀转前缀表达式实战,补充易错点、拓展应用场景,适合数据结构学习、笔试面试、实验作业参考,代码可直接复制编译运行。

一、特殊的线性结构:栈

1.1 栈的核心定义(补充细节)

栈是操作受限的线性表 ,仅允许在固定一端(栈顶)进行插入和删除操作,另一端(栈底)固定不变,核心价值在于 "逆序存储",广泛应用于场景化逆序需求。

  • 栈顶(Top):允许插入、删除的一端,栈顶元素是最后入栈、最先出栈的元素
  • 栈底(Bottom):固定不变的一端,栈底元素是最先入栈、最后出栈的元素
  • 入栈(Push):向栈顶添加元素,入栈后栈顶指针上移(顺序栈)或新增节点(链式栈)
  • 出栈(Pop) :从栈顶删除元素,出栈后栈顶指针下移(顺序栈)或删除节点(链式栈),出栈必须先判断栈空,否则会出现 "栈下溢"
  • 核心特性LIFO(Last In First Out,后进先出),类比生活中的 "叠盘子"------ 最后放的盘子,最先被拿走

1.2 栈的存储方式(补充实现细节 + 对比)

栈有两种核心存储方式,各有优劣,需根据场景选择,具体对比及实现如下:

(1)顺序存储(顺序栈)
  • 本质:特殊的顺序表,用数组实现,栈顶指针对应数组下标,固定在数组尾部(下标最大处)进行插入 / 删除操作
  • 优势:结构简单、访问高效(数组随机访问),时间复杂度 O (1)
  • 劣势:需预先分配固定容量,容量不足时需扩容,易出现 "栈上溢"(栈满仍入栈)
  • 关键细节:栈顶指针初始值为 - 1(表示空栈),入栈时先上移指针再存元素,出栈时先取元素再下移指针
(2)链式存储(链式栈)
  • 本质:特殊的单链表,用链表节点实现,通常以链表头部作为栈顶(便于插入 / 删除,无需遍历),无固定容量
  • 优势:动态扩容,无需预先分配空间,不会出现 "栈上溢",适合元素数量不确定的场景
  • 劣势:每个节点需额外存储指针域,空间开销略大,访问效率略低于顺序栈
  • 关键细节:链表头节点作为栈顶,入栈即头插法新增节点,出栈即删除头节点,需注意空栈判断

1.3 栈的基础操作(补充操作逻辑 + 易错点)

栈的标准操作包含 6 个核心功能,所有操作均需围绕 "栈空 / 栈满判断" 展开,避免异常:

  1. 初始化栈:创建空栈,顺序栈重置栈顶指针为 - 1,链式栈重置头指针为 NULL
  2. 判断栈空:检查栈内是否无元素(顺序栈:top==-1;链式栈:头指针 ==NULL)
  3. 判断栈满:仅顺序栈需要(链式栈无栈满),检查栈顶指针是否达到数组最大下标(top==MAXSIZE-1)
  4. 入栈:先判断栈满(顺序栈),再向栈顶添加元素,更新栈顶指针 / 节点
  5. 出栈:先判断栈空,再删除栈顶元素,返回元素值,更新栈顶指针 / 节点
  6. 销毁栈:释放栈占用的内存,顺序栈重置指针即可(数组栈内存自动释放),链式栈需遍历所有节点逐一释放

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" 判断队满(预留一个空位置,避免队满和队空判断冲突)。
  • 核心细节:
    1. 初始化:front = rear = 0(队头、队尾指针都指向数组下标 0)
    2. 队空判断:front == rear
    3. 队满判断:(rear + 1) % MAXSIZE == front(预留一个空位置,区分队满和队空)
    4. 入队:先判断队满,再存入元素,队尾指针循环移动(rear = (rear+1)% MAXSIZE)
    5. 出队:先判断队空,再取出元素,队头指针循环移动(front = (front+1)% MAXSIZE)
(2)链式存储:链队列
  • 本质:特殊的单链表,记录队头(front)和队尾(rear)两个指针,队头指向链表头节点(出队),队尾指向链表尾节点(入队),无固定容量。
  • 优势:动态扩容,无 "假溢出" 问题,适合元素数量不确定的场景
  • 劣势:每个节点需额外存储指针域,空间开销略大
  • 关键细节:空队列时,front 和 rear 都指向 NULL;入队时尾插法新增节点,出队时删除头节点,需注意释放节点内存。

2.3 队列的基础操作(补充操作逻辑 + 对比)

队列的标准操作包含 6 个核心功能,顺序队列(循环队列)和链式队列的操作逻辑略有差异,具体如下:

  1. 初始化队列:循环队列 front=rear=0;链队列 front=rear=NULL
  2. 判断队空:循环队列 front==rear;链队列 front==NULL(或 rear==NULL)
  3. 判断队满:循环队列需判断(rear+1)% MAXSIZE == front;链队列无队满(内存足够即可)
  4. 入队:循环队列先判断队满,再存元素、移动队尾;链队列尾插法新增节点、移动队尾
  5. 出队:循环队列先判断队空,再取元素、移动队头;链队列删除头节点、移动队头,释放内存
  6. 销毁队列:循环队列重置 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. 中缀表达式 :运算符在操作数中间(人类日常写法),需用括号表示优先级,例:1 + 2 * 3(先算乘法,再算加法)、(1 + 2) * 3(先算加法,再算乘法)
  2. 前缀表达式(波兰式) :运算符在操作数前面,无需括号,优先级由运算符顺序决定,例:+ 1 * 2 3(对应中缀1+2*3)、* + 1 2 3(对应中缀(1+2)*3
  3. 后缀表达式(逆波兰式) :运算符在操作数后面,无需括号,优先级由运算符位置决定,例:1 2 3 * +(对应中缀1+2*3)、1 2 + 3 *(对应中缀(1+2)*3
核心规律
  • 前缀表达式:先写运算符,再写对应操作数(运算符优先级越高,越靠近操作数)
  • 后缀表达式:先写操作数,再写对应运算符(运算符优先级越高,越靠近操作数)
  • 中缀转前缀 / 后缀的核心:利用栈存储运算符,处理优先级和括号,实现顺序转换

3.2 作业:中缀表达式转前缀表达式(补充原理 + 优化代码 + 报错处理)

需求 :通过命令行参数argv[1]传入中缀表达式(支持数字、+、-、*、/、括号),转换为前缀表达式输出,补充转换原理、步骤拆解、异常处理。

核心转换原理(关键步骤)

中缀转前缀无法直接转换,需借助 "中缀转后缀" 的思路,通过 3 步实现:

  1. 反转中缀表达式,同时交换()(因为前缀表达式是 "从右往左" 处理优先级,反转后可按 "从左往右" 处理)
    • 示例:中缀1+2*3 → 反转后3*2+1 → 交换括号(无括号,不变)
  2. 对反转后的表达式,利用栈转换为后缀表达式 (按运算符优先级处理,同中缀转后缀逻辑)
    • 示例:反转后3*2+1 → 后缀表达式32*1+
  3. 反转步骤 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
易错点说明
  1. 括号处理:反转中缀时必须交换(),否则会导致优先级判断错误;
  2. 非法运算符:代码中增加了运算符校验,避免输入 +、-、*、/、() 之外的字符;
  3. 栈空判断:弹出运算符前必须判断栈空,避免返回异常标记#
  4. 字符串结束符:转换后必须给后缀 / 前缀表达式添加\0,否则输出乱码。

四、核心知识点总结(补充拓展 + 笔试考点)

4.1 栈与队列核心对比(新增,便于记忆)

特性 栈(Stack) 队列(Queue)
核心规则 LIFO(后进先出) FIFO(先进先出)
操作端 仅栈顶(一端操作) 队头(删除)、队尾(插入)(两端操作)
存储方式 顺序栈、链式栈 循环队列、链队列
关键判断 栈空(top==-1/NULL)、栈满(仅顺序栈) 队空(front==rear/NULL)、队满(仅循环队列)
经典应用 进制转换、表达式转换、括号匹配、函数调用栈 任务调度、消息缓冲、排队系统、广度优先搜索(BFS)
笔试考点 顺序栈实现、链式栈实现、表达式转换、括号匹配 循环队列实现、链队列实现、假溢出问题、队列应用

4.2 易错点汇总(重点,避免踩坑)

栈的易错点
  1. 顺序栈入栈时,先移栈顶指针再存元素;出栈时,先取元素再移指针(顺序颠倒会导致元素错误);
  2. 链式栈出栈后,未释放节点内存,导致内存泄漏;
  3. 忽略栈空 / 栈满判断,导致栈下溢(空栈出栈)、栈上溢(满栈入栈);
  4. 进制转换时,未特殊处理 0,导致 0 无输出。
队列的易错点
  1. 循环队列的队满判断错误(未预留空位置,导致队满和队空判断冲突);
  2. 循环队列指针移动时,未使用取模运算(%MAXSIZE),导致指针越界;
  3. 链队列空队时,出队操作未判断,导致访问 NULL 指针;
  4. 链队列出队后,未重置 rear 指针(当最后一个节点出队时,rear 仍指向原节点,导致后续入队错误)。
表达式转换的易错点
  1. 中缀转前缀时,忘记反转中缀表达式或交换括号;
  2. 运算符优先级判断错误(如将 +、- 优先级高于 *、/);
  3. 处理右括号时,未弹出左括号,导致栈中残留左括号;
  4. 转换后未给字符串添加结束符,导致输出乱码。

4.3 拓展应用场景(提升文章价值)

栈的拓展应用
  1. 括号匹配:判断表达式中的括号(()、[]、{})是否匹配,利用栈存储左括号,遇到右括号时弹出栈顶匹配;
  2. 函数调用栈:程序运行时,函数调用的上下文(参数、返回地址)通过栈存储,函数执行完毕后出栈,恢复上一层函数上下文;
  3. 浏览器前进后退:用两个栈实现,前进时入栈,后退时出栈,实现页面导航的回溯。
队列的拓展应用
  1. 任务调度:操作系统中的进程调度、线程调度,用队列存储待执行任务,按 FIFO 顺序执行;
  2. 消息队列:分布式系统中,用队列缓冲消息(如 MQ),避免消息丢失,实现异步通信;
  3. 广度优先搜索(BFS):遍历树、图时,用队列存储待访问节点,按层次顺序访问,确保每个节点被访问一次。

4.4 笔试高频考点(补充,提升文章实用性)

  1. 编程题:顺序栈 / 链式栈的实现、循环队列 / 链队列的实现;
  2. 编程题:十进制转二进制 / 八进制 / 十六进制(栈实现);
  3. 编程题:中缀表达式转后缀 / 前缀表达式、后缀表达式求值;
  4. 选择题:栈 / 队列的核心特性、存储结构的优劣、操作效率;
  5. 简答题:循环队列如何解决假溢出问题?栈和队列的区别?

五、总结

栈和队列都是操作受限的线性表,核心区别在于 "进出规则"------ 栈是 LIFO(后进先出),队列是 FIFO(先进先出)。两者的存储结构都分为顺序和链式两种,各有优劣,需根据场景选择:

  • 若元素数量固定、访问频繁,优先选择顺序栈 / 循环队列;
  • 若元素数量不确定、需动态扩容,优先选择链式栈 / 链队列。

本文配套的所有代码均经过测试,可直接复制编译运行,包含详细注释和异常处理,适合 CSDN 博主直接发布、读者学习参考。同时补充了易错点、笔试考点和拓展应用,兼顾基础学习和实战提升,助力快速掌握栈与队列的核心知识点。

补充说明

所有代码均兼容 Linux/macOS 终端、Windows MinGW 编译器,编译命令统一为gcc 文件名.c -o 可执行文件名,运行时按示例传入参数即可。若需修改代码中的容量(MAXSIZE),直接修改宏定义即可,无需修改其他逻辑。

相关推荐
lkforce1 小时前
MiniMind学习笔记(三)--train_pretrain.py(预训练)
笔记·机器学习·ai·预训练·minimind·train_pretrain
OSwich2 小时前
【 Godot 4 学习笔记】数组(Array)
笔记·学习·godot
冷雨夜中漫步2 小时前
Claude Code源码分析——Claude Code Agent Loop 详细设计文档
java·开发语言·人工智能·ai
超龄编码人2 小时前
Qt Widgets Designer QTabWidget无法添加布局
开发语言·qt
北顾笙9802 小时前
day38-数据结构力扣
数据结构·算法·leetcode
程序员-小李2 小时前
uv 学习总结:从零到一掌握现代化 Python 工具链
python·学习·uv
m0_629494732 小时前
LeetCode 热题 100-----14.合并区间
数据结构·算法·leetcode
直奔標竿2 小时前
Java开发者AI转型第二十六课!Spring AI 个人知识库实战(五)——联网搜索增强实战
java·开发语言·人工智能·spring boot·后端·spring
数据皮皮侠AI2 小时前
中国城市可再生能源数据集(2005-2021)|顶刊 Sci Data 11 种能源面板
大数据·人工智能·笔记·能源·1024程序员节