数据结构之栈

一、栈的基础认知(补充核心资料内容)

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;
}
测试说明
  1. 创建test_bracket.c文件,写入包含错误括号的代码(如{[)];);

  2. 编译运行程序,会输出:

    错误:括号不匹配,栈顶左括号 '['(行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展开;
  • 内存管理是链式栈的关键,必须保证mallocfree成对出现,避免内存泄漏;
  • clen字段是优化点,可将栈大小统计、判空的时间复杂度从 O (n) 降至 O (1);
  • 栈的分类(空增 / 空减 / 满增 / 满减)核心是top指针的变化规则,需结合实际场景选择。

2. 学习建议

  • 基础巩固:手动模拟入栈、出栈过程(画链表图),理解指针变化;同时对比顺序栈(数组实现)的top指针规则,区分数据结构栈与系统栈的差异;
  • 调试练习:在核心操作中增加打印(如入栈后打印栈顶值、栈大小),观察栈状态;
  • 扩展优化:
    • 为括号匹配增加 "忽略注释 / 字符串中括号" 的逻辑;
    • 为表达式求值增加括号嵌套支持(如(20+5)*3);
    • 实现链式栈的 "清空""遍历" 操作;
    • 尝试基于 "空增栈 / 空减栈" 规则改造链式栈的top指针逻辑。
相关推荐
爱学习的梵高先生1 小时前
C++:基础知识
开发语言·c++·算法
xlq223221 小时前
24.map set(下)
数据结构·c++·算法
繁华似锦respect2 小时前
C++ & Linux 中 GDB 调试与内存泄漏检测详解
linux·c语言·开发语言·c++·windows·算法
立志成为大牛的小牛2 小时前
数据结构——五十四、处理冲突的方法——开放定址法(王道408)
数据结构·学习·程序人生·考研·算法
子一!!2 小时前
数据结构===红黑树===
数据结构
代码游侠2 小时前
复习——栈、队列、树、哈希表
linux·数据结构·学习·算法
碧海银沙音频科技研究院2 小时前
基于物奇wq7036与恒玄bes2800智能眼镜设计
arm开发·人工智能·深度学习·算法·分类
小白程序员成长日记3 小时前
2025.12.03 力扣每日一题
算法·leetcode·职场和发展
元亓亓亓3 小时前
LeetCode热题100--20. 有效的括号--简单
linux·算法·leetcode