【数据结构实战】川剧 “扯脸” 与栈的 LIFO 特性 :用 C 语言实现 3 种栈结构

一、场景与问题分析

川剧 "扯脸" 是极具特色的变脸手法:事先将白色、黄色、黑色、红色、青色、金色、银色的脸谱依次贴在脸上,表演时在动作掩护下从最后贴的那张开始,一张一张扯下。

从数据结构视角看,"贴脸" 是元素依次入栈 ,"扯脸" 是元素从栈顶依次出栈,完全符合栈 "先进后出(LIFO)" 的核心特性。本文将用 3 种不同的栈实现方式(静态顺序栈、动态顺序栈、链式栈),一步步拆解并求解 "扯脸序列" 问题,代码注释详尽、逻辑层层递进,适合初学数据结构的大学生理解。

核心需求

  • 贴脸顺序:白色 → 黄色 → 黑色 → 红色 → 青色 → 金色 → 银色
  • 扯脸序列:银色 → 金色 → 青色 → 红色 → 黑色 → 黄色 → 白色

二、方案一:静态顺序栈(入门基础版)

核心特点

  • 静态数组存储栈元素,内存由编译器自动分配 / 释放,无需手动管理;
  • 代码最简单,适合刚接触栈的初学者,理解 "栈顶指针" 核心概念。
cpp 复制代码
/*
 * 文件名: stack_static_complete.c
 * 功能: 用静态顺序栈实现川剧扯脸序列求解
 * 适用人群: 数据结构初学者,理解栈的基本概念
 * 核心特性: 静态数组存储,内存自动管理,无需手动malloc/free
 */
#include <stdio.h>
#include <stdlib.h>

// -------------------------- 全局常量与通用函数 --------------------------
// 颜色编号映射:1=白色, 2=黄色, 3=黑色, 4=红色, 5=青色, 6=金色, 7=银色
// 功能:将整数编号转换为直观的颜色字符串
char* getColorName(int colorCode) {
    // 静态数组:下标与编号一一对应,避免冗余switch-case,易维护
    static const char* colorMap[] = {"未知颜色", "白色", "黄色", "黑色", "红色", "青色", "金色", "银色"};
    // 边界安全检查:防止非法编号导致数组越界
    if (colorCode < 1 || colorCode > 7) {
        return (char*)colorMap[0];
    }
    return (char*)colorMap[colorCode];
}

// 贴脸谱的原始顺序(固定常量)
const int faceOrder[] = {1, 2, 3, 4, 5, 6, 7}; 
// 自动计算脸谱数量,无需手动修改
const int colorCount = sizeof(faceOrder) / sizeof(faceOrder[0]);

// -------------------------- 静态顺序栈核心定义 --------------------------
// 元素类型别名:后续修改存储类型仅需改此行
typedef int ElemType;

// 静态顺序栈结构体定义
typedef struct {
    ElemType data[100];  // 静态数组,容量足够存储7个脸谱(预留冗余)
    int top;             // 栈顶指针:-1=空栈,0=第一个元素,以此类推
} Stack;

// -------------------------- 栈操作接口 --------------------------
/**
 * 函数名: initStack
 * 功能: 初始化静态顺序栈
 * 参数: s - 指向栈结构体的指针(传指针才能修改主函数中的栈)
 * 说明: 空栈的核心标志是top=-1
 */
void initStack(Stack *s) {
    s->top = -1; // 初始化栈顶指针为-1,标记栈为空
}

/**
 * 函数名: isEmpty
 * 功能: 判断栈是否为空
 * 参数: s - 指向栈的指针
 * 返回值: 1(空栈)、0(非空栈)
 */
int isEmpty(Stack *s) {
    return (s->top == -1) ? 1 : 0;
}

/**
 * 函数名: push
 * 功能: 入栈操作(对应"贴脸谱")
 * 参数: 
 *   s - 指向栈的指针
 *   e - 要入栈的元素(颜色编号)
 * 返回值: 1(入栈成功)、0(入栈失败,栈满)
 */
int push(Stack *s, ElemType e) {
    // 栈满判断:数组最大下标为99,top>=99时无法入栈
    if (s->top >= 99) {
        printf("入栈失败:栈已满\n");
        return 0;
    }
    s->top++;                // 栈顶指针上移一位,指向新位置
    s->data[s->top] = e;     // 将颜色编号存入新栈顶位置
    return 1;
}

