从斐波拉数列想到递归实现和非递归实现
这篇文章记录了我的这些天的思考过程。大致过程是这样的:
1.学习二叉树的前序、中序、后序遍历的递归实现。
2.学习把递归函数改变成非递归函数。
3.通过学习,我觉得我掌握了递归思想和把递归函数改变成非递归函数的方法后, 我找斐波那契数列练手。
4.斐波拉数列的2种非递归实现。
5.由斐波拉数列的一种非递归实现联想到动态规划中的自底向上的求解。
1.学习二叉树的前序、中序、后序遍历的递归实现。
定义:
1) 递归函数我之见:我的理解比较简单, 函数的存在是为了实现一个功能,递归函数也不例外,只不过它会自己调用自己。
2)二叉树的递归定义:二叉树(BinaryTree)是n(n≥0)个结点的有限集,它或者是空集(n=0),或者由一个根结点及两棵互不相交的、分别称作这个根的左子树和右子树的二叉树组成。(从二叉树的递归定义可以看出,左子树和右子树也是一颗二叉树)。
3)先序遍历的做法:首先访问根结点然后先序遍历左子树,最后先序遍历右子树。
实现:
根据上面的定义,很容易地写出先序遍历函数的递归实现:
按照我的思路是这样的:
1.定义pre_order(node * r)函数,它的功能是对一棵二叉树进行先序遍历。
2.根据线序遍历的做法, pre_order函数先访问根节点,然后“先序遍历”(pre_order)左子树,最后“先序遍历”(pre_order)右子树。
所以pre_order函数的实现为:
//struct of node typedef struct _node { int val; struct _node* lc; struct _node* rc; }node; //功能:对二叉树进行先序遍历 void pre_order(node *r) { if (!r) return;//遇到空二叉树返回 visit(r); //访问根节点, pre_order(r->lc); //先序遍历左子树 pre_order(r->rc); //先序遍历右子树 } //顺便把中序和后序遍历的实现也写出来,好对比着看,加深了解啦~ //功能:对二叉树进行中序遍历 void mid_order(node *r) { if (!r) return;//遇到空二叉树返回 mid_order(r->lc); //中序遍历左子树 visit(r); //访问根节点, mid_order(r->rc); //中序遍历右子树 } //功能:对二叉树进行后序遍历 void post_order(node *r) { if (!r) return;//遇到空二叉树返回 pre_order(r->lc); //后序遍历左子树 pre_order(r->rc); //后序遍历右子树 visit(r); //访问根节点 }
其实大家思考怎么写递归函数的时候,不要老用大脑去想象递归函数的压栈出栈等各种复杂情况。单纯写递归函数的话,那样去想反而让你越想越乱。但是要将递归函数转为非递归函数的时候,却真的是需要考虑递归函数的入栈和出栈问题,那是后话,我们等下说。
2.学习把递归函数改成非递归函数。
我用”回溯法“,实际上就是”模拟堆栈的压栈和出栈“的方式,把递归函数改为非递归函数。
压栈和出栈我之见:每次压栈和给“重要变量”赋值的操作,就相当于是”递归调用函数(保存现场,赋予形参值); 每次出栈就相当于递归函数return了(恢复现场)。
为了证明我的观点,我先贴出前序和中序递归遍历的非递归实现代码。
后续遍历需要增加辅助变量,相对复杂点, 就我这水平说不清楚。有兴趣的朋友请看这里:http://www.cnblogs.com/ybwang/archive/2011/10/04/lastOrderTraverse.html
//前序遍历 void pre_order(node * r) {
stack s; node * tmp_r = r; //可以想象是递归实现中的形参r while (!s.emtpy() || p) //函数停止条件 { while (tmp_r) //栈帧退出条件 { pre_visit(tmp_r); s.push(tmp_r); //入栈,保存现场,创建了新的栈帧 tmp_r = tmp_r.lc; //改变形参,这两句话合起来的作用相当于调用"递归函数"的pre_order(tmp_r.lc) }
if (!s.empty()) { tmp_r = s.pop(); //出栈,并恢复现场。相当于从pre_order(tmp_r)返回, //mid_visit(tmp_r); //如果是中序列遍历的实现,访问函数放在这个地方 tmp_r = tmp_r.rc; //改变形参准备进入下一轮循环,相当于调用"递归函数"的pre_order(tmp_r.rc) } } }
1.其实我更倾向于把“入栈”看成是复制一个一模一样的栈帧到栈定,而后的修改“重要变量”看成事给此栈帧的形参赋值。
这与调用递归函数的效果是一样的,因为递归函数的栈帧的空间结构应该是一样的,只是变量值不同。
2.因为随着pre_order(tmp_r.rc)返回, pre_order(tmp_r)也做完所有工作,所以也返回了,所以在tmp_r = tmp_r.rc之前就把tmp_r从堆栈s中退栈了。
其实我觉得可以做这样的修改,会对进出栈的表现更直观些:
//前序遍历 void pre_order(node * r) { stack s; node * tmp_r = r; //可以想象是递归实现中的形参r while (!s.emtpy() || p) //函数停止条件 { while (tmp_r) //栈帧退出条件 { pre_visit(tmp_r); s.push(tmp_r); //入栈,保存现场,创建了新的栈帧 tmp_r = tmp_r.lc;//改变形参,这两句话合起来的作用相当于调用"递归函数"的pre_order(tmp_r.lc) } /* if (!s.empty()) { tmp_r = s.pop(); //出栈,相当于从pre_order(tmp_r)返回,并恢复现场 //mid_visit(tmp_r); //如果是中序列遍历的实现,访问函数放在这个地方 tmp_r = tmp_r.rc;//改变形参准备进入下一轮循环,相当于调用"递归函数"的pre_order(tmp_r.rc)
}
}
*/ if (!s.emtpy()) { char * tmp_c = tmp_r; //记录当前栈帧的形参,用以判断当前栈帧是不是上一个栈帧的pre_order(tmp_r.rc)调用。 tmp_r = s.pop(); //退栈当前栈帧,恢复上一个栈帧的现场 if (tmp_c == tmp_r.rc) //判断当前栈帧是上一个栈帧的pre_order(tmp_r.rc)调用。 { do
{
tmp_c = tmp_r; //保存退出的栈帧的形参。 tmp_r = s.pop(); //栈帧退出,恢复再上一级的栈帧
//若退出的栈帧是上一个栈帧的pre_order(tmp_r.rc)调用,继续出栈,
//直到找栈空或者退出的栈帧为上一栈帧的pre_order(tmp_r.lc)调用
}while(!s.empty() && (tmp_c == tmp_r.rc));
if (tmp_rc == tmp_r.rc)
{
tmp_r = null; //退无可退了, 结束程序吧
}
else
{
s.push(tmp_r); //保存现场
tmp_r = tmp_r.rc; //进入pre_order(tmp_r.rc)
}
} else { s.push(tmp_r); //保存现场 tmp_r = tmp_r.rc; //进入pre_order(tmp_r.rc) }
} //end if(!s.emtpy())
}//end while(!s.emtpy() || p)
}
3.通过学习,我觉得我掌握了递归思想和把递归函数改变成非递归函数的方法后, 我找斐波那契数列练手。
斐波拉数数列为 1 1 2 3 5 ···· $(n-2) $(n-1) $(n-2)+$(n-1)
所以功能为求斐波那契数列第n个数的递归函数应该这么写:
//求斐波那契数列第n个数 int fib(int n) { if ( n <= 0) return 0; if (n < = 2) return 1; //return fib(n-2) + fib(n-1) 我把这句拆成下面那样 int sum = 0; sum += fib(n-1); sum += fib(n-2); return sum; }
4.斐波那契数列的2种非递归实现。
1.复杂递归实现(回溯法)
//斐波那契数列 回溯法实现 int fib(int n) { stack s; int s = 0; while( ( !s.empty() ) || ( n > 2 ) ) //停止条件 { while( n > 2 ) { s.push(n); //fib(n-1) --n; } if ( n > 0 ) ++sum; if( !empty() ) { n = s.pop(); n -= 2; //fib(n-2) } } if ( n > 0 ) ++sum; return sum; }
我们看n=5的时候的压栈出栈过程
| n值 | stack | val |
| 2 | 5 4 3 | +1 |
| 3-2=1 | 5 4 | +1 |
| 4-2=2 | 5 | +1 |
| 5-2=3 | 3 | |
| 3-1 | 3 | +1 |
| 1 | out while +1 | |
| 5 | ||
2.简单递归实现。
就是没用回溯法的递归实现,我开始没想到可以这样实现。哎,我比较迟钝,没办法····
//fib 非回溯法的非递归实现 int fib( int n ) { int num_1 = 1; int num_2 = 1; ret = 1; for ( int i=3; i<=n; i++ ) { ret = num_1 + num_2; num_1 = num_2; num_2 = ret; } return ret; }
其实后来我比较了这2中非递归的实现,发现他们在运行的时候实际上是做了这样不同的计算:
*回溯法的非递归实现它的实际计算其实是 从上到下的计算方式(我以n=5来举例):
f5 = f4 + f3 = (f3 + f2) + (f2 + f1) = ( (f2 + f1) + f2) + (f2 + f1)
*而非回溯法的非递归实现实际上市从底到上的计算方式(同以n=5)来举例:
f3 = f1 + f2
f4 = f3 + f2
f5 = f4 + f3
从中我们发现,回溯法中的计算明显多很多,复杂很多。其实回溯法的中,有大量的工作都是在重复计算的同样的子问题,而非回溯法采用自底向上的方法,避免了重复计算子问题。所以效率快乐很多。
5.由斐波拉数列的一种非递归实现联想到动态规划中的自底向上的求解
我在这比较的过程中,特别是想到“重复计算相同的子问题”这句话的时候,我脑海里浮现出了一个词“动态规划”。因为我之前看动态规划的时候没怎么看懂,但是“存在大量重复的子问题”这句话还是有点印象的。于是我翻书找出了动态规划求解方法的相关描述:
动态规划基本步骤:
(1)找出最优解的性质,并刻划其结构特征。
(2)递归地定义最优值。
(3)以自底向上的方式计算出最优值。
(4)根据计算最优值时得到的信息,构造最优解。
斐波拉契数列并不存在最优解问题,但是它的非递归实现运用了自底向上的求解方法。也算是个简单的自底向上求解的例子吧。

浙公网安备 33010602011771号