栈 与 递 归

[递归算法及在计算机中的实现]

1、 递归算法
例1、用递归算法把任一给定的十进制正整数(<=32000)转换成八进制数输出,程序如下:
var m:integer;
procedure tran(n:integer); {递归过程}
var k:integer;
begin
k:=n mod 8;
n:=n div 8;
if n<>0 then tran(n);
write(k:1)
end;
begin {主程序}
write('input m:');
readln(m);
write(m,'=(');
tran(m);
writeln(')8');
readln;
end.
输入:m=765 {下划线表示输入}
输出:765=(1375)8

例2、用递归算法求N阶乘(N!=1*2*3*……*N,N<20);
var n:integer;
function f(n:integer):longint; {递归函数,N=20时,超过maxlongint}
begin
if n=0 then f:=1
else f:=n*f(n-1)
end;

begin {主程序}
write('input n:');
readln(n);
write(n,'!=',f(n));
end.

2、 递归在计算机中的实现
计算机执行递归算法时,是通过栈来实现的。具体说来,就是在(递归过程或递归函数)开始运行时,系统首先为递归建立一个栈,该栈的元素类型(数据域)包括值参、局部变量和返回地址;在每次执行递归调用语句时之前,自动把本算法中所使用的值参和局部变量的当前值以及调用后的返回地址压栈(一般形象地称为“保存现场”,以便需要时“恢复现场”返回到某一状态),在每次递归调用结束后,又自动把栈顶元素(各个域)的值分别赋给相应的值参和局部变量(出栈),以便使它们恢复到调用前的值,接着无条件转向(返回)由返回地址所指定的位置继续执行算法。
具体到上面的例1中,当遇到递归调用tran(n)时,系统首先为后面的递归调用建立一个含有3个域(值参n,局部变量k和一个返回地址)的栈;在每次执行递归调用tran(n)前,系统自动把n和k的当前值以及write(k:1)语句的开始位置(即调用结束后的返回地址)压栈;在每次执行到最后的end语句(即一次递归调用结束)后,又自动把栈顶的与n和k对应的值分别赋给n和k(出栈),接着无条件转向write(k:1)语句的开始位置继续向下执行程序。


3、 递归的缺点
早期操作系统DOS的内核是限制只能使用640K内存(当时的机器内存也很小),在此之上的BP运行时,可用的所有内存空间最大也只能为640K,其中程序代码、常量、变量和堆栈(局部变量、子程序参数、返回地址;即递归中的栈)各占64K,在BP中的OPTIONS菜单的MEMORY SIZE子菜单中可以修改STACK SIZE至多到64K(一般设置为65520),可以解决很多递归程序遇到的栈溢出错误。
但有时还是不够,这时就想用640K以内(64K以上)的多余空间,方法是手工开辟一个栈空间模拟系统处理递归的实现方法,这样就可以把一个递归过程转换成非递归过程,一方面可以进一步加深对栈和递归的理解,另一方面也可以解决一些递归无法解决的问题。但带来的问题是程序会很复杂。

[递归转换为非递归]
设P是一个递归算法,假定P中共有m个值参和局部变量,共有t处递归调用P的语句,则把P改写成一个非递归算法的一般规则为:
1、 定义一个栈S,用来保存每次递归调用前值参和局部变量的当前值以及调用后的返回地址。即S应该含有m+1个域,且S的深度必须足够大,使得递归过程中不会发生栈溢出。
2、 定义t+2个语句标号,其中用一个标号标在原算法中的第一条语句上,用另一个标号标在作返回处理的第一条语句上,其余t个标号标在t处递归调用的返回地址,分别标在相应的语句上。
3、 把每一个递归调用语句改写成如下形式:
(1) 把值参和局部变量的当前值以及调用后的返回地址压入栈;
(2) 把值参所对应的实在参数表达式的值赋给值参变量;
(3) 无条件转向原算法的第一条语句;
4、 在算法结束前增加返回处理,当栈非空时做:
(1) 出栈;
(2) 把原栈顶中前m个域的值分别赋给各对应的值参和局部变量;
(3) 无条件转向由本次返回地址所指定的位置;
5、 增设一个同S栈的成分类型(元素)相同的变量,作为进出栈的缓冲变量,对于递归函数,还需要再增设一个保存函数值中间结果的临时变量,用这个变量替换函数体中的所有函数名,待函数结束之前,在把这个变量的值赋给函数名返回。
6、 在原算法的第一条语句之前,增加一条把栈置空的语句。
7、 对于递归函数而言,若某条赋值语句中包含两处或多处递归调用(假设为n处),则应首先把它拆成n条赋值语句,使得每条赋值语句只包含一处递归调用,同时对增加的n-1条赋值语句,要增设n-1个局部变量,然后按以上六条规则转换成非递归函数。