/**
 * 函数名: pop
 * 功能: 出栈操作(对应"扯脸谱")
 * 参数:
 *   s - 指向栈的指针
 *   e - 指针,用于存储出栈的颜色编号(传出参数)
 * 返回值: 1(出栈成功)、0(出栈失败,栈空)
 */
int pop(Stack *s, ElemType *e) {
    // 栈空判断:无元素可出栈
    if (isEmpty(s)) {
        printf("出栈失败:栈为空\n");
        return 0;
    }
    *e = s->data[s->top];    // 取出栈顶元素(颜色编号)
    s->top--;                // 栈顶指针下移,逻辑上删除栈顶元素
    return 1;
}

/**
 * 函数名: printPushOrder
 * 功能: 打印贴脸顺序(栈底→栈顶)
 * 参数: s - 指向栈的指针
 */
void printPushOrder(Stack *s) {
    if (isEmpty(s)) {
        printf("栈为空,无脸谱可打印!\n");
        return;
    }
    printf("贴脸顺序(栈底→栈顶):");
    // 遍历数组:从下标0(第一个贴的)到top(最后一个贴的)
    for (int i = 0; i <= s->top; i++) {
        printf("%s ", getColorName(s->data[i]));
    }
    printf("\n\n");
}

// -------------------------- 主函数(业务逻辑) --------------------------
int main() {
    printf("========== 静态顺序栈实现川剧扯脸序列 ==========\n\n");
    
    // 1. 定义并初始化静态栈(内存分配在栈区,编译器自动管理)
    Stack faceStack;
    initStack(&faceStack);

    // 2. 执行"贴脸谱"(入栈)
    printf("【贴脸谱过程】\n");
    for (int i = 0; i < colorCount; i++) {
        push(&faceStack, faceOrder[i]);
        printf("贴第%d张脸谱:%s\n", i+1, getColorName(faceOrder[i]));
    }
    // 打印贴脸顺序,验证入栈结果
    printPushOrder(&faceStack);

    // 3. 执行"扯脸谱"(出栈),输出最终序列
    printf("【扯脸谱结果】\n");
    ElemType popCode; // 存储每次出栈的颜色编号
    printf("最终扯脸序列:");
    while (!isEmpty(&faceStack)) {
        pop(&faceStack, &popCode);
        printf("%s ", getColorName(popCode));
    }
    printf("\n\n===============================================\n");

    return 0;
}

三、方案二:动态顺序栈(进阶内存管理版)

核心特点

  • malloc动态分配数组内存,容量可灵活调整(本文仍设 100,仅演示逻辑);
  • 需手动调用free释放内存,理解 "堆区内存" 管理,避免内存泄漏。
cpp 复制代码
/*
 * 文件名: stack_dynamic_complete.c
 * 功能: 用动态顺序栈实现川剧扯脸序列求解
 * 适用人群: 进阶学习者,理解堆区内存管理
 * 核心特性: 动态数组存储,需手动malloc分配、free释放内存
 */
#include <stdio.h>
#include <stdlib.h>

// -------------------------- 全局常量与通用函数 --------------------------
// 颜色编号转字符串:1=白色, 2=黄色, 3=黑色, 4=红色, 5=青色, 6=金色, 7=银色
char* getColorName(int colorCode) {
    static const char* colorMap[] = {"未知颜色", "白色", "黄色", "黑色", "红色", "青色", "金色", "银色"};
    if (colorCode < 1 || colorCode > 7) {
        return (char*)colorMap[0];
    }
    return (char*)colorMap[colorCode];
}

// 贴脸谱顺序与数量(固定常量)
const int faceOrder[] = {1, 2, 3, 4, 5, 6, 7}; 
const int colorCount = sizeof(faceOrder) / sizeof(faceOrder[0]);

// -------------------------- 动态顺序栈核心定义 --------------------------
typedef int ElemType;

// 动态顺序栈结构体:数组指针指向堆区内存
typedef struct {
    ElemType *data;  // 动态数组指针(需手动分配/释放)
    int top;         // 栈顶指针:-1=空栈
} Stack;

// -------------------------- 栈操作接口 --------------------------
/**
 * 函数名: initStack
 * 功能: 初始化动态顺序栈(分配堆区内存)
 * 返回值: 指向栈结构体的指针(NULL表示内存分配失败)
 * 说明: 动态栈需先分配结构体内存,再分配数组内存
 */
