栈的应用举例
栈结构所具有的“先进后出”特性,使得栈成为程序设计中的有用工具。今后将学习几个栈应用的典型例子。
1.
数值转换
假设要将十进制数N转换为d进制数,一个简单的转换算法是重复下述两步,直到N等于零:
X=N mod d(其中mod为求余运算)
N=N div d(其中div为整除运算)
|
在上述计算过程中,第一次求出
X值为d进制数的最低位,最后依次求出的X值为d进制数的最高位,所以上述算法是从低位到高位顺序产生d进制数各个数位上的数。
以
d=2威力给出上述算法:输入任意一个非负十进制整数,打印输出与其相应的二进制数。由于上述计算过程是从低位到高位顺序产生二进制数各个数位上的数,而打印输出时应从高位到低位进行,恰好与计算过程相反。根据这个特点,我们可以利用栈来实现,即将计算过程中依次得到的二进制数码按顺序进栈,计算结束后,再顺序出栈,并按出栈序列打印输出。这样即可得到给定的十进制数对应的二进制数。这是利用栈先进后出特性的最简单的例子。当然,本例用数组直接实现也完全可以,但用栈实现时,逻辑过程更清楚。
void Conversion(int N)
{/*对于任意的一个非负十进制数N,打印出与其等值的二进制数*/
Stack S;int x;/*S为顺序栈或链栈*/
InitStack(&S);
while(N>0)
{
x=N%2;
Push(&S,x);/*将转换后的数字压入栈S*/
N=N/2;
}
while(!IsEmpty(&S))
{
Pop(&S,&x);
printf("%d",x);
}
}
|
2.
括号匹配问题
假设表达式中包含三种括号:圆括号、方括号和花括号,它们可互相嵌套,如
([{}]([]))或({([][()])})等均为真确的格式,而{[]})}或{[()]或([]}均为不正确的格式。在检验算法中可设置一个栈,没读入一个括号,若是左括号,则直接入栈,等待相匹配的同类右括号;若读入的是右括号,且与当前栈顶的左括号同类型,则二者匹配,将栈顶的左括号出栈,狗则属于不合法的情况。另外,如果输入序列已读尽,而栈中仍有等待匹配俄左括号,或者读入了一个右括号,而栈中已无等待匹配的左括号,均属不合法的情况。当输入序列和栈同时变为空时,说明所有括号完全匹配。
void BracketMatch(char *str)
/*str[]中为输入的字符串,利用堆栈技术来检查该字符串中的括号是否匹配*/
{
Stack S;int i;char ch;
InitStack(&S);
For(i=0;srt[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;
}
}/*switch*/
}/*for*/
if(IsEmpty(&S))
printf("/n括号匹配!");
else
printf("/n左括号多余!");
}
|
3.
表达式求值
表达式求值是高级语言编译中的一个基本问题,是栈的典型应用实例。任何一个表达式都是由操作数(
operand)、运算符(operator)和界限符(delimiter)组成的。操作数既可以是常数,也可以是被说明为变量或常量的标识符;运算符可以分为算术运算符、关系运算符和逻辑运算符三类;基本界限符有左右括号好表达式结束符等。
这次我将只学习简单的算术表达式的求值问题。
1
)无括号算术表达式求值
■表达式计算
程序设计语言中都有计算表达式的问题,这是语言编译中的典型问题。
(1) 表达式形式:有运算对象、运算符及必要的表达式括号组成;
(2) 表达式运算:运算时要有一个正确的运算形式顺序。
程序设计语言中都有计算表达式的问题,这是语言编译中的典型问题。
由于某些运算符可能具有比别的运算符更高的优先级,因此表达式不可能严格的从左到右,见图
3.5。
■算法实现
为了正确地处理表达式,使用栈来实现正确的指令序列是一个重要的技术。无括号算术表达式的出炉规则如下,其过程见图
3.6。
2
)算术表达式粗粒规则
(1) 规定优先级表。
(2) 设置两个栈:
OVS(运算数栈)好OPTR(运算符栈)。
(3) 自左享有扫描,遇操作数进
OVS,遇操作符则与OPTR栈顶优先数比较:当前操作符>OPTR栈顶,当前操作符进OPTR栈;当前操作符≤OPTR栈顶,OVS栈顶、次项和OPTR栈顶,退栈形成运算T(i),T(i)进OVS栈。
例:实现
A/B↑C+D*E#的运算过程时栈取变化情况如图3.7所示。
3
)带括号算术表达式
假设操作数是整型常数,运算符只含加、减、乘、除等四种运算符,界限符有左右括号和表达式起始、结束符“
#”,如:#(7+15)*(23-28/4)#。引入表达式起始、结束符是为了方便。要对一个简单的算术表达式求值,首先要了解算术四则运算的规律,即:
(
1)先左后右;
(
2)先乘除,后加减;
(
3)先括号内,后括号外。
下面学习一种能够按上述规则对简单算术表达式求值的算符,通常称为“算符优先法”算符优先法适用于一般的表达式求值。
运算符和界限符可统称为算符,它们构成的集合命名为
OPS。根据上述三条运算规则,在运算过程中,任意两个前后相继出现的算符θ1和θ2之间的优先级关系比为下面三种关系之一:
θ
1<θ
2,θ
1的优先权低于θ
2。
θ
1=θ
2,θ
1的优先权等于θ
2。
θ
1>θ
2,θ
1的优先权高于θ
2。
相继出现的算符θ
1和θ
2之间所有可能的优先关系如表3-1所示。
表3-1 算符之间的优先关系
| |||||||
θ
1
θ
2
|
+
|
-
|
*
|
/
|
(
|
)
|
#
|
+
|
>
|
>
|
<
|
<
|
<
|
>
|
>
|
-
|
>
|
>
|
<
|
<
|
<
|
>
|
>
|
*
|
>
|
>
|
>
|
>
|
<
|
>
|
>
|
/
|
>
|
>
|
>
|
>
|
<
|
>
|
>
|
(
|
<
|
<
|
<
|
<
|
<
|
=
|
|
)
|
>
|
>
|
>
|
>
|
|
>
|
>
|
#
|
<
|
<
|
<
|
<
|
<
|
|
=
|
实现算符优先算法时需要使用两个工作栈:一个称作
operator,用以存放运算符;另一个称作operand,用以存放操作数或运算的中间结果。算法的基本过程如下:
首先初始化操作数栈
operand和运算符栈operator,并将表达式起始符“#”压入运算符栈;
依次读入表达式中的每个字符,若是操作数则直接进入操作数栈
operand,若是运算符,则与运算符栈operator的栈顶运算符进行优先权比较,并做如下处理:
(1) 若栈顶运算符的优先级低于刚读入的运算符,则让刚读入的运算符进
operator栈;
(2) 若栈顶运算符的优先级高于当读入的运算符,则将栈顶运算符退栈,送入θ,同时将操作数栈
operand退栈两次,得到两个操作数a、b,对a、b进行θ运算后,将运算结果作为中间结果推入operand栈;
(3) 若栈顶运算符的优先级与刚读入的运算符的优先级相同,说明左右括号相遇,只需将栈顶运算符(左括号)退栈即可。
当
operator栈的栈顶元素和当前读入的字符均为“#”时,说明表达式起始符“#”与表达式结束符“#”相遇,整个表达式求值完毕。
算符具体描述如下:
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))/*不是操作符,是操作数*/
{
int temp;/*存放数字的临时变量*/
temp=ch-'0';/*将字符转换为十进制数*/
ch=getchar();
while(! In(ch,OPS))/*用ch逐个读入操作数的各位数码,并转化为十进制数temp*/
{
temp=temp*10+ch-'0';/*将逐个读入的操作数各位转化为十进制数*/
ch=getchar();
}
Push(&operand,temp);
}
else
switch(Compare(GetTop(operator),ch))
{
case'<':Push(&operator,ch);
ch=getchar();
break;
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);
}
|