递归是一架精巧的机器,在递归解决问题的时候,往往有很多微妙的东西,让我感觉一个好的递归是需要精心设计的。不把子树、出口这些东西搞清楚就好像在宏观和微观里找不到方向似的让人头晕。
递归有哪些要注意的地方呢,叶子节点的出口、剪枝的出口,测试子树的出口,递归的模式,上下层的数据传输,节点本身的数据修改和恢复,递归树分析,递归子树的设计,节点本身的行为,都还是要好好研究的。
怎么建递归子树呢,子树的建立决定了整个树的拓扑结构是怎样的,比如求斐波那契数列,很明显递归树就是一个链条,没有分支,因为在每个节点里只调用了一次递归。二叉树遍历的时候调用了两次递归,这些拓扑就很清楚。
<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 ok void 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的二叉递归树。
菜鸟脑袋冒烟了,不写了