Stack* initStack() {
    // 1. 为栈结构体分配内存
    Stack *s = (Stack*)malloc(sizeof(Stack));
    if (s == NULL) {
        printf("错误:栈结构体内存分配失败!\n");
        return NULL;
    }
    // 2. 为存储脸谱的数组分配内存(100个元素,足够使用)
    s->data = (ElemType*)malloc(sizeof(ElemType) * 100);
    if (s->data == NULL) {
        printf("错误:数组内存分配失败!\n");
        free(s); // 释放已分配的结构体,避免内存泄漏
        s = NULL;
        return NULL;
    }
    s->top = -1; // 初始化空栈
    return s;
}

/**
 * 函数名: isEmpty
 * 功能: 判断栈是否为空(增加指针非空检查,更健壮)
 * 参数: s - 指向栈的指针
 * 返回值: 1(空栈)、0(非空栈)
 */
int isEmpty(Stack *s) {
    // 先检查指针是否有效,再判断栈是否为空
    return (s != NULL && s->top == -1) ? 1 : 0;
}

/**
 * 函数名: push
 * 功能: 入栈操作(贴脸谱)
 * 参数: s - 栈指针;e - 颜色编号
 * 返回值: 1(成功)、0(失败)
 */
int push(Stack *s, ElemType e) {
    // 健壮性检查:指针为空或栈满,直接返回失败
    if (s == NULL || s->top >= 99) {
        printf("入栈失败:栈指针为空或栈已满\n");
        return 0;
    }
    s->data[++s->top] = e; // 栈顶指针上移 + 存入元素(简化写法)
    return 1;
}

/**
 * 函数名: pop
 * 功能: 出栈操作(扯脸谱)
 * 参数: s - 栈指针;e - 存储出栈元素的指针
 * 返回值: 1(成功)、0(失败)
 */
int pop(Stack *s, ElemType *e) {
    if (s == NULL || isEmpty(s)) {
        printf("出栈失败:栈指针为空或栈为空\n");
        return 0;
    }
    *e = s->data[s->top--]; // 取出元素 + 栈顶指针下移(简化写法)
    return 1;
}

/**
 * 函数名: printPushOrder
 * 功能: 打印贴脸顺序(栈底→栈顶)
 * 参数: s - 栈指针
 */
void printPushOrder(Stack *s) {
    if (s == NULL || isEmpty(s)) {
        printf("栈为空,无脸谱可打印!\n");
        return;
    }
    printf("贴脸顺序(栈底→栈顶):");
    for (int i = 0; i <= s->top; i++) {
        printf("%s ", getColorName(s->data[i]));
    }
    printf("\n\n");
}

/**
 * 函数名: destroyStack
 * 功能: 销毁动态栈(释放所有堆区内存)
 * 参数: s - 栈指针的指针(需修改原指针,避免野指针)
 * 说明: 动态内存必须手动释放,否则造成内存泄漏
 */
void destroyStack(Stack **s) {
    if (s == NULL || *s == NULL) return;
    free((*s)->data); // 第一步:释放数组内存
    free(*s);         // 第二步:释放结构体内存
    *s = NULL;        // 第三步:置空指针,避免野指针
}

// -------------------------- 主函数(业务逻辑) --------------------------
int main() {
    printf("========== 动态顺序栈实现川剧扯脸序列 ==========\n\n");
    
    // 1. 初始化动态栈(内存分配在堆区)
    Stack *faceStack = initStack();
    if (faceStack == NULL) { // 内存分配失败,直接退出程序
        return -1;
    }

    // 2. 执行贴脸谱(入栈)
    printf("【贴脸谱过程】\n");
    for (int i = 0; i < colorCount; i++) {
        push(faceStack, faceOrder[i]);
        printf("贴第%d张脸谱:%s\n", i+1, getColorName(faceOrder[i]));
    }
    // 打印贴脸顺序,验证入栈结果
    printPushOrder(faceStack);

    // 3. 执行扯脸谱(出栈)
    printf("【扯脸谱结果】\n");
    ElemType popCode;
    printf("最终扯脸序列:");
    while (!isEmpty(faceStack)) {
        pop(faceStack, &popCode);
        printf("%s ", getColorName(popCode));
    }
    printf("\n");

    // 4. 销毁栈(必做!释放堆区内存,避免泄漏)
    destroyStack(&faceStack);
    printf("\n提示:动态栈已销毁,内存释放完成\n");
    printf("===============================================\n");

    return 0;
}

