递归与非递归及其相互转换

一、什么是递归

递归是指某个函数直接或间接的调用自身。问题的求解过程就是划分成许多相同性质的子问题的求解,而小问题的求解过程可以很容易的求出,这些子问题的解就构成里原问题的解了。

二、递归的几个特点

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++;

}

}

时间: 2024-10-21 10:13:43

递归与非递归及其相互转换的相关文章

数据结构——二叉树遍历之“递归与非递归遍历”

简述 二叉树的遍历分为先序遍历.中序遍历和后序遍历.如下图所示: 递归遍历 private void bianli1(List<Integer> list, TreeNode root) { // 先序遍历 if (root == null) { return; } list.add(root.val); bianli1(list, root.left); bianli1(list, root.right); } private void bianli2(List<Integer>

JAVA递归、非递归遍历二叉树(转)

原文链接: JAVA递归.非递归遍历二叉树 import java.util.Stack; import java.util.HashMap; public class BinTree { private char date; private BinTree lchild; private BinTree rchild; public BinTree(char c) { date = c; } // 先序遍历递归 public static void preOrder(BinTree t) {

Java数据结构系列之——树(4):二叉树的中序遍历的递归与非递归实现

package tree.binarytree; import java.util.Stack; /** * 二叉树的中序遍历:递归与非递归实现 * * @author wl * */ public class BiTreeInOrder { // 中序遍历的递归实现 public static void biTreeInOrderByRecursion(BiTreeNode root) { if (root == null) { return; } biTreeInOrderByRecursi

二叉树遍历递归与非递归实现

说明:本文仅供学习交流,转载请标明出处,欢迎转载! 二叉树遍历是二叉树中非常基础的部分,也是学习二叉树必须熟练掌握的部分,下面我们先给出二叉树三种遍历方式的定义,并通过举例来说明二叉树遍历的过程. 二叉树的遍历分为:前序遍历(也叫先序遍历).中序遍历.后序遍历.所谓前.中.后都是根据当前子树根结点相对左右孩子的位置而言,也就是说: 前序遍历:根结点在前,即:根 ----->左------->右: 中序遍历:根结点在中间,即:左------>根------>右: 后序遍历:根结点在最

树的递归与非递归遍历总结

树的递归遍历遍历很简单,非递归遍历要复杂一些,非递归先序.中序.后序遍历需要用一个辅助栈,而层次遍历则需要一个辅助队列. 树的结构: 1 public class Tree<T> { 2 private T data; 3 private Tree<T> left; 4 private Tree<T> right; 5 ... 6 } 用策略模式定义一个访问工具: 1 public interface Visitor<T> { 2 void process(

全排列(递归与非递归实现)

全排列问题在公司笔试的时候很常见,这里介绍其递归与非递归实现. 递归算法 1.算法简述 简单地说:就是第一个数分别以后面的数进行交换 E.g:E = (a , b , c),则 prem(E)= a.perm(b,c)+ b.perm(a,c)+ c.perm(a,b) 然后a.perm(b,c)= ab.perm(c)+ ac.perm(b)= abc + acb.依次递归进行. void swap(string &pszStr,int k,int m) { if(k==m) return ;

二叉树三种遍历(递归以及非递归实现)

package com.shiyeqiang.tree; import java.util.Stack; public class BiTree { public static void main(String[] args) { // 首先构造叶子节点 BiTree leafA1 = new BiTree(4); BiTree leafA2 = new BiTree(5); BiTree leafB1 = new BiTree(6); BiTree leafB2 = new BiTree(7)

二叉树遍历算法总结(递归与非递归)

一:前言 二叉树的遍历方法分四种:前序,中序,后序以及层次遍历. 其中,前中后遍历方法的实现分递归和非递归,非递归遍历的实现需要借助于栈. 实际上,递归的调用就是一种栈的实现,所以,非递归遍历就需要人工借助栈结构来实现. 而层次遍历需要借助队列. 二:前中后序遍历 递归遍历: 递归遍历的思想和方法很简单,通过调整输出语句来实现前,中,后三种遍历. 代码如下: 1 void show(BiTree T) 2 { 3 if(T) 4 { 5 printf("%c ",T->data)

链表反转的递归和非递归实现方式

链表反转是数据结构的基本功,主要有递归和非递归两种实现方式.我们一一介绍如下: 1. 非递归实现 主要包括如下4步: 1)如果head为空,或者只有head这一个节点,return head即可: 2)从头到尾遍历链表,把reversedHead赋值给当前节点的next: 3)当前节点赋值给reversedHead: 4)遍历结束,return reversedHead. 下图试图来辅助说明: 代码如下: node* reverseList(node* head) { if(head == NU