平衡符号
每个右花括号,右方括号及右圆括号必然对应相应的左括号。序列“[ ( ) ]”是合法的,“ [ ( ] )“ 是错误的。 显然也不值得为此编写一个大型程序。事实上检验这些是很容易的,为简单起见我们仅就圆括号、方括号和花括号进行检验,并忽略其他字符。
这个简单的算法用到一个栈,叙述如下:
做一个空栈。读入字符直到文件尾。如果字符是一个开放字符(即左符号),将其推入栈中。如果字符是一个封闭符号(即右符号),则当栈空时报错;否则将栈元素弹出,若弹出的符号不是对应的开放符号,则报错。在文件尾,如果栈非空则报错。
后缀表达式
有式子 ,该例的典型计算顺序是将 4.99 和 1.06相乘存为,再将5.99与相加再将结果存入。 我们再将6.99和1.06相乘存为,最后将和相加并将最后结果存入。 我们可以将这种操作顺序书写如下:
4.99 1.06*5.99+6.99 1.06*+
这个记法叫做后缀(postfix)记法。计算这个问题最容易的方法是使用一个栈,当见到一个数时就把他推入栈中;在遇到一个运算符时该运算符就作用于从该栈弹出的两个数上,将所得结果推入栈中。例如,后缀表达式
6 5 2 3+8*+3+*
前四个数放入栈中,
3 |
2 |
5 |
6 |
然后遇到一个+号,从栈中弹出两个数 3和2 并相加得到 5,推入栈中。
5 |
5 |
6 |
遇到8推入栈中,
8 |
5 |
5 |
6 |
遇到 * 号,弹出两个数 8和5 ,并相乘得到40 推入栈中
40 |
5 |
6 |
接下来是+,40+5得45,然后是3
3 |
45 |
6 |
现在是+号,45+3得48,接着是 * ,48*6=288。
中缀到后缀的转换
栈不仅可以用来计算后缀表达式的值,还可以将一个标准形式的表达式(中缀式infix)转换为后缀式。我们通过只允许 + 、* 、( 、),并坚持这普通的优先级法则而将一般的问题浓缩为小规模问题。还要进一步假设表达式是合法的。
假设欲将中缀表达式
a+b*c+(d*e+f)*g
转换成后缀表达式,正确答案是:abc*+de*f+g*+
当读到一个操作数立即把它当到输出中。操作符不立即输出所以要先存到某个地方。正确的做法是将已经见到过的操作符放进栈中。当遇见左圆括号也要将其推入栈中。我们从一个空栈开始计算。
如果在栈中见到一个右括号,将栈元素弹出,将弹出的符号写出直到我们遇到一个(对应的)左括号,但这个左括号只被弹出,并不输出
如果在栈中见到任何其他符号("+" "" "("),那么从栈中弹出元素直到发现优先级更低的元素为止。有一个例外:除非是在处理一个")"时,否则我们绝不从栈中一走"("。"+"优先级最低,而"("优先级最高。当从栈弹出元素的工作完成后,我们再将操作符压入栈中。
最后,如果读到输入的末尾,将栈元素弹出直到该栈变为空栈,将符号写道输出中。
我们现在来将上面的中缀表达式转换为后缀形式。首先读入a,将它输出,读入"+"放入栈中
接着读入b,将它输出
+ |
输出:a b |
这时读入""。操作符栈的栈顶元素比""的优先级低,故没有输出,""进栈,接着将c读入并输出
+ |
输出:a b c |
后面的符号是"+",推入栈中,然后发现""将其输出,又发现"+"也将它输出,因为要找优先级更低的,他们优先级相同,然后栈中就只剩下刚刚的"+"
+ |
输出:a b c * + |
下一个符号是"(",由于拥有最高的优先级将其放入栈中,读入d并输出,然后是"",虽然"("优先级最高,""想要找优先级更低的就必须把"("输出,但是之前我们说过了"("是一个例外 只有处理")"时才将其输出。所以这里不输出。
( |
+ |
输出:a b c * + d |
再往后是e,读入e并输出,接着是"+","+"遇到"",将其输出,然后遇到"(",不输出。读入f并输出。
+ |
( |
+ |
输出:a b c * + d e * f |
接下来是")",如之前所说,将栈的元素弹出直到遇到一个左括号,但注意左括号只弹出不输出。
+ |
输出:a b c * + d e * f + |
下面又读到一个"",""遇到"+",不输出,最后读到一个g并输出,现在已经读到文件尾了,此时将栈元素弹出直到空栈并将符号写到输出中。
输出:a b c * + d e * f + g * + |
此时答案为 abc*+de*f+g*+
一种巧妙的想法是将 中缀式" a-b-c "转换为后缀式"ab-c-"而不是"abc--",我们的算法做了正确的工作。
函数调用
当调用一个新函数时,主调函数的所有局部变量需要由系统存储起来,否则被调用的新函数会覆盖调用函数的变量。不仅如此,该主调函数的当前位置必须存储,以便在新函数运行完后知道向哪里转移。这些变量一般由编译器指派给机器的寄存器,但存在某些冲突(通常所有的过程都将某些变量指派给1号寄存器),特别是设计递归的时候。该问题类似于平衡符号,函数调用和函数返回基本上类似于开括号和闭括号,二者想法是一样的。
当存在函数调用的时候,需要存储的所有重要信息,诸如寄存器的值(对应变量的名字)和返回地址(它可从程序计数器得到,典型情况下计数器就是一个寄存器)等,都要以抽象的方式存在“一张纸上”并被置于一个堆(pile)的顶部。然后控制转移到新函数,该函数自由地用它地一些值代替这些寄存器。如果它又进行其他的函数调用,那么它也遵循相同的过程。当该函数要返回时,它查看堆顶部的那张“纸”并复原所有的寄存器。然后它进行返回转移。
显然,所有的工作均可由一个栈来完成,而这正是在实现地柜的每一种程序设计语言中世纪发生的事实。所存储的信息称为 栈帧(stack frame)或者 活动记录(activation record),个人认为栈帧可以这样理解,栈是存放“纸”的空间,帧就是指一张“纸”,栈帧也就是存放在栈空间中记录函数调用信息的“纸”。在实际计算机中的栈常常是从内存分区的高端向下增长,而在许多的系统中是不检测溢出的。由于由太多同时在运行着的函数,用尽栈空间的情况总是可能发生的。显而易见,用尽栈空间通常是致命的错误。
在正常情况下不应该越出栈空间,发生在这种情况通常是由失控递归(忘记基准情形)。另一方面,某些完全合法并且表面上无害的程序也可以使你越出栈空间。
如下例程打印一个链表,该例程是正确的。
void PrintList(List list)
{
while (list != NULL)
{
PrintElement(list->element);
PrintList(list->next);
}
}
不幸的是,如果这个链表含有20000个元素或更多,那么就表示第6行有20000个栈帧或更多,典型的情况是这些栈帧由于它们包含全部信息而特别庞大,因此这个程序很可能要越出栈空间。
这个程序称为尾递归(tail recursion),是使用递归极端不当的例子。尾递归指的是在最后一行的递归调用。尾递归可以通过将递归调用变成 goto 语句并在其前加上对函数每个参数的赋值语句而手工消除。它模拟了递归调用,因为没有什么需要存储,在递归调用结束之后,实际上没有必要知道存储的值。因此,我们可以带着在一次递归调用中已经用过得那些值跳转到函数的顶部。
下面是改进后的程序
void PrintList(List list)
{
top:
if (list != NULL)
{
PrintElement(list->element);
list = list->next;
goto top;
}
}
但是要记住,我们应该使用自然的while循环结构。此处使用goto是为了说明编译器如何自动地去除递归。
尾递归的去除是如此简单,以至于某些编译器能够自动地完成。但是即使如此,最好在你的程序中别这样用。虽然非递归程序一般说来确实比等价的递归程序要快,但是速度优势的代价却是由于去除递归而使得程序清晰性不足。
在这里的打印例程,用自然的while循环是最好的选择。