递归的小结

来源:互联网 发布:中国人工智能股票龙头 编辑:程序博客网 时间:2024/05/19 10:39

递归是一架精巧的机器,在递归解决问题的时候,往往有很多微妙的东西,让我感觉一个好的递归是需要精心设计的。不把子树、出口这些东西搞清楚就好像在宏观和微观里找不到方向似的让人头晕。

递归有哪些要注意的地方呢,叶子节点的出口、剪枝的出口,测试子树的出口,递归的模式,上下层的数据传输,节点本身的数据修改和恢复,递归树分析,递归子树的设计,节点本身的行为,都还是要好好研究的。

怎么建递归子树呢,子树的建立决定了整个树的拓扑结构是怎样的,比如求斐波那契数列,很明显递归树就是一个链条,没有分支,因为在每个节点里只调用了一次递归。二叉树遍历的时候调用了两次递归,这些拓扑就很清楚。

<span style="font-family:SimHei;font-size:12px;">void f(int n){    if(n<=1)return;    --n;    for(int i=0;i<n;++i)        f(i);}</span>
这样的代码就能造成一个右边重左边轻的结构,总之递归的次数就是子树的个数。
另一个微妙的东西是递归过程中数据的传递,传递的方式是否正确是能决定结果的。

比如一个输出顺序数列全排列的函数:

#include <iostream>#include<iterator>#include<vector>#include<algorithm>using namespace std;//[0,pos] is already ok,in the beginning 0 position is not okvoid permutation(vector<int>st,int pos){    if(pos==st.size()-2){            copy(st.begin(),st.end(),ostream_iterator<int>(cout," "));            cout<<"   ";            copy(st.rbegin(),st.rend(),ostream_iterator<int>(cout," "));            cout<<endl;}    else        for(int i=pos+1;i<st.size();++i){                swap(st[pos+1],st[i]);                permutation(vector<int>(st),pos+1);}}int main(){    int test[5]={1,2,3,4,5};    permutation(vector<int>(test,test+5),-1);    return 0;}
为什么只要递归就能得到正确的排列呢,这要看递归树才能看出来:


一开始数据就是顺序的,每一次都是把未处理的(逗号后)的数据从前往后跟新的位置交换,就导致了这样一个令人惊讶的结果。这里上下层的数据传递方式很重要。

再看递归的出口,一般都是if(出口条件)return;这样是普通的叶子型出口,叶子出口是必备的出口,没有的话可能就停不下来。一般叶子出口的作用就是屏蔽本节点的递归,所以

if(出口条件)return;

else 继续递归;

这样最常用。在节点本身行为里也常用剪枝的出口,比如

void f(int n){    if(n<=1)return;    if(find something)return;    f(n-1);}
这样剪枝就半路返回。

还有一种测试子树的出口,比如在“24点游戏”里就用到了,在编程之美里大概这样写:

bool pointgame(n){    if(n==1){...}    for(...)    {        ...        ...        覆盖pointgame使用的数据为状态1;        if(pointgame(n-1))            return true;        覆盖pointgame使用的数据为状态2;        if(pointgame(n-1))            return true;        覆盖pointgame使用的数据为状态3;        if(pointgame(n-1))            return true;        覆盖pointgame使用的数据为状态4;        if(pointgame(n-1))            return true;        ...        ...        恢复数据;    }    return false;}
这里的测试返回不是真的出口,相反每一个测试子树出口都把本条子树走过所有叶子,所有状态(比如这里四个状态)都测过了,实在不行才返回默认的false,这里一个节点有四个子树(四个递归),意思就是,第一条子树可以就返回true,否则第二条字数可以也能返回true,每一条子树都不行才返回false。如果说剪枝的出口可以半路返回,测试子树的的出口就保证了递归到底不能半路返回。

再说分析递归树,每个递归都能以某种方式画出一棵树,我认为这是绝对的,画出树可以指导设计递归或验证已有递归的正确性,比如上边的全排序递归,不画出来很难弄清楚。再比如一个汉诺塔的递归:

#include<cstdio>using namespace std;void f(char s1,char s2,char s3,int n){    if(n<=1)            printf("%c -> %c\n",s1,s3);    else{            f(s1,s3,s2,n-1);            printf("%c -> %c\n",s1,s3);            f(s2,s1,s3,n-1);}}int main(){    f('a','b','c',8);    return 0;}
它有两个子树,每个子树的规模都只减小了1,就能画出不断递减1的二叉递归树。
再比如“24点游戏”的递归树,顶点有6*6=36个子树,顶点就是长度为4的数组,下一层每个有3*6=18个子树,顶点是一对长度为3的数组,越往下越接近结果,到达每个叶子节点都会原路返回顶点。而且整个递归下来数组恢复了原状,因为非叶节点在递归函数在返回前恢复了原状,那非叶节点每一层都恢复了数据,一层这样,每一层就是这样。

菜鸟脑袋冒烟了,不写了

0 0