括号表达式 |
【题目描述】
假设表达式中包含三种括号:圆括号、方括号和花括号,它们可互相嵌套,如 ( [ { } ] ( [ ] ) )或( { ( [ ] [ ( ) ] ) } )等均为正确的格式,而 { [ ] } ) }或 { [ ( ) ] 或 ( [ ] }均为不正确的格式。在检验算法中可设置一个栈,每读入一个括号。
- 若读入左括号,则直接入栈,等待相匹配的同类右括号;
- 若读入右括号,且与当前栈顶的左括号同类型,则二者匹配,将栈顶的左括号出栈,否则属于不合法的情况。
- 另外,如果输入序列已读尽,而栈中仍有等待匹配的左括号,或者读入了一个右括号,而栈中已无等待匹配的左括号,均属不合法的情况。当输入序列和栈同时变为空时,说明所有括号完全匹配。
【算法描述】
/*构造一个空栈 S*/
void InitStack(SeqStack *S) {
S->top= -1;
}
/*判栈空*/
int Isempty(SeqStack *S) {
return (S->top==-1?TRUE:FALSE);
}
/*读栈顶元素*/
int GetTop(SeqStack *S, StackElementType *x) { /* 将栈 S 的栈顶元素弹出,放到 x 所指的存储空间中,但栈顶指针保持不变 */
if(S->top==-1) /*栈为空*/
return(FALSE);
else {
*x = S->elem[S->top];
return(TRUE);
}
}
/*出栈*/
int Pop(SeqStack * S, StackElementType *x) {
/* 将栈 S 的栈顶元素弹出,放到 x 所指的存储空间中 */
if(S->top= =-1) /*栈为空*/
return(FALSE);
else {
*x= S->elem[S->top];
S->top--; /* 修改栈顶指针 */
return(TRUE);
}
}
void BracketMatch(char *str)
/* str[]中为输入的字符串,利用堆栈技术来检查该字符串中的括号是否匹配*/
{
Stack S;
int i;
char ch;
InitStack(&S);
for(i=0; str[i]!='\0'; i++) {
switch(str[i]) {
case '(':
case '[':
case '{':
Push(&S,str[i]);
break;
case ')':
case ']':
case '}':
if(IsEmpty(&S)) {
printf("\n 右括号多余!");
return;
} else {
GetTop (&S,&ch);
if(Match(ch,str[i])) /*用 Match 判断两个括号是否匹配*/
Pop(&S,&ch); /*已匹配的左括号出栈*/
else {
printf("\n 对应的左右括号不同类!");
return;
}
}
}
}
if(IsEmpty(&S))
printf("\n 括号匹配!");
else
printf("\n 左括号多余!");
}
表达式求值 |
【题目描述】
表达式求值是高级语言编译的一个基本问题,是栈的典型应用实例。
任何一个表达式都是由运算数(operand)、运算符(operator)和界限符(delimiter)组成的。运算数既可以是常数,也可以是被说明为变量或常量的标识符;运算符可以分为算术运算符、关系运算符和逻辑运算符三类;基本界限符有左右括号和表达式结束符等。仅讨论无括号算术表达式求值的求值问题。
由于某些运算符可能具有比别的运算符更高的优先级,因此表达式求值不可能严格地从左到右进行,如下图所示,其中↑为幂运算,# 是表达式结束符,这是为运算方便引入的一个特殊符号。
为了正确的处理表达式,使用栈来实现正确的指令序列是一个重要的技术。下面以无括号表达式为例进行说明。
【算法思想】
(1)规定运算符的优先级表
(2)设置两个栈:OVS(运算数栈)、OPTR(运算符栈)
(3)自左向右扫描,进行如下处理:
①遇到运算数则进 OVS 栈;
②遇到运算符则与 OPTR 栈的栈顶运算符进行优先级比较:
- 如果当前运算符>OPTR 栈顶运算符,则当前运算符进 OPTR 栈;
- 如果当前运算符≤OPTR 栈顶运算符,则 OPTR 退栈一次,得到栈顶运算符θ,OVS连续退栈两次,得到运算数 a、运算数 b,对 a,b 执行θ运算,得到结果 T(i),将 T(i) 进OVS 栈。
例 3-1 在实现 A/B↑C+DE 的运算过程中,栈区变化情况如图 3.9 所示。为运算方便,
在表达式后面加上一个结束符#,并将其视为一个优先级最低的特殊运算符,所以实际输入
的表达式为:A/B↑C+DE#。
【算法描述】无括号算术表达式处理算法
/*构造一个空栈 S*/
void InitStack(SeqStack *S) {
S->top= -1;
}
/*进栈S*/
int Push(SeqStack * S, StackElementType x) {
if(S->top== Stack_Size-1) return(FALSE); /*栈已满*/
S->top++;
S->elem[S->top]=x;
return(TRUE);
}
int GetTop(SeqStack *S, StackElementType *x) { /* 将栈 S 的栈顶元素弹出,放到 x 所指的存储空间中,但栈顶指针保持不变 */
if(S->top==-1) /*栈为空*/
return(FALSE);
else {
*x = S->elem[S->top];
return(TRUE);
}
}
int Pop(SeqStack * S, StackElementType *x) {
/* 将栈 S 的栈顶元素弹出,放到 x 所指的存储空间中 */
if(S->top= =-1) /*栈为空*/
return(FALSE);
else {
*x= S->elem[S->top];
S->top--; /* 修改栈顶指针 */
return(TRUE);
}
}
int ExpEvaluation()
/*读入一个简单算术表达式并计算其值。
operator 和 operand 分别为运算符栈和运算数栈,
OPS 为运算符集合*/
{
InitStack(&operand);
InitStack(&operator);
Push(&operator,'#');
printf("\n\nPlease input an expression (Ending with #) :");
ch=getchar();
while(ch!='#'||GetTop(operator)!='#') { /* GetTop()通过函数值返回栈顶元素*/
if(!In(ch,OPS)) { /*不是操作符,是操作数*/
n=GetNumber(ch);
push(&operand,n);
} else
switch(Compare(ch, GetTop(operator))) {
case '>':
Push(&operator,ch);
ch=getchar();
break;
case '=':
case '<':
Pop(&operator,&op);
Pop(&operand,&b);
Pop(&operand,&a);
v=Execute(a,op,b); /* 对 a 和 b 进行 op 运算 */
Push(&operand,v);
break;
}
}
v=GetTop(operand);
return (v);
}
栈与递归 |
递归是指在定义自身的同时又出现了对自身的引用。
- 如果一个函数在其定义体内直接调用自己,则称为直接递归函数
- 如果一个函数经过一系列的中间调用语句,通过其他函数间接调用自己,则称为间接递归函数
(1)递归定义的数学函数
上述 Ackerman 函数可用一个简单的 C 语言函数描述如下:
int ack(int m,int n) {
if(m==0) return n+1;
else if (n==0) return ack(m-1,1);
else return ack(m-1, ack(m,n-1));
}
(2)递归数据结构的处理
在后续章节将要学习的一些数据结构,如广义表、二叉树、树等结构其本身均具有固有的递归特性,因此可以自然地采用递归法进行处理。
(3)递归求解方法
许多问题的求解过程可以用递归分解的方法描述,一个典型的例子是著名的汉诺塔问题(Hanoi)问题。
n 阶 Hanoi 塔问题:假设有三个分别命名为 X,Y 和 Z 的塔座,在塔座 X 上插有 n 个直径大小各不相同、从小到大编号为 1,2,… ,n 的圆盘。现要求将塔座 X 上的 n 个圆盘移至塔座 Z 上,并仍按同样顺序叠排。圆盘移动时必须遵循下列规则:
① 每次只能移动一个圆盘;
② 圆盘可以插在 X,Y 和 Z 中的任何一个塔座上;
③ 任何时刻都不能将一个较大的圆盘压在较小的圆盘之上。
【算法思想】当 n=1 时,问题比较简单,只要将编号为 1 的圆盘从座 X 直接移动到塔座 Z 上即可;当 n>1 时,需利用塔座 Y 作辅助塔座,若能设法将压在编号为 n 的圆盘上的 n-1 个圆盘从塔座 X(依照上述原则)移至塔座 Y 上,则可先将编号为 n 的圆盘从塔座 X 移至塔座 Z 上,然后再将塔座 Y 上的 n-1 个圆盘(依照上述原则)移至塔座 Z 上。而如何将 n-1 个圆盘从一个塔座移至另一个塔座问题是一个和原问题具有相同特征属性的问题,只是问题的规模小于 1,因此可以用同样方法求解。由此可得如下求解 n 阶 Hanoi 塔问题的递归算法。
【算法描述】 算法 3.14 汉诺塔递归算法
void hanoi(int n, char x, char y, char z)
/* 将塔座 X 上从上到下编号为 1 至 n,且按直径由小到大叠放的 n 个圆盘,按
规则搬到塔座 Z 上,Y 用作辅助塔座。*/
{
if (n == 1)
move(x, 1, z); /*将编号为 1 的圆盘从 x 移动 z*/
else
{
hanoi(n - 1, x, z, y); /* 将 X 上编号为 1 至 n-1 的圆盘移到 y,z 作辅助
塔 */
move(x, n, z); /* 将编号为 n 的圆盘从 x 移到 z */
hanoi(n - 1, y, x, z); /* 将 y 上编号为 1 至 n-1 的圆盘移到 z,x 作辅
助塔 */
}
}
- 设计递归算法的方法
递归算法就是在算法中直接或间接调用算法本身的算法。使用递归算法的前提有两个:
⑴原问题可以层层分解为类似的的子问题,且子问题比原问题的规模更小。
⑵规模最小的子问题具有直接解。 - 设计递归算法的原则是用自身的简单情况来定义自身,设计递归算法的方法是:
⑴寻找分解方法:将原问题转化为子问题求解。( 例:n!=n*(n-1)! )
⑵设计递归出口:即根据规模最小的子问题,确定递归终止条件。(例:求解 n!时,当 n=1 时,n!=1)。 - 递归过程的实现
递归进层(i→i +1 层)系统需要做三件事:
⑴ 保留本层参数与返回地址;
⑵ 为被调用函数的局部变量分配存储区,给下层参数赋值;
⑶ 将程序转移到被调函数的入口。
而从被调用函数返回调用函数之前,递归退层(i←i +1 层)系统也应完成
三件工作:
⑴ 保存被调函数的计算结果;
⑵ 释放被调函数的数据区,恢复上层参数;
⑶ 依照被调函数保存的返回地址,将控制转移回调用函数。
递归与非递归转换 |
- 简单递归的消除
①单向递归
单向递归是指递归函数中虽然有一处以上的递归调用语句,但各次递归调用语句的参数只和主调用函数有关,相互之间参数无关,并且这些递归调用语句处于算法的最后。
计算斐波那契数列的递归算法 Fib(n) 是单向递归的一个典型例子。
int fib(int n){
if(n==0||n==1)
return n;
else return fib(n-1)+fib(n-2);
}
用循环方式计算 Fib(n)时,Fib(i) (i=2…n)是在已计算过 Fib (i-1)与 Fib (i-2)的基础上进行的,无重复计算,时间复杂度为 O(n)。虽然非递归算法没有递归算法直观,但时空耗费远少于递归算法。
而用递归方式计算 Fib (n)时,必须计算 Fib (n-1)与 Fib (n-2),而某次递归计算得出的结果,如 Fib (3)无法保存,下一次要用到时还需要重新递归计算,因此其时间复杂度为 O(2n)。
int Fib(int n){/*计算 Fib (0)或 Fib(1) */
int x,y,z;
if(n==0|n==1) return n;
else{
x=0,y=1;/* x= Fib (0) y= Fib (1) */
for(i=2;i<=n;i++){
z=y;/* z= Fib (i-1) */
y=x+y;/* y= Fib (i-1)+ Fib (i-2) 求 Fib (i) */
x=z;/* x= Fib (i-1) */
}
}
return y;
}
- ②尾递归
尾递归是指递归调用语句只有一个,而且是处于算法的最后,尾递归是单向递归的特例。
以阶乘问题的递归算法 Fact(n)为例讨论尾递归算法的运行过程,算法如下:
long Fact(int n)
{
if(n= =0) return 1;
return n*Fact(n-1);
}
对于尾递归形式的递归算法,不必利用系统的运行时栈保存各种信息。尾递归形式的算法实际上可变成循环结构的算法。循环结构的阶乘问题算法 Fact(n)如下:
long Fact(int n)
{
int fac = 1;
for (int i = 1; i <= n; i++)/*依次计算 f(1)… f(n)*/
{
fac = fac * i;/* f(i)= f(i-1)*i */
}
return fac;
}