数据结构应用实例(一)——中缀表达式

Content:

问题描述

一个算术表达式是由操作数(operand)、运算符(operator)和括号组成的。假设操作数均是正实数,运算符只含加减乘除 ( + , − , ∗ , / ) (+,-,*,/) (+,−,∗,/) 四种运算符。

一、判断表达式的合法性

参考博客:这篇博客表达式合法性的判断写得比较好,使用了《编译原理》中的相关知识,本人尚未学习,模仿着理解了一下。

1、词法分析

首先对表达式涉及到的单词进行编码:

单词符号 种别编码
+ 1
- 2
* 3
/ 4
正实数 5
( 6
) 7
其次,将中缀表达式拆分成单词的集合,每个单词与相应的种别编码配对,填入栈word中,这一步是通过对中缀表达式扫描实现的,如果扫描到其他符号或者小数点后面还有数字或者出现直接除零操作,则表示词法错误;如果没有出现词法错误,进行语法分析;
2、语法分析

算术表达式的文法 G [ E ] G[E] G[E] 如下:

E → E + T    ∣    E − T    ∣    T E → E+T\;|\; E-T\;|\;T E→E+T∣E−T∣T
T → T ∗ F    ∣    T / F    ∣    F T → T*F\;|\;T/F\;|\;F T→T∗F∣T/F∣F
F → ( E ) ∣ d F → (E) | d F→(E)∣d

消去非终结符 E E E 和 T T T 的左递归后,改写 G [ E ] G[E] G[E] 文法如下:

E → T E ′ E → TE' E→TE′
E ′ → + T E ′    ∣    − T E ′    ∣    ε E' → +TE'\;|\;-TE'\;|\;ε E′→+TE′∣−TE′∣ε
T → F T ′ T → FT' T→FT′
T ′ → ∗ F T ′    ∣    / F T ′    ∣    ε T' → *FT'\;|\;/FT'\;|\;ε T′→∗FT′∣/FT′∣ε
F → ( E )    ∣    d F → (E)\;|\;d F→(E)∣d

可以证明上述无递归文法是 L L ( 1 ) LL(1) LL(1) 文法,可以使用递归下降分析法 。递归下降分析法是确定的自上而下分析法,这种分析法要求文法是 L L ( 1 ) LL(1) LL(1) 文法。它的基本思想是:对文法中的每一个非终结符编写一个函数(或子程序),函数名是相应的非终结符,函数体是根据规则右部符号串的结构编写,函数(或子程序)功能是识别由该非终结符所表示的语法成分。

(1) 当遇到终结符 a a a 时,则编写语句

if(当前读来的符号 == a a a)读下一个输入符号;

(2) 当遇到非终结符 A A A 时,则编写语句调用 A ( ) A( ) A();

(3) 当遇到 A − > ε A->ε A−>ε 规则时,则编写语句

if(当前读来的输入符号 不属于 F O L L O W ( A ) FOLLOW(A) FOLLOW(A)) e r r = − 1 err=-1 err=−1;//表示出现错误

(4) 当某个非终结符的规则有多个候选式时,按 L L ( 1 ) LL(1) LL(1) 文法的条件能唯一地选择一个候选式进行推导。

需要求出 F O L L O W ( E ′ ) FOLLOW(E') FOLLOW(E′) 和 F O L L O W ( T ′ ) FOLLOW(T') FOLLOW(T′):

F O L L O W ( E ′ ) = F O L L O W ( E ) = {    ) , #    } FOLLOW( E' )= FOLLOW(E)=\lbrace\;),\#\;\rbrace FOLLOW(E′)=FOLLOW(E)={),#}
F O L L O W ( T ′ ) = F O L L O W ( T ) = {    + , − , ) , #    } FOLLOW( T' ) = FOLLOW( T ) =\lbrace\;+,−,),\#\;\rbrace FOLLOW(T′)=FOLLOW(T)={+,−,),#}

