数据结构与算法——栈与队列(二) 学习笔记

接上一条博客的目录继续

1.3 栈应用

1.3.4 延迟缓冲:中缀表达式求值

在线性扫描算法中,往往对数据的读取速度和处理速度不同步,而往往是各步的计算速度滞后于扫描的速度,需待到必要的信息已完整到一定程度之后,才能作出判断并实施计算。

算法思路

减而治之:在表达式中寻找能够优先计算的子串,并将其的值重新放在原位置进行后续计算。
对于非常长的表达式,如果通过线性扫描的次序来处理表达式,很难确认当前所能计算的运算符,即每当扫描到一个运算符,很难确定它已经是当前能够计算的,也就是说计算次序和扫描次序未必一致。
因此借助栈结构,将所有已经扫描过的部分保存为一个栈,在所有已经扫描过的部分中,有一些是经过判断在局部具有足够优先级并已经处理的;有一些是已经扫描过但还不足以判断能够计算的,将通过这个栈缓冲起来。算法的策略就是将尚未扫描过的部分扫描并且处理掉。

下图为清华大学数据结构慕课截图

借助栈对表达式进行扫描和处理

整个扫描并判断运算符能否计算的过程,感觉类似于寻找局部最优的过程,当某个运算符相邻的左右运算符的优先级均低于其本身时,该运算符便可以执行运算;而当两个优先级相同的运算符相邻时,前面那个运算符便可以执行运算。

为使得处理更加方便,需将运算符和运算数分别对待,即需要两个栈

算法

算法主体框架

  1. 建立两个栈,分别用于存储运算数(operand)运算符(operator)
  2. 将尾哨兵’\0’也作为头哨兵首先入栈
  3. 主循环:逐个处理当前字符,直至运算符栈空。若当前字符为操作数,则通过一函数readNumber()读入(可能多位的)操作数入运算数栈;若当前字符为运算符,则视其与栈顶运算符之间优先级的高低(通过查询预先建立的运算符优先级表),进行不同的处理:
    ①栈顶运算符优先级更低:计算延迟,当前运算符入栈,继续往后扫描
    ②优先级相等:(当前运算符为右括号或尾部哨兵’\0’)弹出运算符栈顶元素(脱括号)并继续往后扫描
    ③栈顶运算符优先级更高:将运算符栈顶元素出栈,执行相应运算:需分一元运算符二元运算符分别处理;特别注意,若是二元运算符,在从运算数栈中取出两个操作数时,由于两个操作数是有先后顺序的,而栈具有LIFO特性,所以不能直接用两个pop()取出来数进行计算(因为反过来了),而要借助两个临时变量储存两个操作数,再进行操作!!!

:操作数入栈时,若是多位操作数,则readNumber() 每一次将上一次的结果弹出栈,乘以十后加上当前数字元素并再次入栈

书上的代码这儿就不贴了,2333

1.3.5 逆波兰表达式

逆波兰表达式(Reverse Polish Notation,RPN)又称作后缀表达式(postfix),原表达式则称作中缀表达式(infix)

常规表达式:( 1 + 2 ) * 3 ^ 4
RPN表达式:1 2 + 3 4 ^ *

RPN求值算法

算法过程:建立一个栈。若遇到操作数,则直接入栈;若遇到操作符,随即执行相应的运算,需要几个操作数就从栈中取出几个,计算结果再压入栈中。最终取出栈顶(也是栈底)元素,即为RPN表达式的值
RPN表达式只需要一个栈,因此RPN求值算法十分高效、简洁。

书中算法伪代码:

输入:RPN表达式
输出:表达式数值
{
	引入栈S,用以存放操作数;
	while(RPN尚未扫描完毕)
	{
		从RPN中读取下一元素x;
		if(x是操作数)
			将x压入S;
		else	//x是运算符
		{
			从栈S中弹出运算符x所需数目的操作数;
			对弹出的操作数实施x运算,并将运算结果重新压入S;
		}
	}
	返回栈顶;	//也是栈底,即为RPN表达式的值
}

如果存在某一个不确定数字的表达式,比如某一个代数表达式,并且需要经常地反复调用,可以将它转化为RPN形式,这样一旦其中每一个变量的值确定,就可以套用RPN求值算法快速简明地计算出其值

作为预处理,将中缀表达式转化为逆波兰表达式,在效率上非常值得

表达式转换算法

从中缀表达式向RPN表达式手工转换思路

  1. 为每一个操作符假想一对括号帮助界定其在表达式中的优先级
  2. 将各运算符后移到对应的右括号后
  3. 抹去所有括号

注意,转换过程中运算符的相对次序可能改变,但运算数的相对次序一定不変

转换算法
根据上述手工转换思路,只需在先前的中缀表达式算法中稍加改动即可:

  1. 在原函数参数中添加一个char* & RPN的参数,用于记录RPN
  2. 在当前字符是操作数时,执行完readNumber()函数后,直接将刚刚推入栈顶的操作数续接到RPN的尾部
  3. 在当前的字符是运算符时,当且仅当栈顶元素优先级>当前运算符优先级时(即栈顶的元素可以立即执行时),才将该栈顶运算符续接到RPN的尾部(完全等同于手工算法中将运算符移至对应右括号的右侧,因为右括号代表着该运算符已可以立即执行)
  4. 该中缀表达式求值算法运行完毕后,参数RPN中便是转换的RPN表达式

清华大学数据结构(C++语言版)书中源代码:

float evaluate(char* S, char*& RPN) { //对(已剔除白空格的)表达式S求值,并转换为逆波兰式RPN
	Stack<float> opnd; Stack<char> optr; //运算数栈、运算符栈 /*DSA*/任何时刻,其中每对相邻元素之间均大小一致
	char* expr = S;
	optr.push('\0'); //尾哨兵'\0'也作为头哨兵首先入栈
	while (!optr.empty()) { //在运算符栈非空之前,逐个处理表达式中各字符
		if (isdigit(*S)) { //若当前字符为操作数,则
			readNumber(S, opnd); append(RPN, opnd.top()); //读入操作数,并将其接至RPN末尾
		}
		else //若当前字符为运算符,则
			switch (orderBetween(optr.top(), *S)) { //视其与栈顶运算符之间优先级高低分别处理
			case '<': //栈顶运算符优先级更低时
				optr.push(*S); S++; //计算推迟,当前运算符进栈
				break;
			case '=': //优先级相等(当前运算符为右括号或者尾部哨兵'\0')时
				optr.pop(); S++; //脱括号并接收下一个字符
				break;
			case '>': { //栈顶运算符优先级更高时,可实施相应的计算,并将结果重新入栈
				char op = optr.pop(); append(RPN, op); //栈顶运算符出栈并续接至RPN末尾
				if ('!' == op) { //若属于一元运算符
					float pOpnd = opnd.pop(); //只需取出一个操作数,并
					opnd.push(calcu(op, pOpnd)); //实施一元计算,结果入栈
				}
				else { //对于其它(二元)运算符
					float pOpnd2 = opnd.pop(), pOpnd1 = opnd.pop(); //取出后、前操作数 /*DSA*/提问:可否省去两个临时变量?
					opnd.push(calcu(pOpnd1, op, pOpnd2)); //实施二元计算,结果入栈
				}
				break;
			}
			default: exit(-1); //逢语法错误,不做处理直接退出
			}//switch
		displayProgress(expr, S, opnd, optr, RPN);
	}//while
	return opnd.pop(); //弹出并返回最后的计算结果
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值