数据结构 -- 栈
栈的定义与特性
栈是一种受限线性表,仅允许在表的一端(栈顶)进行插入(入栈)、删除(出栈)操作,遵循 “先进后出(LIFO,Last In First Out)” 原则。
栈的核心结构
- 栈顶(Top):允许进行插入(入栈)、删除(出栈)操作的一端,动态变化(随元素增减移动 )。
- 栈底(Bottom):不允许直接操作(插入 / 删除)的一端,位置相对固定(栈初始化时确定 )。
栈的典型操作
- 入栈(Push):在栈顶添加新元素,栈顶指针上移。
- 出栈(Pop):移除并返回栈顶元素,栈顶指针下移(栈非空时操作 )。
- 获取栈顶元素(GetTop/Peek):返回栈顶元素值,但不删除该元素(栈非空时操作 )。
- 判空(IsEmpty):检查栈是否无元素,辅助判断操作合法性(如避免空栈出栈报错 )。
栈的应用场景
依托 “先进后出” 特性,栈在编程中解决多种问题:
- 递归实现:编译器用栈保存递归调用的上下文(参数、局部变量等 ),递归返回时按栈序恢复。
- 表达式求值:如计算 “3 + (2 * 5)”,用栈暂存操作数、运算符,按优先级和括号匹配处理。
- 括号匹配:遍历代码 / 字符串,左括号入栈、右括号尝试匹配栈顶左括号,最终栈空则匹配成功。
- 浏览器历史记录:前进 / 后退功能,用栈保存访问页面,回退时按栈序弹出历史页。
栈的实现方式通常有 顺序栈(数组基实现) 和 链栈(链表基实现)
栈的操作函数
#include "LinkStack.h" // 包含链栈的头文件,定义了LinkStack、LinkStackNode、DATATYPE等结构体
#include <stdio.h> // 标准输入输出库,用于错误信息打印
#include <stdlib.h> // 标准库,提供malloc、free等内存管理函数
#include <string.h> // 字符串处理库,提供memcpy等内存复制函数/*** @brief 创建一个空的链栈* * @return LinkStack* 成功返回链栈指针,失败返回NULL*/
LinkStack* CreateLinkStack()
{// 为链栈结构体分配内存LinkStack* ls = malloc(sizeof(LinkStack));if (NULL == ls) // 内存分配失败{perror("CreateLinkStack malloc error\n"); // 打印错误信息return NULL;}// 初始化栈顶指针为NULL(空栈)ls->top = NULL;// 初始化栈中元素个数为0ls->clen = 0;return ls; // 返回创建的空栈
}/*** @brief 入栈操作,在栈顶增加元素(底层使用链表头插法实现)** @param ls 待操作的链栈指针* @param data 需要入栈的元素指针* @return int 0表示成功,非0表示失败*/
int PushLinkStack(LinkStack* ls, DATATYPE* data)
{// 为新节点分配内存LinkStackNode* newnode = malloc(sizeof(LinkStackNode));if (NULL == newnode) // 内存分配失败{perror("PushLinkStack malloc error\n"); // 打印错误信息return 1; // 返回失败状态}// 将待入栈的数据复制到新节点中(深拷贝,避免外部数据修改影响栈内数据)memcpy(&newnode->data, data, sizeof(DATATYPE));newnode->next = NULL; // 新节点的next指针初始化为NULL// 头插法:新节点的next指向当前栈顶节点newnode->next = ls->top;// 更新栈顶指针为新节点(新节点成为新的栈顶)ls->top = newnode;// 栈中元素个数加1ls->clen++;return 0; // 入栈成功
}/*** @brief 出栈操作,删除栈顶元素(底层使用链表头删法实现)** @param ls 待操作的链栈指针* @return int 0表示成功,非0表示失败*/
int PopLinkStack(LinkStack* ls)
{// 检查栈是否为空,为空则无法出栈if (IsEmptyLinkStack(ls)){printf("linkstack is empty\n"); // 打印错误信息return 1; // 返回失败状态}// 保存当前栈顶节点的地址(避免释放后丢失指针)LinkStackNode* tmp = ls->top;// 更新栈顶指针为下一个节点(原栈顶的下一个节点成为新栈顶)ls->top = ls->top->next;// 释放原栈顶节点的内存free(tmp);// 栈中元素个数减1ls->clen--;return 0; // 出栈成功
}/*** @brief 获取栈顶元素(不删除元素)** @param ls 待操作的链栈指针* @return DATATYPE* 成功返回栈顶元素的指针,失败返回NULL*/
DATATYPE* GetTopLinkStack(LinkStack* ls)
{// 检查栈是否为空,为空则无栈顶元素if (IsEmptyLinkStack(ls)){return NULL; // 返回NULL表示失败}// 返回栈顶节点中数据的地址return &ls->top->data;
}/*** @brief 判断栈是否为空** @param ls 待操作的链栈指针* @return int 1表示栈为空,0表示栈不为空*/
int IsEmptyLinkStack(LinkStack* ls)
{// 通过栈中元素个数是否为0判断栈是否为空return 0 == ls->clen;
}/*** @brief 获取栈中元素的个数** @param ls 待操作的链栈指针* @return int 栈中元素的个数*/
int GetSizeLinkStack(LinkStack* ls)
{// 直接返回栈的长度属性return ls->clen;
}/*** @brief 销毁链栈,释放所有分配的内存** @param ls 待销毁的链栈指针* @return int 0表示成功*/
int DestroyLinkStack(LinkStack* ls)
{// 循环出栈,直到栈为空while (!IsEmptyLinkStack(ls)){PopLinkStack(ls); // 出栈操作会释放每个节点的内存,并减少元素计数}// 释放链栈结构体本身的内存free(ls);return 0; // 销毁成功
}
练习
1、括号匹配检查程序,用于检测 C 语言源文件中圆括号()
、方括号[]
、花括号{}
的匹配情况。程序通过 "栈" 这种数据结构实现括号匹配检查,能定位并输出不匹配括号的位置(行号和列号),帮助排查代码中的语法错误。
#include <stdio.h> // 标准输入输出库,用于文件操作和打印
#include <string.h> // 字符串处理库,用于内存初始化等操作
#include "LinkStack.h"// 自定义链表栈头文件,提供栈的相关操作/*** 检查一行字符中的括号匹配情况* @param linebuf 待检查的行缓冲区* @param ls 栈指针,用于存储左括号信息* @param num 当前行的行号* @return 0表示当前行检查正常,1表示发现不匹配错误*/
int do_check(char* linebuf, LinkStack* ls, int num)
{char* tmp = linebuf; // 临时指针,用于遍历行缓冲区中的每个字符DATATYPE data = {0}; // 存储括号信息的结构体(包含括号字符、行号、列号)DATATYPE* top = NULL; // 指向栈顶元素的指针,用于检查匹配关系int col = 1; // 记录当前字符在该行中的列号(从1开始计数)// 遍历行缓冲区中的每个字符,直到遇到字符串结束符'\0'while (*tmp){bzero(&data, sizeof(data)); // 清空data结构体,避免残留数据影响后续使用// 根据当前字符类型进行不同处理switch (*tmp){// 遇到左括号:入栈保存(记录括号类型和位置)case '(':case '[':case '{':data.c = *tmp; // 存储当前左括号字符data.col = col; // 记录当前列号data.row = num; // 记录当前行号PushLinkStack(ls, &data); // 将左括号信息压入栈break;// 遇到右圆括号')':检查栈顶是否为对应的左圆括号'('case ')':top = GetTopLinkStack(ls); // 获取栈顶元素(最近的左括号)if (NULL == top) // 栈为空:没有对应的左括号,右括号多余{printf("当前符号错误 %c 行:%d 列:%d\n", *tmp, num, col);return 1; // 返回错误状态}// 栈顶是对应的左括号'(':弹出栈顶(匹配成功)if (top->c == '('){PopLinkStack(ls);}else // 栈顶不是对应的左括号:括号类型不匹配{printf("当前符号错误 %c 行:%d 列:%d 或 栈顶符号 %c 行:%d 列:%d\n",*tmp, num, col, top->c, top->row, top->col);return 1; // 返回错误状态}break;// 遇到右方括号']':检查栈顶是否为对应的左方括号'['case ']':top = GetTopLinkStack(ls); // 获取栈顶元素if (NULL == top) // 栈为空:右括号多余{printf("当前符号错误 %c 行:%d 列:%d\n", *tmp, num, col);return 1;}// 栈顶是对应的左方括号'[':弹出栈顶(匹配成功)if (top->c == '['){PopLinkStack(ls);}else // 类型不匹配{printf("当前符号错误 %c 行:%d 列:%d 或 栈顶符号 %c 行:%d 列:%d\n",*tmp, num, col, top->c, top->row, top->col);return 1;}break;// 遇到右花括号'}':检查栈顶是否为对应的左花括号'{'case '}':top = GetTopLinkStack(ls); // 获取栈顶元素if (NULL == top) // 栈为空:右括号多余{printf("当前符号错误 %c 行:%d 列:%d\n", *tmp, num, col);return 1;}// 栈顶是对应的左花括号'{':弹出栈顶(匹配成功)if (top->c == '{'){PopLinkStack(ls);}else // 类型不匹配{printf("当前符号错误 %c 行:%d 列:%d 或 栈顶符号 %c 行:%d 列:%d\n",*tmp, num, col, top->c, top->row, top->col);return 1;}break;}tmp++; // 移动到下一个字符col++; // 列号自增(记录下一个字符的位置)}return 0; // 当前行检查无错误
}int main(int argc, char** argv)
{LinkStack* ls = CreateLinkStack(); // 创建一个空的链表栈,用于存储左括号// 打开待检查的文件(这里固定为"/home/linux/2.c")FILE* fp = fopen("/home/linux/2.c", "r");if (NULL == fp) // 文件打开失败{printf("文件打开失败");return 1;}int num = 1; // 记录当前处理的行号(从1开始计数)// 循环读取文件中的每一行,直到文件结束while (1){char buf[1024] = {0}; // 缓冲区,用于存储读取到的一行内容// 读取一行内容到buf(最多1023个字符,避免溢出)if (NULL == fgets(buf, sizeof(buf), fp)){break; // 读取失败(文件结束),退出循环}// 检查当前行的括号匹配情况int ret = do_check(buf, ls, num);if (1 == ret) // 发现错误{fclose(fp); // 关闭文件DestroyLinkStack(ls); // 销毁栈,释放内存return 1; // 程序异常退出}num++; // 行号自增(准备处理下一行)}// 所有行处理完毕后,检查栈是否为空if (IsEmptyLinkStack(ls)){printf("括号匹配正常"); // 栈为空:所有左括号都已匹配}else{// 栈不为空:存在未匹配的左括号,输出栈顶左括号的位置(最内层未匹配的左括号)DATATYPE* top = GetTopLinkStack(ls);printf("栈顶符号 %c 行:%d 列:%d 未匹配\n", top->c, top->row, top->col);}// 清理资源fclose(fp); // 关闭文件DestroyLinkStack(ls); // 销毁栈,释放内存return 0; // 程序正常退出
}
2、使用 "双栈法" 解析算术表达式,通过两个栈分别存储数字和运算符,根据运算符优先级决定何时进行计算。
#include <stdio.h> // 标准输入输出库
#include <string.h> // 字符串处理库,提供bzero等函数
#include "LinkStack.h" // 自定义链栈头文件,用于栈操作int num = 0; // 全局变量,用于临时存储解析出的数字/*** @brief 将字符形式的数字累积转换为整数* @param c 字符形式的数字('0'~'9')*/
void get_num(char c)
{// 将当前字符数字累加到num中(例如"123"会被解析为1*10+2=12,再12*10+3=123)num = num * 10 + c - '0';
}/*** @brief 获取运算符的优先级* @param c 运算符(+、-、*、/)* @return 优先级:1(+、-),2(*、/),0(其他字符)*/
int get_priority(char c)
{switch (c){case '+':case '-':return 1; // 加减优先级较低case '*':case '/':return 2; // 乘除优先级较高default:return 0; // 非运算符返回0}
}/*** @brief 计算两个数的运算结果* @param num1 第一个操作数(左操作数)* @param num2 第二个操作数(右操作数)* @param c 运算符* @return 运算结果*/
int get_result(int num1, int num2, char c)
{switch (c){case '+':return num1 + num2; // 加法case '-':return num1 - num2; // 减法case '*':return num1 * num2; // 乘法case '/':return num1 / num2; // 除法(整数除法)}return 0; // 理论上不会执行到这里
}int main(int argc, char** argv)
{char* express = "20*3+5"; // 待计算的表达式LinkStack* ls_num; // 数字栈:用于存储运算过程中的数字LinkStack* ls_sym; // 符号栈:用于存储运算符char* tmp; // 临时指针,用于遍历表达式DATATYPE* top; // 指向栈顶元素的指针DATATYPE data; // 用于存储栈操作的数据// 创建两个栈ls_num = CreateLinkStack();ls_sym = CreateLinkStack();tmp = express; // 指向表达式的起始位置// 遍历表达式中的每个字符while (*tmp){bzero(&data, sizeof(data)); // 清空data结构体// 判断当前字符是否为数字if (*tmp >= '0' && *tmp <= '9'){get_num(*tmp); // 累积数字(处理多位数)tmp++; // 移动到下一个字符continue; // 继续处理下一个字符}// 如果不是数字,说明遇到了运算符,将之前累积的数字入栈data.num = num; // 将全局变量num的值存入datanum = 0; // 重置num,准备解析下一个数字PushLinkStack(ls_num, &data); // 将数字压入数字栈// 处理运算符入栈逻辑(保证栈顶运算符优先级不高于当前运算符)while (1){top = GetTopLinkStack(ls_sym); // 获取符号栈顶元素// 如果符号栈为空,或栈顶运算符优先级低于当前运算符if (IsEmptyLinkStack(ls_sym) ||get_priority(top->sym) < get_priority(*tmp)){// 将当前运算符入栈bzero(&data, sizeof(data));data.sym = *tmp; // 存储当前运算符PushLinkStack(ls_sym, &data); // 压入符号栈break; // 退出循环,继续处理表达式下一个字符}else{// 栈顶运算符优先级高于等于当前运算符,先计算栈顶运算符的结果// 从数字栈弹出两个操作数(注意顺序:后弹出的是左操作数)top = GetTopLinkStack(ls_num);int num2 = top->num; // 右操作数PopLinkStack(ls_num);top = GetTopLinkStack(ls_num);int num1 = top->num; // 左操作数PopLinkStack(ls_num);// 从符号栈弹出运算符top = GetTopLinkStack(ls_sym);char op = top->sym; // 运算符PopLinkStack(ls_sym);// 计算结果并将结果压入数字栈int result = get_result(num1, num2, op);bzero(&data, sizeof(data));data.num = result;PushLinkStack(ls_num, &data);}}tmp++; // 移动到下一个字符}// 表达式遍历结束后,将最后一个数字入栈data.num = num;num = 0;PushLinkStack(ls_num, &data);// 处理符号栈中剩余的运算符(计算所有剩余运算)while (!IsEmptyLinkStack(ls_sym)){// 弹出两个操作数top = GetTopLinkStack(ls_num);int num2 = top->num;PopLinkStack(ls_num);top = GetTopLinkStack(ls_num);int num1 = top->num;PopLinkStack(ls_num);// 弹出运算符并计算top = GetTopLinkStack(ls_sym);char op = top->sym;PopLinkStack(ls_sym);int result = get_result(num1, num2, op);// 将结果入栈bzero(&data, sizeof(data));data.num = result;PushLinkStack(ls_num, &data);}// 获取最终计算结果(数字栈中仅剩的一个元素)top = GetTopLinkStack(ls_num);int result = top->num;PopLinkStack(ls_num);// 输出结果printf("result %d\n", result); // 对于表达式"20*3+5",结果应为65// 销毁栈,释放内存DestroyLinkStack(ls_num);DestroyLinkStack(ls_sym);return 0;
}