栈还有一个重要应用是在程序设计语言中实现递归.
一、采用递归算法解决的问题所谓递归是指,若在一个函数、过程或者数据结构定义的内部又直接(或间接)出现定义本身的应用, 则称它们是递归的,
或者是递归定义的. 在以下三种情况下, 常常使用递归的方法.
1.定义是递归的
很多数学函数是递归定义的, 如大家熟悉的
阶乘函数
二阶Fibonacci数列
对于阶乘函数, 可以使用递归过程来求解.
[算法描述]
long Fact(long n)
{
if(n==0) /*递归终止的条件*/
{
return 1;
}
else /*递归步骤*/
{
return n*Fact(n-1);
}
}
类似的可以写出Fibonacci数列的递归程序.
[算法描述] long Fib(long n)
{
if(n==1 || n==2) /*递归终止的条件*/
{
return 1;
}
else /*递归步骤*/
{
return Fib(n-1)+Fib(n-2);
}
}
对于类似这种的复杂问题, 若能够分解成几个相对简单且解法相同或类似的子问题来求解, 便称作递归求解. 这种分解——求解的策略叫做"分治法".
采取"分治法"进行递归求解的问题需要满足一下三个条件:
1)能将一个问题转变成一个新问题, 而新问题与原问题的解法相同或类同, 不同的仅是处理的对象, 且这些处理对象更小且变化有规律.
2)可以通过上述转换而使问题简化.
3)必须有一个明确的递归出口, 或称递归的边界.
"分治法"求解递归问题算法的一般形式为: void p(参数表)
{
if(递归结束条件成立)可直接求解 ; /*递归终止的条件*/
else p(较小的参数); /*递归步骤*/
}
2.数据结构是递归的
某些数据结果本身具有递归的特性, 则他们的操作可递归地描述, 如链表, 就是一种递归的数据结构.
[问题描述]从概念上讲, 可将一个表头指针为L的单链表定义为一个递归结构, 即:
1)一个结点, 其指针域为NULL, 是一个单链表
2)一个结点, 其指针域指向单链表, 仍是一个单链表
对于递归的数据结构, 相应算法采用递归的方法来实现特别方便. 依次输出链表的各个结点的递归算法, 其递归结束条件是 p==NULL
依次输出链表中各个结点的递归算法
[算法描述] void Print(LinkList p)
{
if(p)
{
cout<<p->data<<endl;
Print(p->next);
}
}
3.问题的解法是递归的
还有一类问题, 虽然问题本身没有明显的递归结构, 但用递归求解比迭代求解更简单, 如八皇后问题、Hanoi塔问题等.
[问题描述]
汉诺塔问题:
1)有三根柱子A、B、C. A柱上有n个圆盘,最大的一个在底下, 其余一个比一个小, 依次叠上去。2)每次移动一个圆盘, 小盘只能叠在大盘上面.
3)把所有圆盘从A柱全部移到C柱.
如何实现移动圆盘的操作呢?可以用分治求解的递归方法来解决这个问题. 设A柱上最初的盘子总数为n, 当n=1时,只要将编号1的圆盘从塔座A直接移至塔座C上即可;
否则, 执行以下三步;
1)用C柱做过渡,将A柱上的(n-1)个盘子移到B柱上;
2)将A柱上最后一个盘子直接移动到C柱上;
3)用A柱做过渡,将B柱上的(n-1)个盘子移到C柱上;
根据这种解法,如何将n-1个圆盘从一个塔座移至另一个塔座的问题时一个和原问题具有相同特征属性的问题,只是问题的规模小1, 因此可以用同样的方法求解.
[算法描述] void Hanoi(int n, char A, char B, char C)
{
/*将塔座A上的n个圆盘按规则搬到C上, B可用做辅助塔座*/
/*搬到操作move(A,n,C)可定义为(m是初值为0的全局变量,对搬动计数)*/
/*cout<<++m<<","<<n<<","<<A<<","<<C<<endl;*/
if(n == 1)
{
move(A, 1, C); /*将编号为1的圆盘从A移到C*/
}
else
{
Hanoi(n-1, A, C, B); /*将A上编号为1至n-1的圆盘移到B, C做辅助塔座*/
move(A, n, C); /*将编号为n的圆盘从A移到C*/
Hanoi(n-1, B, A, C); /*将B上编号为1至n-1的圆盘移到C, A做辅助塔座*/
}
}
二、递归过程与递归工作栈
一个递归函数, 在函数的执行过程中, 需多次进行自我调用.那么,这个递归函数是如何执行的? 先看任意两个函数之间进行调用的情形.
在汇编程序设计主程序和子程序之间的链接及信息交换相类似, 在高级语言编制的程序中,调用函数和被调用函数之间的链接及信息交换需要通过
栈来进行.
通常, 当在一个函数的运行期间调用另一个函数时, 在运行被调用函数之前,系统需先完成3件事:
1)将所有的实参、返回地址等信息传递给被调用函数保存;
2)为被调用函数的局部变量分配存储区;
3)将控制转移到被调函数的入口;
而从被调用函数返回调用函数之前, 系统也应完成3件工作:
1)保存被调函数的计算结果;
2)释放被调函数的数据区;
3)依照被调函数保存的返回地址将控制转移到调用函数;
当有多个函数构成嵌套调用时, 按照"后调用先返回"的原则, 上述函数之间的信息传递和控制转移必须通过"栈"来实现, 即系统将整个程序运行时所需的数据空间安排在一个栈中,
每当调用一个函数时, 就为它在栈顶分配一个存储区, 每当从一个函数退出时就释放它的存储区, 则当前正运行的函数的数据区必在栈顶.
一个递归函数的运行过程类似于多个函数的嵌套调用, 只是调用函数是同一个函数, 因此, 和每次调用相关的一个重要概念是递归函数运行的"层次".三、递归算法的效率分析
在算法分析中, 当一个算法中包含递归调用时, 其时间复杂度的分析可以转化为一个递归方程求解.实际上,这个问题时数学上求解渐进阶的问题, 而递归方程的形式多种多样,
其求解方法也不一而足. 迭代法是求解递归方程的一种常用方法, 其基本步骤是迭代地展开递归方程的右端, 使之成为一个非递归的和式, 然后通过对和式的估计来达到对方程左端的估计.
下面以阶乘的递归函数Fact(n)为例, 说明通过迭代法求解递归方程来计算时间复杂度的方法.
设Fact(n)的执行时间是T(n). 次递归函数中语句 if(n == 0)return 1;的执行时间是O(1), 递归调用Fact(n-1)的调用时间是T(n-1),所以else return n*Fact(n-1);的执行时间是O(1)+T(n-1).其中,
设两个数相乘和赋值操作的执行时间为O(1), 则对某常数C、D有如下递归方程:
设n>2,利用上式对T(n-1)展开,即在上式中用n-1代替n得到
T(n-1)=C+T(n-2);
再代入T(n)=C+T(n-1)中,有
T(n)=2C+T(n-2);
同理,当n>3时有
T(n)=3C+T(n-3);
以此类推,当n>i时有
T(n)=iC+T(n-i);
最后,当i=n时有
T(n)=nC+T(0)=nC+D;
求得递归方程的解为:T(n)=O(n);
采用这种方法计算Fibonacci数列和Hanoi塔问题递归算法的时间复杂度均为O(2^n);四、将递归转换为非递归的方法
通过上述讨论可以看出递归算法的有优点结构清晰, 程序易读. 但递归程序在执行时需要系统提供隐式栈这种数据结构来实现, 占用内存空间较多, 运行效率低. 为此,
在求解某些问题时, 为提高某些的时空性能, 可以用递归算法来分析问题, 用非递归算法求解具体问题. 将递归转换为非递归的方法有两种.
1.循环方法
一般对于单向递归和尾递归的情况, 都可用循环方法将递归过程改为非递归过程.
1)单向递归是指递归函数中虽然有一处以上的递归调用语句, 但各次递归调用语句参数只和主调用函数有关, 相互之间参数无关, 而且这些递归调用语句处于算法的后面.
如前面给出的Fibonacci数列的递归算法, 在计算Fib(i)时, 必须递归调用Fib(i-1)和Fib(i-2). 而Fib(i-1)和Fib(i-2)相互之间参数无关, 其参数都由主调函数Fib(i)决定的,
所以这个递归算法是单向递归的一个典型例子. 在其递归算法中, 重复地调用函数和多次地传递参数, 而某一次递归计算的结果无法保存, 下次用时还需要重新递归计算,
因此时间复杂度为O(2^n). 这样就不如用循环实现算法的效率高, 用循环实现的Fibonacci数列的非递归算法如下.
[算法描述] long Fib(long n)
{
if(n==1 || n==2)
{
return 1;
}
else
{
t1=1;
t2=1;
for(i=3;i<=n;i++)
{
t3=t1+t2;
t1=t2;
t2=t3;
}
return t3;
}
}
用循环方式计算Fib(i)时, Fib(i-1)和Fib(i-2)已求出,无重复计算,求解Fib(n)时, 只需计算n次, 因此时间复杂度为O(n).
2)尾递归是指递归调用用语句只有一个, 且处于算法的最后,尾递归是单向递归的特例. n!递归算法是尾递归的一个典型例子.
对于尾递归,当递归返回时, 返回到上一层递归调用语句的下一语句时已到程序的末尾, 所以不需要递归工作栈保存返回地址, 且除了返回值和引用值外,
其他参数和局部变量都不需要, 因此可以不用栈, 直接用循环形式写出非递归过程, 从而提高程序的执行效率.
例如, n!的递归算法可写成如下循环算法.
[算法描述]
long Fact(long n)
{
t=1;
for(i=1;i<=n;i++)
{
t=t*i;
}
return t;
}
2、利用栈消除递归
对于一般的递归过程, 仿照递归算法执行过程中递归工作栈的状态变化可直接写出相应的非递归算法. 这中利用栈消除递归过程的步骤如下:
1)设置一个工作栈存放递归工作记录(包括实参、返回地址及局部变量等).
2)进入非递归调用入口(即被调用程序开始处)将调用程序传来的实在参数和返回地址入栈(递归程序不可以作为主程序, 因而可认为初始是被某个条用程序调用)
3)进入递归调用入口:当不满足递归结束条件时, 逐层递归, 将实参、返回地址及局部变量入栈, 这一过程可用循环语句来实现——模拟递归分解的过程
4)递归结束条件满足, 将到达递归出口的给定常数作为当前的函数值.
5)返回处理:在栈不空的情况下, 反复退出栈顶记录, 根据记录中的返回地址进行题意规定的操作, 即逐层计算当前函数值, 直至栈空为止——模拟递归求值过程.
通过以上步骤, 可将任何递归算法改写成非递归算法. 但改写后的非递归算法和原来比起来结构不够清晰, 可读性差, 有的还需要经过一系列的优化, 这里不再举例详述,
具体示例参见二叉树遍历的非递归算法.
由于递归函数结构清晰, 程序易读, 而且其正确性容易得到证明, 因此, 利用允许递归调用的语言进行程序设计时, 给用户编制程序和调试程序带来很大方便.
因为对于这样一类递归问题编程时, 不需用户自己而由系统来管理递归工作栈.