栈的链式储存结构及应用:四则运算表达式求值(中缀表达式转化为后缀表达式并求值)
目录
前言:好久没写过百来行的代码了,难得有机会练练手,最近重温了栈的链式储存结构,照着资料上用栈处理四则运算的思路,写了段代码,期间遇到了不少问题与知识遗漏之处,故记录之。
真的被指针和内存访问薄纱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_; }
完善格式限制和无法完成运算时的程序出口
例如:输入格式不符合要求时重新输入或运算会出错时正常退出程序