Files
912-notes/thu_dsa/chp4/notice.md
2019-12-10 17:52:16 +08:00

5.8 KiB
Raw Blame History

栈与队列相关重点知识

本篇的内容是栈和队列中的一些较为深入的内容,主要包括栈混洗问题以及中缀表达式求值。需要日后添加合并到chp4/chp4.md当中。

中缀表达式求值

特殊的运算符:阶乘和指数

对于阶乘运算符,关键在于这是一个单目运算符,并且具有最高的优先级。由于它是一个单目运算符,因此在计算时只需要对操作数栈进行一次弹栈;而由于它具有最高的优先级,因此如果它在操作符栈中,则必然存在于栈顶,并且只能有一个阶乘运算符在栈中。需要注意的是,阶乘运算符的优先级与左括号是不可比较的,因为阶乘之后不可以跟一个左括号。

指数运算符的优先级仅次于阶乘,它的特殊性在于在通常约定的语意下,指数运算符是右结合的(其他诸如加法、乘法运算符是左结合)。这就意味着x^y^z需要被理解为x^(y^z)而非(x^y)^z。为此,只需要对prio数组的这一项进行修改,使得prio[POW][POW] = '<'即可。

操作符栈的大小

以下仅考虑一个非常简单的字符集的情况,其中仅包含+, -. *, /, ^, (, ), \0, !

设表达式中的括号数量为n,则要使操作符栈达到最大,其中的组成应该大体是下面这样的模式:

--------------------------------------------------------------
| \0 | + | * | ^ | ( | + | * | ^ | ... | ( | + | * | ^ | !
--------------------------------------------------------------
bottom                                                     top

这里需要注意的点主要在于阶乘符号在栈中只能存在一个,且必须是在栈顶。因此,序列( + * ^一共会循环n次,再加上最前方的字符和最后面的!,操作符栈的最大值为4n + 5。同时也可以看出,在表达式不含括号时,操作符栈的大小为常数O(1)

增加差错检验的功能

实际的计算器不光要能够求值,还要有差错检验的能力,即及时发现输入表达式的差错。否则,对于异常输入,程序往往会崩溃。同时对于某些特定的异常输入,却仍然可以计算出一个结果(尽管没有意义),例如(12)3 + ! 4 * + 5

增加的差错检验的功能应该包含三个方面的内容:

  • 对括号匹配的检验。
  • 在进行实质的计算时,操作数栈不得为空。
  • 输入表达式是否符合中缀表达式的规范。

对于第三个方面检验的方法是在任意操作符即将入栈时即操作符之间的优先级比较以及可能的弹栈与计算操作已经结束操作数栈的规模应该恰好比操作符栈大1。需要注意上述结论中不应该计入\0以及(

对这个结论可以利用数学归纳法证明,主要就是对单目运算符和双目运算符进行讨论,这里不再赘述。

表达式树

可以将中缀表达式和后缀表达式转化为表达式树。

对于中缀表达式,需要一个操作数栈和操作符栈。按照同样的方法遍历中缀表达式,操作数封装成单个的树节点入操作数栈,操作符按照优先级关系入栈出栈。对于出栈的双目运算符,弹出操作数栈中的两棵表达式树,以该运算符为根节点,两棵表达式树分别作为其左右子树,再将新得到的表达式入栈,如此不断进行下去即可。

对于后缀表达式,并不需要操作符栈,依序读到操作符,然后按照和中缀表达式同样的操作,即出栈两棵树,构造新树,然后再入栈,即可。

表达式树的中序遍历即为中缀表达式,后序遍历为后缀表达式。

卡特兰数

括号匹配问题,栈混洗问题以及异构二叉树的数量问题,都可以归入到卡特兰数的范畴,这里做一个简单的说明。

异构二叉树的数量

异构二叉树的数量应该是这里最明确的问题,设SP(n)表示节点数量为n的二叉树的异构数量,以其中一个节点为为根,设左子树的规模为k,则右子树的规模为n - k - 1,因此有


SP(n) = \Sigma_{k = 0}^{n - 1}SP(k)SP(n - k - 1)

其中,定义SP(0) = 1,这就是卡特兰数的递推公式。

括号匹配问题

对于括号匹配问题,设这里有n对括号,$S_n$为一个匹配的序列。我之前产生过一些错误的看法,比如可以把$S_n$分解为两个各自匹配的序列$S_k$和$S_{n - k}$,即


S_n = S_k S_{n - k}

此时是否可以得到


SP(n) = \Sigma_{k = 0}SP(k)SP(n - k)

答案当然是错误的,因为这里会产生重复计数的问题。比如设$S_3 = ()()()$$S_1 = ()$$S_2 = ()()$,那么$S_3 = S_1S_2 = S_2S_1$,会被计数两次。另一种划分$S_n = S_k()S_{n - k - 1}$也是同样的问题。

因此,这里正确的划分应该是$S_n = (S_k)S_{n - k - 1}$,容易证明,此时没有上面的重复计数情况。此时就可以得到


SP(n) = \Sigma_{k = 0}^{n - 1}SP(k)SP(n - k - 1)

该结论和二叉树的问题一致。实际上,考虑二叉树的一次中序遍历,如果把每个节点的入栈视作一个左括号,出栈视作右括号,此时两个问题是完全等效的。此时$S_n = (S_k)S_{n - k}$中包围$S_K$的括号,就对应了二叉树中根节点的入栈与出栈操作。

栈混洗

对于栈混洗问题也可以得到相似的结论。考虑n个元素的栈混洗,设首元素在第k个位置出栈。显然,首元素出栈后栈为空。此时不难得到


SP(n) = \Sigma_{k = 0}^{n - 1}SP(k)SP(n - k - 1)

实际上,把栈混洗的入栈视作一个左括号,出栈视作一个右括号,则栈混洗问题与括号匹配问题完全一致,与二叉树异构也是完全一致的。此时,首元素的入栈和出栈分别对应了根节点的入栈和出栈。