四、方案三:链式栈(链表实战版)

核心特点

  • 链表节点存储栈元素,无需预先分配固定容量,按需创建节点;
  • 考察指针操作、链表头插法核心逻辑,理解 "逻辑上的栈" 与 "物理存储" 的分离。
cpp 复制代码
/*
 * 文件名: stack_linked_complete.c
 * 功能: 用链式栈实现川剧扯脸序列求解
 * 适用人群: 进阶学习者,理解链表与指针操作
 * 核心特性: 链表节点存储,无需固定容量,按需分配节点内存
 */
#include <stdio.h>
#include <stdlib.h>

// -------------------------- 全局常量与通用函数 --------------------------
// 颜色编号转字符串:1=白色, 2=黄色, 3=黑色, 4=红色, 5=青色, 6=金色, 7=银色
char* getColorName(int colorCode) {
    static const char* colorMap[] = {"未知颜色", "白色", "黄色", "黑色", "红色", "青色", "金色", "银色"};
    if (colorCode < 1 || colorCode > 7) {
        return (char*)colorMap[0];
    }
    return (char*)colorMap[colorCode];
}

// 贴脸谱顺序与数量(固定常量)
const int faceOrder[] = {1, 2, 3, 4, 5, 6, 7}; 
const int colorCount = sizeof(faceOrder) / sizeof(faceOrder[0]);

// -------------------------- 链式栈核心定义 --------------------------
typedef int ElemType;

// 栈节点结构体:存储数据+指向下一节点的指针
typedef struct StackNode {
    ElemType data;          // 数据域:存储颜色编号
    struct StackNode *next; // 指针域:指向后继节点
} StackNode;

// 链式栈管理结构体:仅需栈顶指针即可管理整个栈
typedef struct {
    StackNode *top; // 栈顶指针:指向第一个有效节点(空栈时为NULL)
} Stack;

// -------------------------- 栈操作接口 --------------------------
/**
 * 函数名: initStack
 * 功能: 初始化链式栈(创建管理结构体)
 * 返回值: 指向栈的指针(NULL表示分配失败)
 */
Stack* initStack() {
    Stack *s = (Stack*)malloc(sizeof(Stack));
    if (s == NULL) {
        printf("错误:栈管理结构体分配失败!\n");
        return NULL;
    }
    s->top = NULL; // 空栈:栈顶指针指向NULL
    return s;
}

/**
 * 函数名: isEmpty
 * 功能: 判断链式栈是否为空
 * 参数: s - 栈指针
 * 返回值: 1(空栈)、0(非空栈)
 */
int isEmpty(Stack *s) {
    return (s != NULL && s->top == NULL) ? 1 : 0;
}

/**
 * 函数名: push
 * 功能: 入栈操作(头插法,链表头部即为栈顶)
 * 参数: s - 栈指针;e - 颜色编号
 * 返回值: 1(成功)、0(失败)
 * 说明: 链式栈入栈=链表头插,无需考虑栈满(内存足够即可)
 */
int push(Stack *s, ElemType e) {
    if (s == NULL) {
        printf("入栈失败:栈指针为空\n");
        return 0;
    }
    // 1. 创建新节点并分配内存
    StackNode *newNode = (StackNode*)malloc(sizeof(StackNode));
    if (newNode == NULL) {
        printf("错误:新节点内存分配失败!\n");
        return 0;
    }
    // 2. 新节点赋值
    newNode->data = e;
    // 3. 头插法核心:新节点指向原栈顶
    newNode->next = s->top;
    // 4. 栈顶指针更新为新节点
    s->top = newNode;
    return 1;
}

/**
 * 函数名: pop
 * 功能: 出栈操作(删除栈顶节点)
 * 参数: s - 栈指针;e - 存储出栈元素的指针
 * 返回值: 1(成功)、0(失败)
 */
int pop(Stack *s, ElemType *e) {
    if (s == NULL || isEmpty(s)) {
        printf("出栈失败:栈指针为空或栈为空\n");
        return 0;
    }
    // 1. 临时指针指向栈顶节点(避免断链)
    StackNode *temp = s->top;
    // 2. 取出栈顶节点数据
    *e = temp->data;
    // 3. 栈顶指针后移,指向原第二个节点
    s->top = temp->next;
    // 4. 释放原栈顶节点内存(避免泄漏)
    free(temp);
    return 1;
}

