第三章--栈和队列
□3.2.4 迷宫求解
由于计算机解迷宫时,通常用的是"穷举求解"的方法,即从入口出发,顺某一方向向前探索,若能走通,则继续往前走,否则沿原路退回,换一个方向再继续探索。直至所有可能的通路都探索到为止。
求迷宫中一条从入口到出口的路径的算法可以简单描述如下:
do{
若当前位置可通,
则{
将当前位置插入栈顶;
若该位置是出口位置,则结束;
否则切换当前位置的东邻方块为新的当前位置;
}
否则,
若栈不空且栈顶位置尚有其他方向未经探索,
则设定新的当前位置为沿顺时针方向旋转找到的栈顶位置的下一相邻块;
若栈不空但栈顶位置的四周均不可通
则
删去栈顶位置;从路径中删去该通道块;
若栈不空,则重新测试新的栈顶位置,
直至找到一个可通的相邻块或出栈至空栈;
}while(栈不空)
#include "Stack.h" /* 全局变量 */ #define MAXLENGTH 25 /* 设迷宫的最大行列为25 */ typedef int MazeType[MAXLENGTH][MAXLENGTH]; /* 迷宫数组[行][列] */ MazeType m; /* 迷宫数组 */ int curstep = 1; /* 当前足迹,初值为1 */ Status Pass(PosType b) { /* 当迷宫m的b点的序号为1(可通过路径),return OK; 否则,return ERROR。 */ if(m[b.x][b.y] == 1){ return OK; } else{ return ERROR; } } void FootPrint(PosType a) { /* 使迷宫m的a点的序号变为足迹(curstep) */ m[a.x][a.y] = curstep; } PosType NextPos(PosType c, int di) { /* 根据当前位置及移动方向,返回下一位置 */ PosType direc[4]={{0,1},{1,0},{0,-1},{-1,0}}; /* {行增量,列增量} */ /* 移动方向,依次为东南西北 */ c.x += direc[di].x; c.y += direc[di].y; return c; } void MarkPrint(PosType b) { /* 使迷宫m的b点的序号变为-1(不能通过的路径) */ m[b.x][b.y] = -1; } /*若迷宫maze中存在从入口Start到出口end的通道,则求得一条存放在栈中 (从栈底到栈顶)并返回TRUE;否则返回False */ Status MazePath(MazeType maze, PosType start, PosType end) /* 算法3.3 */ { SqStack S; PosType curpos; SElemType e; InitStack(&S); curpos = start; do{ if(Pass(curpos)){ /* 当前位置可以通过,即是未曾走到过的通道块 */ FootPrint(curpos); /* 留下足迹 */ e.ord = curstep; e.seat.x = curpos.x; e.seat.y = curpos.y; e.di = 0; Push(&S, e); /* 入栈当前位置及状态 */ curstep++; /* 足迹加1 */ if(curpos.x == end.x && curpos.y == end.y){ /* 到达终点(出口) */ return TRUE; } curpos = NextPos(curpos, e.di); } else{ /* 当前位置不能通过 */ if(!StackEmpty(S)){ Pop(&S, &e); /* 退栈到前一位置 */ curstep--; while(e.di == 3 && !StackEmpty(S)){ /* 前一位置处于最后一个方向(北) */ MarkPrint(e.seat); /* 留下不能通过的标记(-1) */ Pop(&S, &e); /* 退回一步 */ curstep--; } if(e.di < 3){ /* 没到最后一个方向(北) */ e.di++; /* 换下一个方向探索 */ Push(&S, e); curstep++; curpos = NextPos(e.seat,e.di); /* 设定当前位置是该新方向上的相邻块 */ } } } }while(!StackEmpty(S)); return FALSE; } void Print(int x,int y) { /* 输出迷宫的解 */ int i,j; for(i=0;i<x;i++){ for(j=0;j<y;j++){ printf("%3d",m[i][j]); } printf("\n"); } } int _tmain(int argc, _TCHAR* argv[]) { PosType begin,end; int i,j,x,y,x1,y1; printf("请输入迷宫的行数,列数(包括外墙):"); scanf("%d,%d",&x,&y); for(i=0;i<x;i++) /* 定义周边值为0(同墙) */ { m[0][i]=0; /* 行周边 */ m[x-1][i]=0; } for(j=1;j<y-1;j++) { m[j][0]=0; /* 列周边 */ m[j][y-1]=0; } for(i=1;i<x-1;i++) for(j=1;j<y-1;j++) m[i][j]=1; /* 定义通道初值为1 */ printf("请输入迷宫内墙单元数:"); scanf("%d",&j); printf("请依次输入迷宫内墙每个单元的行数,列数:\n"); for(i=1;i<=j;i++) { scanf("%d,%d",&x1,&y1); m[x1][y1]=0; /* 定义墙的值为0 */ } printf("迷宫结构如下:\n"); Print(x,y); printf("请输入起点的行数,列数:"); scanf("%d,%d",&begin.x,&begin.y); printf("请输入终点的行数,列数:"); scanf("%d,%d",&end.x,&end.y); if( MazePath(m,begin,end) ) /* 求得一条通路 */ { printf("此迷宫从入口到出口的一条路径如下:\n"); Print(x,y); /* 输出此通路 */ } else{ printf("此迷宫没有从入口到出口的路径\n"); } return 0; }
在此,尚需说明的一点是,所谓当前位置可通,指的是未曾走到过的通道块,即要求该方块位置不仅是通道块,而且既不在当前路径上,也不是曾经纳入过路径的通道块。
□3.2.5 表达式求值
表达式求值是程序设计语言编译中的一个最基本问题。
首先要了解算术四则运算的规则:
(1)先乘除后加减
(2)从左算到右
(3)先括号内,后括号外。
任何一个表达式都是由操作数(operand),运算符(operator),界限符(delimiter)组成的,我们称它为单词。为了实现算符优先算法,可以使用两个工作栈。一个称作OPTR,用以寄存运算符;另一个称作OPND,用以寄存操作数或运算结果。算法的基本思想是:
(1)首先置操作数栈为空栈,表达式起始符"#"为运算符栈的栈底元素。
(2)依次读入表达式中每个字符,若是操作数则进OPND栈,若是运算符则和OPTR栈的栈顶运算符比较优先权后作相应操作,直至整个表达式求值完毕。(即OPTR栈的栈顶元素和当前读入的字符均为"#")。
#include "Stack.h" Status In(SElemType c) { /* 判断c是否为运算符 */ switch(c){ case'+': case'-': case'*': case'/': case'(': case')': case'#': return TRUE; default: return FALSE; } } SElemType Precede(SElemType t1, SElemType t2) { /* 根据教科书表3.1,判断两符号的优先关系 */ SElemType f; switch(t2){ case '+': case '-': if(t1 == '(' || t1 == '#'){ f = '<'; } else{ f = '>'; } break; case '*': case '/': if( t1 == '*' || t1 == '/' || t1==')'){ f = '>' ; } else{ f = '<'; } break; case '(': if( t1 == ')'){ printf("ERROR1\n"); exit(ERROR); } else{ f = '<'; } break; case ')': switch(t1){ case '(': f = '='; break; case '#': printf("ERROR2\n"); exit(ERROR); default: f = '>'; } break; case '#': switch(t1){ case '#': f = '='; break; case '(': printf("ERROR3\n"); exit(ERROR); default: f = '>'; } } return f; } SElemType Operate(SElemType a, SElemType theta, SElemType b) { SElemType c; a = a - 48; b = b - 48; switch(theta){ case'+': c = a + b + 48; break; case'-': c = a - b + 48; break; case'*': c = a * b + 48; break; case'/': c = a / b + 48; break; } return c; } /* 算术表达式求值的算符优先算法。设OPTR和OPND分别为运算符栈和运算数栈 */ SElemType EvaluateExpression() { SqStack OPTR; SqStack OPND; SElemType a,b,x,c,theta; InitStack(&OPTR); InitStack(&OPND); Push(&OPTR,'#'); c = getchar(); GetTop(OPTR, &x); while(c != '#' || x != '#'){ if(In(c)){ /* 是7种运算符之一 */ switch(Precede(x,c)){ case'<': Push(&OPTR,c); /* 栈顶元素优先权低 */ c = getchar(); break; case'=': Pop(&OPTR,&x); /* 脱括号并接收下一字符 */ c = getchar(); break; case'>': Pop(&OPTR, &theta); /* 退栈并将运算结果入栈 */ Pop(&OPND, &b); Pop(&OPND, &a); Push(&OPND, Operate(a,theta,b)); break; } } else if(c >= '0'&&c <= '9'){ /* c是操作数 */ Push(&OPND, c); c = getchar(); } else{ /* c是非法字符 */ printf("ERROR4\n"); exit(ERROR); } GetTop(OPTR, &x); }//while /*返回操作数及其结果*/ GetTop(OPND, &x); return x; } int _tmain(int argc, _TCHAR* argv[]) { printf("请输入算术表达式(中间值及最终结果要在0~9之间),并以#结束\n"); printf("%c\n",EvaluateExpression()); return 0; }
我们把运算符和界限符统称为算符,他们构成的集合命名为OP。根据上述3条运算规则,在运算的每一步中,任意两个相继出现的算符之间的优先关系至多是下面3种关系之一:
①的优先权低于②
①的优先权等于②
①的优先权高于②
算法中还调用了两个函数:其中Precede()是判定运算符栈的栈顶运算符与读入运算符之间优先关系的函数。Operate为a和b进行二元运算的函数。如果是编译表达式,则产生这个运算的一组相应指令并返回存放结果的中间变量名;如果是解释执行表达式,则直接进行该运算。
□3.3 栈与递归的实现
栈还有一个重要应用是在程序设计语言中实现递归。一个直接调用自己或通过一系列的调用语句间接地调用自己的函数,称作递归函数。递归是程序设计中的一个强有力的工具.其一,有很多数学函数是递归定义的,如大家熟悉的阶乘函数,2阶Fibonacci数列和Ackerman函数。其二有的数据结构,如二叉树,广义表等,由于结构本身固有的递归特征,则它们的操作可递归地描述。其三,还有一类问题,虽然问题本身没有明显的递归结构,但用递归求解比迭代求解更简单,如八皇后问题,Hanoi塔问题等。
int gCount=0; /* 全局变量,搬动次数 */ /* 第n个圆盘从塔座x搬到塔座z */ void move(char x, int n, char z) { printf("第%i步: 将%i号盘从%c移到%c\n", ++gCount, n, x, z); } /* 将塔座x上按直径由小到大且自上而下编号为1至n的n个圆盘按规则搬到 塔座z上,y可用作辅助塔座。 搬动操作move(x,n,z)可以定义为(c是初值为0的全局变量,对搬动计数) */ void hanoi(int n, char x, char y, char z) { 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作辅助塔*/ } } int _tmain(int argc, _TCHAR* argv[]) { int n; printf("3个塔座为a、b、c,圆盘最初在a座,借助b座移到c座。请输入圆盘数:"); scanf("%d", &n); hanoi(n,'a','b','c'); return 0; }
如何实现移动圆盘的操作呢?当n=1时,问题比较简单,只要将编号为1的原盘从塔座X直接移动至塔座Z上即可;当n > 1时,需利用塔座Y作辅助塔座,若能设法将压在编号为n的圆盘之上的n-1个圆盘从塔座X移至塔座Y上,则可先将编号为n的原盘从塔座X移至塔座Z上,然后再将塔座Y上的n-1个圆盘移动至塔座Z上。