栈
与
递
归
回
溯
一、递归算法及在计算机中的实现
1、
递归算法举例
例
1、用递归算法求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、用递归算法把任一给定的十进制正整数(<=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、
递归在计算机中的实现
递归的优点是程序思路很清晰,结构简单。其实计算机在执行递归算法时,是通过栈来实现的。具体说来,就是在递归子程序开始运行时,系统首先为递归子程序建立一个栈,该栈的元素类型(数据域)包括值参、局部变量和返回地址;在每次执行递归调用语句之前,自动把本子程序中所使用的值参和局部变量的当前值以及调用后的返回地址压栈(一般形象地称为“
保存现场”,以便需要时“
恢复现场”返回到某一状态),在每次递归调用结束后,又自动把栈顶元素(各个域)的值分别赋给相应的值参和局部变量(出栈),以便使它们恢复到调用前的值,接着无条件转向(返回)由返回地址所指定的位置继续执行子程序。
具体到上面的例
2中,当遇到递归调用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、把例
2中的递归过程改写成非递归过程。
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、把例1中的递归函数改写成非递归函数
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;
五、栈与回溯法
由于回溯法采用的也是递归算法,所以在实现时也是用栈实现的。当然,回溯法的程序也可以改成非递归的、用栈模拟执行。
例
5、下面的这个程序是验证“四色原理”的,请你改写成非递归算法。
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.
例
6、自然数的拆分。任何一个大于1的自然数总可以拆分成若干个自然数之和。
4=
1+1+1+1
4=
1+1+2
4=
1+3
4=
2+2
[问题分析]
设拆分出的数
s1≤s2≤…≤sk。定义数组s为一个栈,用来存放因子。从1开始搜索因子,求和,若sum ≤n就将因子压栈;若sum =n,则输出解,出栈;若sum >n,则修改栈顶元素的值,即回溯。
top:=1;sum:=0; j:=0; {j是因子}
repeat
j:=j+1;
sum:=sum+j;
if sum<=n then begin
s[top]:=j; { j进栈
}
if sum=n then begin
print; {调用过程
}
sum:=sum-s[top];top:=top-1; {出栈}
sum:=sum-s[top];
j:=s[top];
end
else begin {继续找下一个因子}
j:=s[top]; top:=top+1;
end
end
else begin
{sum>n的情况}
sum:=sum-j; top:=top-1;
sum:=sum-s[top];j:=s[top];
end
until top=0;
六、作业
作业1、用递归算法计算Fibonacci数列的任一项,再改写成非递归算法(用栈模拟)。
Fibonacci数列为以下形式的一系列整数:0 1 1 2
3 5 8 13 21 34 55 89 144……
作业
2、用递归算法实现hanoi(汉诺塔问题),再改写成非递归算法(用栈模拟)。
作业3、自然数的拆分一个非素数的正整数可以表示成因子的乘积(因子不包括
1和它本身),如:12=2*2*3=4*3=2*6
编程读入一个整数,求出所有的因子乘积的表达式。