/**
 * 函数名: printPushOrder
 * 功能: 打印贴脸顺序(栈顶→栈底,链表遍历方向)
 * 参数: s - 栈指针
 */
void printPushOrder(Stack *s) {
    if (s == NULL || isEmpty(s)) {
        printf("栈为空,无脸谱可打印!\n");
        return;
    }
    printf("贴脸顺序(栈顶→栈底):");
    StackNode *p = s->top;
    while (p != NULL) {
        printf("%s ", getColorName(p->data));
        p = p->next;
    }
    printf("\n\n");
}

/**
 * 函数名: destroyStack
 * 功能: 销毁链式栈(释放所有节点+管理结构体)
 * 参数: s - 栈指针的指针
 */
void destroyStack(Stack **s) {
    if (s == NULL || *s == NULL) return;
    // 第一步:遍历所有节点并释放
    StackNode *p = (*s)->top;
    while (p != NULL) {
        StackNode *temp = p;
        p = p->next;
        free(temp);
    }
    // 第二步:释放管理结构体
    free(*s);
    *s = NULL;
}

// -------------------------- 主函数(业务逻辑) --------------------------
int main() {
    printf("========== 链式栈实现川剧扯脸序列 ==========\n\n");
    
    // 1. 初始化链式栈
    Stack *faceStack = initStack();
    if (faceStack == NULL) {
        return -1;
    }

    // 2. 执行贴脸谱(入栈=链表头插)
    printf("【贴脸谱过程】\n");
    for (int i = 0; i < colorCount; i++) {
        push(faceStack, faceOrder[i]);
        printf("贴第%d张脸谱:%s\n", i+1, getColorName(faceOrder[i]));
    }
    // 打印贴脸顺序,验证入栈结果
    printPushOrder(faceStack);

    // 3. 执行扯脸谱(出栈=删除链表头节点)
    printf("【扯脸谱结果】\n");
    ElemType popCode;
    printf("最终扯脸序列:");
    while (!isEmpty(faceStack)) {
        pop(faceStack, &popCode);
        printf("%s ", getColorName(popCode));
    }
    printf("\n");

    // 4. 销毁栈(释放所有节点内存)
    destroyStack(&faceStack);
    printf("\n提示:链式栈已销毁,内存释放完成\n");
    printf("===============================================\n");

    return 0;
}

五、核心总结

  1. 逻辑统一:3 种实现的核心逻辑均围绕 "入栈(贴脸)→ 出栈(扯脸)",栈 "先进后出" 的特性是求解问题的关键;
  2. 存储差异
    • 静态顺序栈:数组固定容量,内存自动管理,简单但不灵活;
    • 动态顺序栈:数组动态分配,需手动释放内存,灵活但需注意泄漏;
    • 链式栈:链表节点按需分配,无容量限制,考察指针与链表操作;

本文通过川剧 "扯脸" 的趣味场景,将抽象的栈结构落地为可运行的 C 语言代码,从入门到进阶层层递进,希望能帮助大家理解栈的核心特性与实际应用。

相关推荐
3GPP仿真实验室2 小时前
【MATLAB源码】感知:CFAR 检测算法库
算法·matlab·目标跟踪
fengenrong2 小时前
20260324
c++·算法
qq_416018722 小时前
设计模式在C++中的实现
开发语言·c++·算法
倾心琴心2 小时前
【agent辅助pcb routing coding学习】实践9 CU GR 代码 算法学习
算法·agent·pcb·eda·routing
数据智能老司机2 小时前
谷歌 TurboQuant 深度拆解:LLM 内存压缩 6 倍、推理加速 8 倍、零精度损失,它是怎么做到的?
算法
2301_776508722 小时前
C++与机器学习框架
开发语言·c++·算法
Albertbreak2 小时前
STL容器内部实现剖析
开发语言·c++·算法
CoovallyAIHub2 小时前
AAAI 2026 | AnoStyler:文本驱动风格迁移实现零样本异常图像生成,轻量高效(附代码)
算法·架构·github
2301_795741792 小时前
模板编译期机器学习
开发语言·c++·算法