(#表示表达式结束标记);

之后就是利用程序实现了;

二、转化成后缀表达式

首先,建立一个栈,存储操作符;

从左到右扫描中缀表达式:

如果扫描到右括号,则弹栈,将弹出的操作符放到后缀表达式中,直到弹出左

括号,但是左括号不放入后缀表达式;

如果扫描到左括号,直接进栈;

如果扫描到 + + + 或 − - −,弹栈,将弹出的操作符放入后缀表达式,直到遇到左括号或者栈为空时,停止弹栈,将扫描到的操作符入栈;

如果扫描到 ∗ * ∗ 或 / / /,弹栈,将弹出的操作符放入后缀表达式,直到出现左括号或者 + + +, − - − 这些优先级更低的操作符或者栈为空时停止,将扫描到的操作符入栈;

如果扫描到操作数,直接将操作数放入后缀表达式,并在操作数的后面加分隔符;

扫描完中缀表达式之后,如果栈仍不空,弹栈,将弹出的操作符放入后缀表达式,直到栈为空;

三、后缀表达式求值

首先,建立一个栈,存储操作数;

从左到右扫描后缀表达式:

如果扫描到 + , − , ∗ , / +,-,*,/ +,−,∗,/,弹出栈顶的两个元素进行相应运算(栈顶元素在操作符前),然后将运算结果入栈;

如果扫描到操作数,将其转化成float型,然后入栈;

最后,如果后缀表达式合法的话,栈中应只剩一个数值,便是最终结果,将其返回;

四、源代码

c 复制代码
#include<iostream>
#include<fstream>
#include<sstream>
#include<math.h>
#include<vector>
#include<string>
#include<string.h>
#include<iomanip>
#include<utility>
using namespace std;
#define Maxsize 100
#pragma warning(disable:4996)

double stringToDouble(const string& str);//将字符串str转化成double型,返回转化结果
int word_analysis(vector<pair<string,int>>& word, const string expr);//对中缀表达式expr进行词法分析,word为存储操作数和操作符的栈,返回值用于判断是否出现错误
void trans(char *infix, char *postfix);//将中缀表达式infix转化成后缀表达式postfix
double compvalue(char *postfix);//计算后缀表达式postfix的值,返回计算结果

void next();//读取下一单词的种别编码
//运用递归下降分析法进行语法分析
void E();//E->TE'
void E1();//E'->+TE'|-TE'|e 
void T();//T->FT'
void T1();//T'->*FT'|/FT'|e
void F();//F->(E)|d

//全局变量
vector<pair<string,int>> word;//存放单词符号和编码
// 单词符号  种别编码
//    +        1
//    -        2
//    *        3
//    /        4
//  正实数      5
//    (        6
//    )        7
unsigned int idx=0;//语法分析时遍历中缀表达式的计数器
int sym;//指示种别编码
int err=0;//语法分析返回值

int main(void)
{
	fstream fp;
	string expr;//输入的中缀表达式
	int err_num;//词法分析返回值
	const char *s;	
	char infix[Maxsize];//C语言字符串版的中缀表达式   
	char postfix[Maxsize];//转化来的后缀表达式
	double v;//表达式结果
	
	cout<<"请输入要进行处理的中缀表达式('#'表结束):"<<endl;
	cin>>expr;

	/*//文件操作	
	fp.open("expression.txt",ios::in);
	if (!fp)
	{
	cout<<"文件不存在!"<<endl;
	exit(-1);
	}
	fp>>expr;*/

	while (expr != "#")
	{
		err_num=word_analysis(word,expr);
		cout<<endl<<"进行合法性判断,判断结果:"<<endl;
		if (err_num == -1)
			cout<<"词法错误!"<<endl;
		else//词法正确
		{	//进行语法分析
			next();
			E();
			if(sym == 0 && err == 0)//两个条件均要满足,sym==0表示读到末尾
			{
				cout<<"表达式合法,下面将其转化成后缀表达式."<<endl;
				s=expr.c_str();//先转化成C语言字符串
				strcpy(infix,s);
				//将表达式转化成后缀表达式
				trans(infix,postfix);
				cout<<"转化成的后缀表达式为:"<<endl;
				cout<<postfix<<endl;
				//计算后缀表达式的值
				v=compvalue(postfix);
				cout<<"计算结果为:"<<endl;
				cout<<setprecision(2)<<setiosflags(ios::fixed|ios::showpoint);//设置显示为小数点后两位
				cout<<v<<endl<<endl;
			}
			else
				cout<<"表达式非法."<<endl;
		}

		//重置全局变量
		word.clear();//清空栈
		idx=0;
		err=0;

		cout<<"请输入要进行处理的中缀表达式('#'表结束):"<<endl;
		cin>>expr;

		//fp>>expr;
	}

	//fp.close();//关闭文件
	return 0;
}

double stringToDouble(const string& str)//将字符串str转化成double型
{
	double d; 
	stringstream ss;
	ss << str;    // 把字符串写入字符流
	ss >> d;      // 输出到double
	return d;
}

int word_analysis(vector<pair<string,int>>& word, const string expr)//对中缀表达式expr进行词法分析,word为存储操作数或操作符的栈
{
	int i;
	int m;
	int n=expr.length();
	for (i = 0; i < n; i++)
	{
		// 如果是 + - * / ( ),将其与对应数字配对,添加到动态数组(栈)word中
		if (expr[i] == '+' || expr[i] == '-' || expr[i] == '*' || expr[i] == '/' || expr[i]=='(' || expr[i]==')')
		{
			string tmp;
			tmp.push_back(expr[i]);
			switch (expr[i])
			{
			case '+':
				word.push_back(make_pair(tmp,1));
				break;
			case '-':
				word.push_back(make_pair(tmp,2));
				break;
			case '*':
				word.push_back(make_pair(tmp,3));
				break;
			case '/':
				word.push_back(make_pair(tmp,4));
				break;
			case '(':
				word.push_back(make_pair(tmp,6));
				break;
			case ')':
				word.push_back(make_pair(tmp,7));
				break;
			}
		}
		//如果是数字
		else if ('0' <= expr[i] && expr[i] <= '9')
		{
			string tmp;
			while ('0' <= expr[i] && expr[i] <= '9')
			{
				tmp.push_back(expr[i]);
				i++;
			}
			if (expr[i] == '.')
			{
				i++;
				if ('0' <= expr[i] && expr[i] <= '9')
				{
					tmp.push_back('.');
					while ('0' <= expr[i] && expr[i] <= '9')
					{
						tmp.push_back(expr[i]);
						i++;
					}
				}
				else
					return -1;//.后不是数字,词法错误
			}
			m=word.size();
			if(m>0 && word[m-1].second == 4 && stringToDouble(tmp) == 0.0)//如果出现除0操作,也有错误
				return -1;
			word.push_back(make_pair(tmp,5));
			i--;//为下一次读入做准备
		}
		else//其余情况:既不是操作符,也不是操作数,表示词法错误
			return -1;
	}
	return 0;//表示没有出现词法错误
}


void next()//读取下一单词的种别编码
{
	if (idx < word.size())
	{
		sym = word[idx].second;
		idx++;
	}
	else
		sym=0;//表示读取结束
}

void E()// E->TE'
{
	T();
	E1();
}

void E1()//E' -> +TE'|-TE'|e 
{
	if (sym == 1 || sym ==2)// + or -
	{
		next();
		T();
		E1();
	}
	else
	{
		if (sym != 7 && sym != 0) // 不是 ) 也不是末尾
			err=-1;//有错误
	}
}

void T()//T->FT'
{
	F();
	T1();
}

void T1()//T'->*FT'|/FT'|e
{
	if (sym == 3 || sym == 4)// * or /
	{
		next();
		F();
		T1();
	}
	else
	{
		if(sym!=1 && sym!=2 && sym!=7 && sym!=0)//不是+ -,也不是)或末尾
			err=-1;//有错误
	}
}

void F()//F->(E)|d
{
	if (sym == 5)//操作数
		next();
	else if (sym == 6)// (
	{
		next();
		E();
		if (sym == 7)// )
			next();
		else
			err=-1;//有错误
	}
	else
		err=-1;//有错误
}


void trans(char *infix, char *postfix)//将中缀表达式infix转化成后缀表达式postfix
{
	int i,j;
	char ch;
	char *t;//建立栈,用于存储运算符
	int top=-1;
	t=(char *)malloc(Maxsize*sizeof(char));

	i=0;
	j=0;
	ch=infix[i];
	while (ch != '\0')//读取中缀表达式
	{
		switch (ch)
		{
		case ')'://弹栈,直到弹出左括号
			while (top >= 0 && t[top] != '(')
			{
				postfix[j++]=t[top--];
			}
			if (top >= 0 && t[top] == '(')//将左括号弹出
				top--;
			break;
		case '(':// '('入栈
			top++;
			t[top]='(';
			break;
		case '+':
		case '-':
			while(top >= 0 && t[top] != '(')//一直弹栈,直到栈为空,或者出现'('
			{
				postfix[j++]=t[top--];
			}
			//将ch入栈
			top++;
			t[top]=ch;
			break;
		case '*':
		case '/':
			while(top >= 0 && (t[top] == '*'|| t[top] == '/'))//一直弹栈,直到栈为空,或者出现'('或'+''-'
			{
				postfix[j++]=t[top--];
			}
			//将ch入栈
			top++;
			t[top]=ch;
			break;
		default:
			while (('0' <= ch && ch <= '9') || ch == '.')//读取数字
			{
				postfix[j++]=ch;
				i++;
				ch=infix[i];
			}
			postfix[j++]='|';//操作数后面增加分隔符
			i--;//退一格,以便重新读取
		}
		//更新ch
		i++;
		ch=infix[i];
	}

	//读取完之后,如果栈非空,弹栈
	while (top >= 0)
	{
		postfix[j++]=t[top--];
	}
	//封尾
	postfix[j]='\0';
	free(t);//t使用结束,释放掉其指向的空间
}

double compvalue(char *postfix)//计算后缀表达式postfix的值
{
	int i,j;
	char ch;
	double d1,d2;
	double *t;//用于存储表达式中的操作数(这里是正实数)
	int top=-1;
	t=(double *)malloc(Maxsize*sizeof(double));

	i=0;
	ch=postfix[i];
	while (ch != '\0')//读取后缀表达式
	{
		switch (ch)
		{
		case '+'://栈中的最后两个元素进行相加运算,并更新栈顶元素
			t[top-1]+=t[top];
			top--;
			break;
		case '-'://栈中的最后两个元素进行相减运算,并更新栈顶元素
			t[top-1]-=t[top];
			top--;
			break;
		case '*'://栈中的最后两个元素进行相乘运算,并更新栈顶元素
			t[top-1]*=t[top];
			top--;
			break;
		case '/'://栈中的最后两个元素进行相除运算,并更新栈顶元素
			t[top - 1] /= t[top];//这里t[top]!=0
			top--;
			break;
		case '|'://跳过分隔符
			break;
		default:
			d1=0.0;
			while ('0' <= ch && ch <= '9')//计算小数点之前的数值
			{
				d1=10*d1+ch-'0';
				i++;
				ch=postfix[i];
			}
			if (ch == '.')
			{
				d2=0.0;
				j=0;//统计小数点之后的位数
				i++;
				ch=postfix[i];
				//计算小数点之后的数值
				while ('0' <= ch && ch <= '9')
				{
					j++;
					d2=10*d2+ch-'0';
					i++;
					ch=postfix[i];
				}
				d2*=pow(0.1,j);
				top++;
				t[top]=d1+d2;
			}
			else
			{
				top++;
				t[top]=d1;
			}
			i--;//后退一步,以便重新读入
		}
		i++;
		ch=postfix[i];
	}

	d1=t[top];//最后返回栈顶数值即可
	free(t);  //释放空间
	return d1;
}

五、小结

  1. 后缀表达式中操作数之间要有分隔符,以免计算表达式数值时将两个或多个操作数识别成一个操作数;
  2. 中缀表达式读取结束之后,操作符栈中可能非空,将剩余的操作符转移到后缀表达式中,之后给后缀表达式添加字符串结束标记;
  3. 读取完中缀表达式中的操作数后,将计数器减1,以便统一更新扫描值或者说是避免漏读;
  4. 为确保计算结果的精度,统一采用double型进行运算;
  5. 使用了C语言形式的字符数组和C++的string对象两种形式对字符串进行相关处理;
  6. 几种非法的算术表达式:
    (1)、 第一个字符应该是数字或者左括号:)1+2 +1;
    (2)、 运算符前必须是数字或者右括号:(+2 -+3;
    (3)、 运算符后必须是数字或者左括号:1+) 1++;
    (4)、 除数为0:1/0.0,但是不考虑隐式的零,比如1/(0.0),或者1/(2-2);
    (5)、 括号数目不匹配:((1+2)+3;
    (6)、 无操作数:();
    (7)、 两个小数点相邻:1...2,...2, ...;
    (8)、 运算符个数与运算数不匹配;
    (9)、 含有除操作符、数字和小数点之外的其他符号;
相关推荐
wheeldown7 小时前
【数据结构】选择排序
数据结构·算法·排序算法
躺不平的理查德11 小时前
数据结构-链表【chapter1】【c语言版】
c语言·开发语言·数据结构·链表·visual studio
阿洵Rain11 小时前
【C++】哈希
数据结构·c++·算法·list·哈希算法
Leo.yuan12 小时前
39页PDF | 华为数据架构建设交流材料(限免下载)
数据结构·华为
半夜不咋不困12 小时前
单链表OJ题(3):合并两个有序链表、链表分割、链表的回文结构
数据结构·链表
忘梓.13 小时前
排序的秘密(1)——排序简介以及插入排序
数据结构·c++·算法·排序算法
y_m_h15 小时前
leetcode912.排序数组的题解
数据结构·算法
1 9 J15 小时前
数据结构 C/C++(实验三:队列)
c语言·数据结构·c++·算法
921正在学习编程15 小时前
数据结构之二叉树前序,中序,后序习题分析(递归图)
c语言·数据结构·算法·二叉树
毕竟秋山澪16 小时前
岛屿数量 广搜版BFS C#
数据结构·算法·宽度优先