栈的链式储存结构及应用:四则运算表达式求值(中缀表达式转化为后缀表达式并求值)

目录

前言:好久没写过百来行的代码了,难得有机会练练手,最近重温了栈的链式储存结构,照着资料上用栈处理四则运算的思路,写了段代码,期间遇到了不少问题与知识遗漏之处,故记录之。

真的被指针和内存访问薄纱TAT

话不多说,先贴代码。😐

代码全览

/*/////////////////////////////////////
通过链栈将中缀表达式转换为后缀表达式并计算出结果
2022·10·14:完成转换部分
2022·10·15/16:完成运算部分
待实现:
支持小数运算
*//////////////////////////////////////
#include<stdio.h>
#include<string>
#include<stdlib.h>
#include<math.h>
#define MAX_LENGTH 100
#define NUM 1
#define OPERATOR 0
#define DELIMITER '#'
//判断字符a是否为数字(注:宏定义判断表达式时,建议带上括号,以免取反时出错)
#define ISNUM(a) (a >= '0' && a <= '9')
//判断运算符a的优先级是否不低于或等于(遗漏!)运算符b
#define ISPRIORITY(a, b) (((a == '*' || a == '/') && (b == '-' || b == '+')) || ((a == '+' || a == '-') && b == '('))
#define CHAR_TO_NUM(a) (a - '0')
typedef bool Type;
typedef int Info;
//链表结点的信息
typedef struct StackNode
{
Info info;//字符信息
StackNode *next;//指向下一个结点
}StackNode, *StackPtr;
//头结点,保存链栈的整体信息
typedef struct LinkStack
{
Type type;//结点变量info的类型(char/int)
int count;
StackPtr top;//指向首结点
}LinkStack;
StackNode* Pop(LinkStack *link);
void Push(int info, LinkStack *link);
void Inquire_Print(Type type, Info info);
char *Translate(LinkStack *link, char *_formula);
int Calculate(LinkStack *link, char *formula_);
//出栈操作
StackNode* Pop(LinkStack *link)
{
StackNode* pop = link->top;
//更新头节点的信息
link->top = ((link->top)->next);
link->count --;
//出栈的结点指向的下一个结点为空
pop->next = NULL;
Inquire_Print(link->type, pop->info);
printf("已出栈\n");
return pop;
}
void Push(int info, LinkStack *link)
{
/*
2022·10·13
函数内定义的变量地址固定,因此需要为入栈结点动态分配内存
否则每次入栈的结点实际上为同一个结点(因为地址相同)
方法一: 通过malloc()动态分配内存
方法二: 通过free()释放被分配的内存
*/
//开辟一处空间储存新入栈的结点
/*
函数malloc
包含于stdlib.h
返回一个void类型的地址,建议强制类型转换为所需类型
*/
/*
函数malloc
包含于stdlib.h
返回一个void类型的地址,建议强制类型转换为所需类型
*/
StackPtr push = (StackPtr)malloc(sizeof(StackNode));
push->info = info;
push->next = link->top;
link->top = push;
link->count ++;
Inquire_Print(link->type, push->info);
printf("已入栈\n");
}
void TraverseNode(LinkStack *link)
{
StackPtr nodePtr = link->top;
printf("当前栈内情况:\n");
while(nodePtr != NULL)
{
if(link->type == NUM)
{
printf("%3d\n", nodePtr->info);
}
if(link->type == OPERATOR)
{
printf("%3c\n", nodePtr->info);
}
nodePtr = nodePtr->next;
}
}
void Inquire_Print(Type type, Info info)
{
switch(type)
{
case NUM:printf("数字%d", info);break;
case OPERATOR:printf("字符%c", info);break;
}
}
char* Translate(LinkStack *link, char *_formula)
{
//当前栈内的信息类型为运算符
link->type = OPERATOR;
char *formula_ = (char *) malloc(sizeof(char[MAX_LENGTH]));
/*
遍历各个字符,将中缀表达式转换为后缀表达式
*/
//i,j分别为遍历两个表达式的下标,i随循环自增,j随元素放入自增
for(int i = 0, j = 0; _formula[i] != '\0'; i ++)
{
printf("遍历到%c\n", _formula[i]);
//如果字符为数字,则放入后缀表达式中
if(ISNUM(_formula[i]))
{
printf("放入后缀表达式中\n");
formula_[j ++] = _formula[i];
if(!ISNUM(_formula[i + 1]))
{
printf("单个值遍历完毕,添加分隔符!\n");
formula_[j ++] = DELIMITER;
}
}
else if (_formula[i] == '(')
{
Push(_formula[i], link);
}
else if(_formula[i] == ')')
{
if(link->top != NULL)
{
while(link->top->info != '(')
{
formula_[j ++] = Pop(link)->info;
//找不到左空格
if(link->top != NULL)
{
pritnf("错误:无法匹配左括号’(‘,即将越界访问内存!\n");
}
}
Pop(link);
}
else
{
printf("Error!\n");
return NULL;
}
}
else
{
/*
如果待入栈运算符的优先级不高于(如果包含等于会导致结合方向出错)栈顶运算符,则入栈
否则弹出栈顶运算符,直至条件成立再入栈
*/
//防止指针越界访问内存导致程序错误
if(link->top != NULL)
{
while(!ISPRIORITY(_formula[i], link->top->info))
{
printf("待入栈运算符优先级较低,栈顶运算符出栈!\n");
formula_[j ++] = Pop(link)->info;
TraverseNode(link);
//防止指针越界访问内存导致程序错误
if(link->top == NULL)
break;
}
}
Push(_formula[i], link);
TraverseNode(link);
}
/*
如果前缀表达式已经遍历完毕,
则将栈内运算符弹出放入后缀表达式,补上结束符'\0'
*/
if(_formula[i + 1] == '\0')
{
printf("遍历完成,全部出栈!\n");
while(link->top != NULL)
{
formula_[j ++] = Pop(link)->info;
TraverseNode(link);
}
formula_[j] = '\0';
}
}
return formula_;
}
int Calculate(LinkStack *link, char *formula_)
{
int result = 999999;
int numTmp = 0;
printf("开始运算:\n");
link->type = NUM;
for(int i = 0; formula_[i] != '\0'; i ++)
{
if(ISNUM(formula_[i]))
{
numTmp += CHAR_TO_NUM(formula_[i]);
printf("数字%d放入待入栈值的个位\n", numTmp);
if(formula_[i + 1] != DELIMITER)
{
numTmp *= 10;
printf("后方有连续数字,待入栈值提高一位\n", numTmp);
}
else
{
Push(numTmp, link);
numTmp = 0;
printf("单个值已获取,入栈\n", numTmp);
}
}
else if(formula_[i] == '+')
{
int sum = Pop(link)->info + Pop(link)->info;
printf("加法运算,和%d入栈\n", sum);
Push(sum, link);
}
else if(formula_[i] == '-')
{
int b = Pop(link)->info;
int a = Pop(link)->info;
Push(a - b, link);
printf("减法运算,差%d入栈\n", a - b);
}
else if(formula_[i] == '*')
{
int mulitiply = Pop(link)->info * Pop(link)->info;
Push(mulitiply, link);
printf("乘法运算,积%d入栈\n", mulitiply);
}
else if(formula_[i] == '/')
{
int b = Pop(link)->info;
int a = Pop(link)->info;
Push(a / b, link);
printf("除法运算, 商%d入栈\n", a / b);
}
}
return result = Pop(link)->info;
}
int main()
{
char _formula[MAX_LENGTH]; //中缀表达式
char *formula_; //后缀表达式
int result = 99999;
formula_ = (char *) malloc(sizeof(char[MAX_LENGTH]));
LinkStack linkStack; //头节点
linkStack.top = NULL; //头节点
linkStack.count = 0;
printf("输入一个中缀表达式:\n");
//gets函数在C/C++11后被弃用,C/C++14后被移除
gets(_formula);
formula_ = Translate(&linkStack, _formula);
printf("转化为后缀表达式:");
puts(formula_);
result = Calculate(&linkStack, formula_);
printf("运算结果为:%d", result);
getchar();
return 0;
}