[应用举例]
例3、 把例1中的递归过程改写成非递归过程。
procedure tran(n:integer); {非递归过程}
label 1,2,3; {因为只有1处递归调用,所以定义t+2=3个标号}
type node=record {定义栈的成分类型,因为值参和局部变量共2个,所以m+1=3个域}
n:integer; {值参n 的域}
k:integer; {局部变量k的域}
r:integer; {返回地址的域}
end;
stack=record {定义一个栈类型,包括数据域(一个数组)和一个栈顶指针域}
vec:array[1..7] of node; {32000以内的十进制正整数转换成八进制数,不会超过七位数,数组元素类型为node类型}
top:integer; {栈顶指针}
end;
var s:stack; {定义栈变量}
x:node; {进出栈的缓冲变量}
k:integer; {原来的局部变量}

procedure push(var s:stack;x:node); {进栈过程,注意s 一定要定义成变量型参数}
begin {因为栈的变化要带出过程}
if s.top=7 then begin write('up-overflow');exit;end
else begin s.top:=s.top+1;s.vec[s.top]:=x;end;
end;

procedure pop(var s:stack;var x:node); {出栈过程,都要定义成变量型参。一方面出栈的元素存放在x中要带出过程,另外栈顶指针也变化了,所以s也要定义成变量型参}
begin 
if s.top=0 then begin write('down-overflow');exit;end
else begin x:=s.vec[s.top];s.top:=s.top-1;end;
end;

begin
s.top:=0; {按照第6条}
1:k:=n mod 8; {按照第2条的红色语句}
n:=n div 8;
if n<>0 then begin {按照第3条,3个步骤,本题不需要第(2)小句}
x.n:=n;
x.k:=k;
x.r:=2;
push(s,x); {(1)}
goto 1; {(3)}
end;
2:write(k:1); {按照第2条的蓝色语句}
3:if s.top>0 then begin {按照第4条,3个步骤}
pop(s,x); {(1)}
n:=x.n; 
k:=x.k; {(2)}
goto 2; {(3)} 
end;
end; {建议:单步跟踪各个变量,观察理解过程}

例4、把例2中的递归函数改写成非递归函数
function f(n:integer):longint; {非递归函数}
label 1,2,3;
var s:array[1..20] of integer;{栈,必须大于等于n,保证不溢出}
top:integer; {栈顶}
f1:longint; {保存中间结果的临时变量}
begin
top:=0; {栈的初始化}
1:if n=0 then begin f1:=1;goto 3;end {遇到边界就结束转返回处理}
else begin top:=top+1; {否则,进栈}
a[top]:=n;
n:=n-1; {实参减1}
goto 1; {转向开始,继续}
end;
2:f1:=n*f1; {根据n和f(n-1),求f(n)}
3:if top>0 then begin n:=a[top]; {做返回处理}
top:=top-1;
goto 2; {转向返回地址}
end;
f:=f1; {赋值}
end;
注意:
1、 上面的程序其实已经进行了简化,一是栈只设置了一个保存值参n的域,二是忽略了缓冲变量,而直接用n,三是省略了返回地址,因为每个递归调用的返回地址都相同;
2、 以上算法中,从标号1到goto 1所构成的循环,实际上是一个递推过程;从n推到0为止;从标号2到goto 2所构成的循环是一个回代过程;
3、 假设n=5,请大家画出栈的变化情况。

