接着上一篇博客中的栈,这次介绍下栈的一些应用。
在看到的栈的这个特性后进先出的性质时,第一感觉就是这样做有什么用呢?把一个表的操作限制成这个样子,不是在削减吗?然而,在实际的应用中,这些存在于栈中的少数的操作却是非常的强大和重要。下面给出三个栈的应用 :
一、平衡符号
在我们平时的编程过程中,常常由于缺少一个括号(如遗漏一个花括号或是注释齐起始符)导致编译过程中编译器列出上百行的错误,而真正的错误并没有找出来。当然,由于编译器软件的迅猛发展,现在很多编译器都会用红色的波浪线提示我们这些丢失的东西。
那编译器是如何做到帮助我们检查这个错误的呢?我们先来思考一下,在我们平时的编程中,每一个花括号、右括号及右圆括号必然对应其相应的左半部分。[()]是合法的,但[(])就是错误的了。显然,不值得为此编写一个大型程序,但这说明了这样的检验是很容易实现的。为了简单起见,我们仅就圆括号、方括号和花括号进行检验并忽略出现的任何其他的字符。
简单的算法描述如下:
(1)在算法的开始,我们准备一个空的栈。
(2)读取字符直至到达文件的末尾。
(3)如果读入的字符是一个开放符号,则将此字符压入栈中;
(4)如果读入的字符m是一个封闭符号,那么查看栈是否为空。如果栈为空,则提示错误;如果栈不为空,则将栈顶的元素弹出。如果此栈顶元素和m不是相对应的,则提示错误。
(5)在文件读完的时候,如果栈是非空的,则提示错误。
可以确信,这个算法是可以正确运行的。很清楚,算法是线性的,事实上它只需要对输入进行一次检查。因此,它是联机的并且相当的快。并且,我们可以做一些附加的工作来决定当检测出错误的时候应该如何进行处理。
二、后缀表达式
其实,在用到栈的时候,让我感觉最有用的就是这个栈处理后缀表达式的能力了。在我们平时进行数学公式中简单的加、减、乘、除的时候,对于我们人脑而言,这方面的数学运算是很简单的,因为各种规则我们可以处理的很好。但是,当我们用编程处理数学计算的时候,那么,我们就要想了,如何在程序中处理加、减、乘、除及带括号的这些情况呢?在没有看到后缀表达式的时候,说实话,我是懵逼的,因为数学表达式的长度是可以无限的,那么如果进行枚举之类的方法的时候,我想这是会让人崩溃的,即便用循环处理,我想也是很闹大的,至少我是没有理过来,知道看到后缀表达式,我一下子就顿悟了。
下面我们来具体看看关于后缀表达式的东西。
举个简单的例子,在计算12*3-4*3的时候,我们人脑的计算过程是先计算出12*3的值放入a,然后计算4*3的值放入b,最后我们会计算a-b。对于这种操作的顺序,我们可以按如下的顺序书写:
12 3 * 4 3 * +
这种记法叫做后缀或者逆波兰记法,其求值的过程恰好就是上面所述我们人脑的计算过程。计算这个问题最容易使用的就是使用栈,这也正是我们的重头戏。这个方法的执行过程很简单:
(1)当遇到一个数时,将这个数压入栈中。
(2)当遇到一个操作符的时候,则从栈中弹出两个数,然后进行相应的运算,并将运算的结果压入栈中。
(3)持续上述的两个操作直至处理结束,最后栈中剩余的数字即为结果。
例如,我们计算这个后缀表达式:6 5 2 3 + 8 * + 3 + *,这个表达式的正常表达式为6*(5+(2+3)*8+3)。处理过程如下:
(1)准备一个空的栈,如下:
(2)将前4个数字放入栈中,此时栈的情况如下:
(3)下面读到了一个“+”号,所以从栈中弹出3和2,并且进行加法运算,将结果5压入栈中,如下图所示:
(4)接着,将数字8压入栈中,如下:
(5)接着读到的是“*”号,因此将栈顶的8和5弹出,并且计算5*8=40(这里请注意,我们是将5作为第一个操作的数的,这个在减法和除法里尤为重要,操作数的顺序要仔细),并将40压入栈中,如下所示:
(6)接下来督导的是一个“+”号,此时将栈顶的40和5弹出,并且计算5+40=45,将45压入栈中,如下所示:
(7)将读入的3压入栈中,如下所示:
(8)接着读到了“+”号,将栈顶的3和45弹出,并计算45+3=48,并将48压入栈中,如下所示:
(9)最后,读到一个“*”号,将栈顶的48和6弹出,并计算6*48=288,并将288压入栈中,如下所示:
在上述的过程中,我们可以看出,计算一个后缀表达式所花费的时间是O(N),因为对输入中的每个元素的处理都是由一些栈操作组成,从而花费常数时间。在上述执行的过程中,我们可以发现,在计算后缀表达式的时候,我们不需要关注运算符中的有限规则,这是一个很明显的有点,它让我们的计算变得非常的简单了。
在看到后缀表达式这么简单便能处理数学计算的时候,是不是很心动了,但是是不是又会纠结于如何得到这个后缀表达式呢?那么下面我们就不做歇息了,直奔下一个站点。
三、中缀表达式到后缀表达式的转换
在见识了栈对于后缀表达式后,我想说的是,如何得到后缀表达式栈也是可以帮助我们实现的,这简直就是一条龙服务嘛,超级棒!
对于一个标准形式的表达式(或叫做中缀表达式),我们从简单的方面入手,这里我们暂时只处理操作符“+”、“-”、“*”、“”、“(”、“)”,并保持我们平时数学计算中的优先规则。
此算法的过程如下:
1、准备一个空的栈,用于存放操作符。
2、当读到一个操作数的时候,立即将它放到输出中。
3、当读到操作符的时候,我们不会立即将它输出,而是将操作符放入到栈中,这个放入操作也是有一定的要求的:
(1)如果见到一个右括号,那么将栈中的元素弹出,将弹出的操作符放到输入中直到遇到一个(对应的)左括号,但是这个左括号只被弹出,并不放到输出中。
(2)如果遇到任何其他的操作符(如“+”、“-”、“*”、“”、“(”),那么从栈中弹出栈元素直到发现优先级更低的元素为止。有一个例外:除非是在处理“)”的时候,否则决不从栈中移走“(”。对于这种操作,“+”、“-”的优先级最低,而“(”的优先级最高。当从栈中弹出元素的工作完成后,将读到的操作符压入栈中。
4、如果读到输入的末尾了,那么将栈中的元素弹出直到栈变为空,并将这些操作符放到输出中。
对于上述的规则,刚看到的时候可能有点云里雾里的,这个没关系,我们也没有必要去咬字眼,实践出真知嘛,那么下面我们在例子中来熟悉这些规则。
例如,对于一个中缀表达式:a+b*c+(d*e+f)*g,下面我们来将它处理成后缀表达式:
(1)首先,读入的是操作数a,将它送到输出中,如下:
(2)然后将“+”读入并压入到栈中,如下所示:
(3)将b读入并送到输出中,如下所示:
(4)将“*”号读入,此时我们来看下规则,操作符栈中栈顶的元素为“+”,显然比“*”号的优先级低,故此时“+”不需要弹出,我们将“*”号压入栈中,如下所示:
(5)将操作数c读入,并放到输出中,如下所示:
(6)读入操作符“+”号,此时我们又要看一下规则了,检查栈顶可以发现,栈顶的操作符为“*”号,显然优先级比“+”号高,则我们需要将栈顶的“*”弹出并放入到输出中;继续检查栈顶,发现栈顶的元素为“+”号,显然优先级一样,而规则中是要求我们弹出栈顶元素知道发现优先级更低的元素,那么此时我们是可以将栈顶的“+”号弹出的,并且将它放到输出中,最后将读入的“+”号压入栈中,如下所示:
(7)将操作符“(”读入,由于它具有最高的优先级且具有特殊性,故直接将它放入到栈中,如下所示:
(8)将操作数d读入并放到输出中,如下所示:
(9)将操作符“*”读入,看下规则可以发现,由于除非正在处理的操作符是右括号,否则左括号是不会从栈中弹出的,因此没有输出,直接将“*”压入栈中,如下所示:
(10)将操作数e读入并放入到输出中,如下所示:
(11)将操作符“+”号读入,这个时候根据规则,我们检查栈顶的元素,栈顶元素为“*”号,优先级比“+”号高,则将“*”弹出并放入到输出中,然后继续检查栈顶元素,此时栈顶元素为“(”,因为此符号只有在处理右括号的时候才会弹出,故此时可以将读入的“+”好压入栈中了,如下所示:
(12)将操作数f读入,并放入到输出中,如下所示:
(13)将操作符“)”读入,由规则可知,此时可以弹出栈顶的元素知道“(”被弹出,因此可以输出一个“+”号,如下所示:
(14)将操作符“*”读入,由规则可知,栈顶元素“+”号优先级比“*”低,直接将“*”号压入栈中,如下所示:
(15)将操作数f读入,并放入到输出中,如下所示:
(16)读入为空了,因此我们将栈中的所有操作符弹出并放到输出中,如下所示:
与前面相同,这种转换只需要O(N)的时间并可以通过一次输入便可以完成工作。
好了,这次就只写这些吧,写太多了,有点累了,关于编程方面的具体实现就留待下一篇博客再和大家分享吧!