总结一下遇到的问题吧

遇到的问题一:Segmentation Fault(存储器区块错误)

作为出现次数最多的报错,每次看到SF的红框框都能让我眼前一~~暗

导致此类错误的原因大致有以下:

1. 字符数组越界访问

尤其是通过字符指针存储字符串时,要注意用malloc为其开辟内存,也就是分配好一定大小的内存(否则?鬼知道下标会把指针带到哪片非法之地

formula_ = (char *) malloc(sizeof(char[MAX_LENGTH]));

2. 指针越界访问内存

下面这段代码的功能是为遍历到的右括号匹配到栈内的左括号,并且将其间的运算符出栈,由此转换为后缀形式的表达式,其中写了不少防止指针越界访问的代码。
Warning:如果逻辑设计有误,导致左括号没有入栈,则会卡在循环无限出栈直到访问到非法的内存!😒所以涉及需要遍历链表匹配元素的功能,不仅要设计好预防代码,还要保证入栈逻辑正确,使匹配完成(●'◡'●)

else if(_formula[i] == ')')
{
if(link->top != NULL)
{
while(link->top->info != '(')
{
formula_[j ++] = Pop(link)->info;
//找不到左空格
if(link->top != NULL)
{
pritnf("错误:无法匹配左括号’(‘,即将越界访问内存!\n");
}
}
Pop(link);
}
else
{
printf("Error!\n");
return NULL;
}
}

遇到的问题二:同一结点反复入栈(错误理解函数内的变量声明)

以前我一直以为自定义函数内的变量在每次调用时都会重新声明,所以可以以此实例化一个新结点,这是错误的!😅😅😅
在DevC++上和Visual Studio运行以下代码段,输出相应函数内变量的地址,可以发现每次调用时,其变量的地址都是相同的。

typedef struct Node{
int info;
Node* next;
}Node, *NodePtr;
void Test()
{
int a;
char b;
NodePtr c;
printf("&a = %p\n", &a);
printf("&b = %p\n", &b);
printf("c = %p\n", c);
}
int main(void)
{
for(int i = 0; i < 5; i ++)
Test();
return 0;
}

所以,如果将错就错,以此新建结点入栈,得到的结果是:一个只有单个结点,且节点内元素反复变化,首尾相连的单链表!

遇到的问题三:对字符串如何标记和提取单个数值

其实这是来凑数的(bushi

定义一个临时变量,将当前遍历到的数字加入,如果下次遍历到的元素还是数字,则将临时变量提高一位,代码如下。

if(ISNUM(formula_[i]))
{
numTmp += CHAR_TO_NUM(formula_[i]);
printf("数字%d放入待入栈值的个位\n", numTmp);
if(formula_[i + 1] != DELIMITER)
{
numTmp *= 10;
printf("后方有连续数字,待入栈值提高一位\n", numTmp);
}
else
{
Push(numTmp, link);
numTmp = 0;
printf("单个值已获取,入栈\n", numTmp);
}
}

将来可能会实现的功能

支持小数运算


关键在识别并处理小数点及小数部分,大致思路与提取整数相似,可以另外声明一个float型变量,通过降低位数实现小数部分的获取😸

10.17完成, 下面时代码修改部分

计算部分

float Calculate(LinkStack *link, char *formula_)
{
float result = 999999;
int numTmp = 0;
float floatTmp = 0.0;
bool isDecimal = false;
printf("开始运算:\n");
link->type = NUM;
for(int i = 0; formula_[i] != '\0'; i ++)
{
if(ISNUM(formula_[i]))
{
//识别整数位与小数位
switch(isDecimal)
{
case true: floatTmp += CHAR_TO_NUM(formula_[i]); floatTmp /= 10.0; break;
case false: numTmp += CHAR_TO_NUM(formula_[i]); numTmp *= 10; break;
}
//遇到分隔符,数值提取完毕,入栈等待运算
if(formula_[i + 1] == DELIMITER)
{
/*这里的numTmp从个位进到十位,有多余的进位
floatTmp从个位缩到小数点后一位,没有多余的缩位*/
Push(numTmp/10 + floatTmp, link);//wrong
isDecimal = false;
numTmp = floatTmp = 0;
printf("单个值已获取,入栈\n", numTmp);
}
}
//识别到小数点,之后操作的数字都为小数
if(formula_[i] == '.')
{
isDecimal = true;
}
else if(formula_[i] == '+')
{
float sum = Pop(link)->info + Pop(link)->info;
printf("加法运算,和%f入栈\n", sum);
Push(sum, link);
}
else if(formula_[i] == '-')
{
float b = Pop(link)->info;
float a = Pop(link)->info;
Push(a - b, link);
printf("减法运算,差%f入栈\n", a - b);
}
else if(formula_[i] == '*')
{
float mulitiply = Pop(link)->info * Pop(link)->info;
Push(mulitiply, link);
printf("乘法运算,积%f入栈\n", mulitiply);
}
else if(formula_[i] == '/')
{
float b = Pop(link)->info;
float a = Pop(link)->info;
Push(a / b, link);
printf("除法运算, 商%f入栈\n", a / b);
}
}
return result = Pop(link)->info;
}

转换部分

char* Translate(LinkStack *link, char *_formula)
{
//当前栈内的信息类型为运算符
link->type = OPERATOR;
char *formula_ = (char *) malloc(sizeof(char[MAX_LENGTH]));
/*
遍历各个字符,将中缀表达式转换为后缀表达式
*/
//i,j分别为遍历两个表达式的下标,i随循环自增,j随元素放入自增
for(int i = 0, j = 0; _formula[i] != '\0'; i ++)
{
printf("遍历到%c\n", _formula[i]);
//如果字符为数字或小数点,则放入后缀表达式中
if(ISNUM(_formula[i]) || _formula[i] == '.')
{
printf("放入后缀表达式中\n");
formula_[j ++] = _formula[i];
if(!(ISNUM(_formula[i + 1]) || _formula[i + 1] == '.'))
{
printf("单个值遍历完毕,添加分隔符!\n");
formula_[j ++] = DELIMITER;
}
}
else if (_formula[i] == '(')
{
Push(_formula[i], link);
}
else if(_formula[i] == ')')
{
if(link->top != NULL)
{
while(link->top->info != '(')
{
formula_[j ++] = Pop(link)->info;
//找不到左空格
if(link->top != NULL)
{
printf("错误:无法匹配左括号(,即将越界访问内存!\n");
}
}
Pop(link);
}
else
{
printf("Error!\n");
return NULL;
}
}
else
{
/*
如果待入栈运算符的优先级不高于(如果包含等于会导致结合方向出错)栈顶运算符,则入栈
否则弹出栈顶运算符,直至条件成立再入栈
*/
//防止指针越界访问内存导致程序错误
if(link->top != NULL)
{
while(!ISPRIORITY(_formula[i], link->top->info))
{
printf("待入栈运算符优先级较低,栈顶运算符出栈!\n");
formula_[j ++] = Pop(link)->info;
TraverseNode(link);
//防止指针越界访问内存导致程序错误
if(link->top == NULL)
break;
}
}
Push(_formula[i], link);
TraverseNode(link);
}
/*
如果前缀表达式已经遍历完毕,
则将栈内运算符弹出放入后缀表达式,补上结束符'\0'
*/
if(_formula[i + 1] == '\0')
{
printf("遍历完成,全部出栈!\n");
while(link->top != NULL)
{
formula_[j ++] = Pop(link)->info;
TraverseNode(link);
}
formula_[j] = '\0';
}
}
return formula_;
}

完善格式限制和无法完成运算时的程序出口

例如:输入格式不符合要求时重新输入或运算会出错时正常退出程序

posted @   YusJade  阅读(148)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
点击右上角即可分享
微信分享提示