递归转非递归
http://blog.csdn.net/shunrei/article/details/5680579
递归算法实际上是一种分而治之的方法,它把复杂问题分解为简单问题来求解。对于某些复杂问题(例如hanio塔问题),递归算法是一种自然且合乎逻辑的解决问题的方式,但是递归算法的执行效率通常比较差。因此,在求解某些问题时,常采用递归算法来分析问题,用非递归算法来求解问题;另外,有些程序设计语言不支持递归,这就需要把递归算法转换为非递归算法。
将递归算法转换为非递归算法有两种方法,一种是直接求值(迭代/循环),不需要回溯;另一种是不能直接求值,需要回溯。前者使用一些变量保存中间结果,称为直接转换法;后者使用栈保存中间结果,称为间接转换法,下面分别讨论这两种方法。
1. 直接转换法
直接转换法通常用来消除尾递归和单向递归,将递归结构用循环结构来替代。
单向递归:简单的说是指递归的过程总是朝着一个方向进行,如果函数1调用了函数2,而函数2又调用了函数1,则这种情况不属于单向递归。斐波那契数列的递归求解可转用一个迭代法实现。
斐波那契数列的递归求解:
int Fib(int n) {
if(n <= 1) return n;
else return Fib(n - 1) + Fib(n - 2);
}
转化为迭代求解:
int Fib(int n) {
if(n <= 1) return n;
int twoBack = 0;
int oneBack = 1;
int cur;
for(int i = 2;i < = n; i++) {
cur = twoBack + oneBack;
twoBack = oneBack;
oneBack = cur;
}
return cur;
}
尾递归函数是以递归调用结尾的函数,是单向递归的特例。它的递归调用语句只有一个,而且是放在过程的最后。当递归调用返回时,返回到上一层递归调用语句的下一语句,而这个位置正好是程序的结尾,因此递归工作栈中可以不保存返回地址;除了返回值和引用值外,其他参数和局部变量都不再需要,因此可以不用栈,直接采用循环写出非递归过程。
阶乘函数就不是一个尾递归。因为在它收到递归调用的结果后,必须在返回调用前再做一次乘法运算。但是阶乘函数可以转化成一个尾递归函数,例:
阶乘的递归求解:
int factorial(int n)
{
if(n == 0) return 1;
else
{
int val = factorial(n - 1);
return n * val;
}
}
转化为尾递归求解:
int factorial(int acc, int x)
{ //acc传的值为1。
if(x <= 1) return acc;
else
return factorial(x * acc, x - 1);
}
尾递归的重要性在于当进行尾递归调用时,调用者的返回位置不需要被存在调用栈里。当递归调用返回时,它直接分支到先前已保存的返回地址。因此,在支持尾递归优化的编译器上,尾递归在时间和空间上都比较划算。迭代算法需要一个临时变量,这无疑导致了程序的可读性降低,迭代函数不像递归函数那样需要考虑函数调用的支出,而且对一个线程来说可用的栈空间通常比可用的堆空间要少得多,而递归算法则相对迭代算法需要更多的栈空间!
2. 间接转换法
该方法使用栈保存中间结果,一般需根据递归函数在执行过程中栈的变化得到。其一般过程如下:
将初始状态s0进栈
while (栈不为空)
{
退栈,将栈顶元素赋给s;
if (s是要找的结果) 返回;
else {
寻找到s的相关状态s1;
将s1进栈
}
}
间接转换法在数据结构中有较多实例,如二叉树遍历算法的非递归实现、图的深度优先遍历算法的非递归实现等等,请读者参考主教材中相关内容。
递推:构造低阶的规模(如规模为i,一般i=0)的问题,并求出解,推导出问题规模为i+1的问题以及解,依次推到规模为n的问题。(知道第一个,推出下一个,直到达到目的。,关键要找到递推公式)
递归:将问题规模为n的问题,降解成若干个规模为n-1的问题,依次降解,直到问题规模可求,求出低阶规模的解,代入高阶问题中,直至求出规模为n的问题的解。(要知道第一个,需要先知道下一个,直到一个已知的,再反回来,得到上一个,直到第一个。)
递归包括回溯和递推两个过程。
1.递归与非递归的区别:
递归的代码量比非递归的代码量少,因为非递归需要额外的变量记录当前所处的位置信息,以及额外的控制语句。而递归所使用的方式是函数调用,这是非常自然的栈结构,不需要记录位置信息,不需要添加控制语句,这些工作都由函数调用的特性解决了。
递归的执行效率比非递归的执行效率低,因为递归的实质是函数调用,而函数调用必然要进行线程栈空间的分配,记录每一次函数调用前的状态等工作,开销是比较大的。而非递归则不需要进行这些工作。
递归与非递归调用最主要区别就是在函数调用上。在计算机的工作方式中,函数调用是以栈结构来实现的,最早调用的函数处于栈底,最晚调用的函数处于栈顶,栈中存放的是每个函数中的局部变量等信息,当函数调用返回时该函数相关的信息就会从栈中弹出。
由此可以看出,递归每深入一层,栈的深度也会加一,而且当每一层的递归调用结束,都会自动返回上一层的递归中,因此不需要额外的变量记录当前所处的递归位置,也不需要while、if等控制语句进入或返回上一层或下一层递归。因此代码量比非递归的少。
但正因为函数调用要在栈中进行各种操作,例如分配新的空间,保存当前函数调用的信息,为新的函数调用初始化等,效率比较低下。所以递归的效率比非递归的低。
2.转化
递归是指某个函数或过程直接或间接的调用自身。一般地一个递归包括递归出口和递归体两部分,递归出口确定递归到何时结束,而递归体确定递归求解时的递推关系。递归算法有两个基本特征:一是递归算法是一种分而治之的、把复杂问题分解为简单问题的求解问题方法,对于求解某些复杂问题,递归算法分析问题的方法是有效地;而是递归算法的时间、控件效率通常比较差。因此对解决某些问题时,我们希望用递归算法分析问题,用非递归算法解决问题,这就需要把递归算法转换为非递归算法。
把递归算法转化为非递归算法有如下三种基本方法:
(1). 通过分析,跳过分解过程,直接用循环结构的算法实现求解过程。
(2). 自己用栈模拟系统的运行时栈,通过分析只保存必须保存的信息,从而用非递归算法替代递归算法。
(3). 利用栈保存参数,由于栈的后进先出特性吻合递归算法的执行过程,因而可以用非递归算法替代递归算法。
3.分析
递归深度小于3的,优化结果不明显。大于3的,优化后差距能达到70%以上。
目前QQ超市1店5口,以后2店3口之后的图,递归深度甚至达到了60层。
经过测试发现路径算出速度低了10多倍。非递归的转换势在必行!!
要回去从头学一下二叉树了……
http://blog.chinaunix.net/uid-21411227-id-1826854.html
一、什么是递归
递归是指某个函数直接或间接的调用自身。问题的求解过程就是划分成许多相同性质的子问题的求解,而小问题的求解过程可以很容易的求出,这些子问题的解就构成里原问题的解了。
二、递归的几个特点
1.递归式,就是如何将原问题划分成子问题。
2.递归出口,递归终止的条件,即最小子问题的求解,可以允许多个出口。
3.界函数,问题规模变化的函数,它保证递归的规模向出口条件靠拢
三、递归的运做机制
很明显,很多问题本身固有的性质就决定此类问题是递归定义,所以递归程序很直接算法程序结构清晰、思路明了。但是递归的执行过程却很让人费解,这也是让很多人难理解递归的原因之一。由于递归调用是对函数自身的调用,在一次调用没有结束之前又开始了另外一次调用,按照作用域的规定,函数在执行终止之前是不能收回所占用的空间,必须保存下来,这也就意味着每一次的调用都要把分配的相应空间保存起来。为了更好管理这些空间,系统内部设置一个栈,用于存放每次函数调用与返回所需的各种数据,其中主要包括函数的调用结束的返回地址,返回值,参数和局部变量等。
其过程大致如下:
1.计算当前函数的实参的值
2.分配空间,并将首地址压栈,保护现场
3.转到函数体,执行各语句,此前部分会重复发生(递归调用)
4.直到出口,从栈顶取出相应数据,包括,返回地址,返回值等等
5.收回空间,恢复现场,转到上一层的调用位置继续执行本次调用未完成的语句。
四、引入非递归
从用户使用角度来说,递归真的很简便,对程序宏观上容易理解。递归程序的时间复杂度虽然可以根据T(n)=T(n-1)*f(n)递归求出,其中f(n)是递归式的执行时间复杂度,一般来说,时间复杂度和对应的非递归差不多,但是递归的效率是相当低的它主要花费在反复的进栈出栈,各种中断等机制上(具体的可以参考操作系统)更有甚者,在递归求解过程中,某些解会重复的求好几次,这是不能容忍的,这些也是引入非递归机制的原因之一。
五、递归转非递归的两种方法
1.一般根据是否需要回朔可以把递归分成简单递归和复杂递归,简单递归一般就是根据递归式来找出递推公式(这也就引申出分治思想和动态规划)。
2.复杂递归一般就是模拟系统处理递归的机制,使用栈或队列等数据结构保存回朔点来求解。
六.如何用栈实现递归与非递归的转换:
1.递归与非递归转换的原理.
递归与非递归的转换基于以下的原理:
所有的递归程序都可以用树结构表示出来.
下面我们以二叉树来说明,不过大多数情况下二叉树已经够用,而且理解了二叉树的遍历,其它的树遍历方式就不难了.
1)前序遍历
a)递归方式:
void preorder_recursive(Bitree T) /* 先序遍历二叉树的递归算法 */
{
if (T) {
visit(T); /* 访问当前结点 */
preorder_recursive(T->;lchild); /* 访问左子树 */
preorder_recursive(T->;rchild); /* 访问右子树 */
}
}
b)非递归方式
void preorder_nonrecursive(Bitree T) /* 先序遍历二叉树的非递归算法 */
{
initstack(S);
push(S,T); /* 根指针进栈 */
while(!stackempty(S)) {
while(gettop(S,p)&&p) { /* 向左走到尽头 */
visit(p); /* 每向前走一步都访问当前结点 */
push(S,p->lchild);
}
pop(S,p);
if(!stackempty(S)) { /* 向右走一步 */
pop(S,p); /* 空指针退栈 *
push(S,p->rchild);
}
}
}
2)中序遍历
a)递归方式
void inorder_recursive(Bitree T) /* 中序遍历二叉树的递归算法 */
{
if (T) {
inorder_recursive(T->;lchild); /* 访问左子树 */
visit(T); /* 访问当前结点 */
inorder_recursive(T->;rchild); /* 访问右子树 */
}
}
b)非递归方式
void inorder_nonrecursive(Bitree T)
{
initstack(S); /* 初始化栈 */
push(S, T); /* 根指针入栈 */
while (!stackempty(S)) {
while (gettop(S, p) && p) /* 向左走到尽头 */
push(S, p->lchild);
pop(S, p); /* 空指针退栈 */
if (!stackempty(S)) {
pop(S, p);
visit(p); /* 访问当前结点 */
push(S, p->;rchild); /* 向右走一步 */
}
}
}
3)后序遍历
a)递归方式
void postorder_recursive(Bitree T) /* 中序遍历二叉树的递归算法 */
{
if (T) {
postorder_recursive(T->;lchild); /* 访问左子树 */
postorder_recursive(T->;rchild); /* 访问右子树 */
visit(T); /* 访问当前结点 */
}
}
b)非递归方式
typedef struct {
BTNode* ptr;
enum {0,1,2} mark;
} PMType; /* 有mark域的结点指针类型 */
void postorder_nonrecursive(BiTree T) /* 后续遍历二叉树的非递归算法/
{
PMType a;
initstack(S); /* S的元素为PMType类型 */
push (S,{T,0}); /* 根结点入栈 */
while(!stackempty(S)) {
pop(S,a);
switch(a.mark)
{
case 0:
push(S,{a.ptr,1}); /* 修改mark域 */
if(a.ptr->;lchild)
push(S,{a.ptr->;lchild,0}); /* 访问左子树 */
break;
case 1:
push(S,{a.ptr,2}); /* 修改mark域 */
if(a.ptr->;rchild)
push(S,{a.ptr->;rchild,0}); /* 访问右子树 */
break;
case 2:
visit(a.ptr); /* 访问结点 */
}
}
}
4)如何实现递归与非递归的转换
通常,一个函数在调用另一个函数之前,要作如下的事情:
a)将实在参数,返回地址等信息传递给被调用函数保存;
b)为被调用函数的局部变量分配存储区;
c)将控制转移到被调函数的入口.
从被调用函数返回调用函数之前,也要做三件事情
:a)保存被调函数的计算结果;
b)释放被调函数的数据区;
c)依照被调函数保存的返回地址将控制转移到调用函数.
所有的这些,不论是变量还是地址,本质上来说都是"数据",都是保存在系统所分配的栈中的.
递归调用时数据都是保存在栈中的,有多少个数据需要保存就要设置多少个栈,而且最重要的一点是:控制所有这些栈的栈顶指针都是相同的,否则无法实现 同步.
下面来解决第二个问题:在非递归中,程序如何知道到底要转移到哪个部分继续执行?
回到上面说的树的三种遍历方式,抽象出来只有三种操作:访问当前结点,访问左子树,访问右子树.这三种操作的顺序不同,遍历方式也不同.如果我们再抽象一点,对这三种操作再进行一个概括,可以得到:
a)访问当前结点:对目前的数据进行一些处理;
b)访问左子树:变换当前的数据以进行下一次处理;
c)访问右子树:再次变换当前的数据以进行下一次处理(与访问左子树所不同的方式).
下面以先序遍历来说明:
void preorder_recursive(Bitree T) /* 先序遍历二叉树的递归算法 */
{
if (T) {
visit(T); /* 访问当前结点 */
preorder_recursive(T->;lchild); /* 访问左子树 */
preorder_recursive(T->;rchild); /* 访问右子树 */
}
}
visit(T)这个操作就是对当前数据进行的处理, preorder_recursive(T->;lchild)就是把当前数据变换为它的左子树,访问右子树的操作可以同样理解了.
现在回到我们提出的第二个问题:如何确定转移到哪里继续执行?关键在于以下三个地方:
a) 确定对当前数据的访问顺序,简单一点说就是确定这个递归程序可以转换为哪种方式遍历的树结构;
b)确定这个递归函数转换为递归调用树时的分支是如何划分的,即确定什么是这个递归调用树的"左子树"和"右子树"
c)确定这个递归调用树何时返回,即确定什么结点是这个递归调用树的"叶子结点.
三.两个例子
好了上面的理论知识已经足够了,下面让我们看看几个例子,结合例子加深我们对问题的认识:
1)例子一:
f(n) = n + 1; (n <2)
f[n/2] + f[n/4](n >= 2);
这个例子相对简单一些,递归程序如下:
int f_recursive(int n)
{
int u1, u2, f;
if (n < 2)
f = n + 1;
else {
u1 = f_recursive((int)(n/2));
u2 = f_recursive((int)(n/4));
f = u1 * u2;
}
return f;
}
下面按照我们上面说的,确定好递归调用树的结构,这一步是最重要的.首先,什么是叶子结点 ,我们看到当n < 2时f = n + 1,这就是返回的语句,有人问为什么不是f = u1 * u2,这也是一个返回的语句呀?
答案是:这条语句是在u1 = exmp1((int)(n/2))和u2 = exmp1((int)(n/4))之后执行的,是这两条语句的父结点. 其次,什么是当前结点,由上面的分析,f = u1 * u2即是父结点.然后,顺理成章的u1 = exmp1((int)(n/2))和u2 = exmp1((int)(n/4))就分别是左子树和右子树了.最后,我们可以看到,这个递归函数可以表示成后序遍历的二叉调用树.好了,树的情况分析到这里,
下面来分析一下栈的情况,看看我们要把什么数据保存在栈中,在上面给出的后序遍历的如果这个过程你没非递归程序中我们已经看到了要加入一个标志域,因此在栈中要保存这个标志域;另外,u1,u2和每次调用递归函数时的n/2和n/4参数都要保存,这样就要分别有三个栈分别保存:标志域,返回量和参数,不过我们可以做一个优化,因为在向上一层返回的时候,参数已经没有用了,而返回量也
只有在向上返回时才用到,因此可以把这两个栈合为一个栈.
如果对于上面的分析你没有明白,建议你根据这个递归函数写出它的递归栈的变化情况以加深理解,再次重申一点:前期对树结构和栈的分析是最重要的,如果你的程序出错,那么请返回到这一步来再次分析,最好把递归调用树和栈的变化情况都画出来,并且结合一些简单的参数来人工分析你的算法到底出错在哪里.
2)例子二
快速排序算法
递归算法如下:
void swap(int array[], int low, int high)
{
int temp;
temp = array[low];
array[low] = array[high];
array[high] = temp;
}
int partition(int array[], int low, int high)
{
int p;
p = array[low];
while (low < high) {
while (low < high && array[high] >= p)
high--;
swap(array,low,high);
while (low < high && array[low] <= p)
low++;
swap(array,low,high);
}
return low;
}
void qsort_recursive(int array[], int low, int high)
{
int p;
if(low < high) {
p = partition(array, low, high);
qsort_recursive(array, low, p - 1);
qsort_recursive(array, p + 1, high);
}
}
需要说明一下快速排序的算法: partition函数根据数组中的某一个数把数组划分为两个部分, 左边的部分均不大于这个数,右边的数均不小于这个数,然后再对左右两边的数组再进行划分.这里我们专注于递归与非递归的转换,partition函数在非递归函数中同样的可以调用(其实partition函数就是对当前结点的访问).
再次进行递归调用树和栈的分析:
递归调用树:
a)对当前结点的访问是调用partition函数;
b)左子树: qsort_recursive(array, low, p - 1);
c)右子树:qsort_recursive(array, p + 1, high);
d)叶子结点:当low < high时;
e)可以看出这是一个先序调用的二叉树
栈:要保存的数据是两个表示范围的坐标.
void qsort_nonrecursive(int array[], int low, int high)
{
int m[50], n[50], cp, p;
/* 初始化栈和栈顶指针 */
cp = 0;
m[0] = low;
n[0] = high;
while (m[cp] < n[cp]) {
while (m[cp] < n[cp]) { /* 向左走到尽头 */
p = partition(array, m[cp], n[cp]); /* 对当前结点的访问 */
cp++;
m[cp] = m[cp - 1];
n[cp] = p - 1;
}
/* 向右走一步 */
m[cp + 1] = n[cp] + 2;
n[cp + 1] = n[cp - 1];
cp++;
}
}

浙公网安备 33010602011771号