[小结思考]
从以上可以看出,递归算法简单直观,是整个计算机算法和程序设计领域一个非常重要的方面,必须熟练掌握和应用它。但计算机的执行过程比较复杂,需要用系统栈进行频繁的进出栈操作和转移操作。递归转化为非递归后,可以解决一些空间上不够的问题,但程序太复杂。所以,并不是一切递归问题都要设计成非递归算法。实际上,很多稍微复杂一点的问题(比如:二叉树的遍历、图的遍历、快速排序等),不仅很难写出它们的非递归过程,而且即使写出来也非常累赘和难懂。在这种情况下,编写出递归算法是最佳选择,有时比较简单的递归算法也可以用迭代加循环或栈加循环的方法去实现。如:
function f(n:integer):integer; {求第n个fibonacci数,迭代+循环}
var I,f1:integer;
begin
i:=0;
f1:=1;
while i<n do begin i:=i+1;f1:=i*f1; end;
f:=f1;
end;

procedure tran(n:integer);{例1改写成:栈+循环}
var s:array[1..7] of integer;
I,top:integer;
Begin
Top:=0;
While n<>0 do
Begin
Top:=top+1;
s[top]:=n mod 8;
n:=n div 8;
end;
for i:=top downto 1 do write(s[i]:1); 
End;

[栈与回溯法]
由于回溯法采用的也是递归算法,所以在实现时也是用栈实现的。当然,回溯法的程序也可以改成非递归的、用栈模拟执行。比如下面的这个程序是验证“四色原理”的,请你改写成非递归算法。
const num=20; {最多20个区域}
var a:array [1..num,1..num] of 0..1;{用邻接矩阵表示图,0—表示两个区域不相邻,
1—表示相邻}
s:array [1..num] of 0..4; {用1-4分别代表RBWY四种颜色;0代表末填进任何颜色}
k1,k2,n:integer;

function pd(i,j:integer):boolean;{判断可行性:第I个区域填上第J种颜色}
var k:integer;
begin
for k:=1 to i-1 do
if (a[i,k]=1) and (j=s[k]) {区域I和J相邻且将填进的颜色和已有的颜色相同}
then begin pd:=false; exit; end;
pd:=true;
end;

procedure print;{打印结果}
var k:integer;
begin
for k:=1 to n do {将数字转为RBWY串输出}
case s[k] of
1:write('R':4);
2:write('B':4);
3:write('W':4);
4:write('Y':4);
end;
writeln;
end;

procedure try(i:integer); {递归回溯}
var j:integer;
begin
for j:=1 to 4 do
if pd(i,j) then begin
s[i]:=j;
if i=n then print 
else try(i+1);
s[i]:=0;
end;
end;

BEGIN {主程序,输入一个图的邻接矩阵,输出一种“四色”填色方案}
write('please input city number: '); readln(n);
writeln('please input the relation of the cities:');
for k1:=1 to n do {读入邻接矩阵}
begin
for k2:=1 to n do read(a[k1,k2]);
readln;
end;
for k1:=1 to n do s[k1]:=0; {初始化}
try(1); 
END.

[作业]
作业1、用递归算法计算Fibonacci数列的任一项。再改写成非递归算法(用栈模拟)。
Fibonacci数列为以下形式的一系列整数:0 1 1 2 3 5 8 13 21 34 55 89 144……

作业2、用递归算法实现hanoi(汉诺塔问题)。再改写成非递归算法(用栈模拟)。

转自:http://www.oi.bbjy.com/n61c6.aspx

posted on 2014-08-15 22:19  IT先生  阅读(292)  评论(0编辑  收藏  举报