递归的难点在于构造而不在于求解,一个良好的递归算法的实现,其实是需要很多技巧的。
首先,递归的组成部分包含递归边界与递归式。前者是边界条件,任何递归如果想有结果,就必须要有边界,后者是递推关系,可以理解为状态转移函数。
让我们先看一段代码来体验递归的快感:
//求最大公因数
int gcd(int x,int y){
if(y==0)return x;
return gcd(y,x%y);
}
递归难就难在递归模型的建立上,这让我想起来当年高数老师和我们说“微分方程不难求解,难在建模”。这里我打算用汉诺塔来具体说明一下。
汉诺塔的任务是将A柱子上的盘子借助B全部放到C上,要保持盘子原来的次序,且一次只能移动一个盘子。为了直观地理解这个过程,我们可以选小规模的数据自行地模拟一下。这里就不进行说明了。
首先,什么问题可以用递归?递归的定义是调用自身的、能在有限步内计算的函数。从定义出发,要用递归解决的问题需要具有两个性质——1.问题的规模可以改变,不同规模的问题的求解的步骤有重复性;2.规模为1的问题有解。我们要做的就是剖析问题是否能够分为阶段性的重复步骤求解。
在递归过程中,需要有一个向前的状态变换和一个向后的结果传输。如斐波那契数列计算,n即为状态,f(n)就是结果。
将N作为问题分解的标志。求解A柱上有N个盘子与求解A柱上N-1个盘子的联系是:每次完成N盘子的摆放都要把N-1个盘子移到B(辅助柱)上,把N盘子放到C(目标盘)上,然后把B上N-1个盘子都放到C(目标盘上)。抽象化表示如下:
F(A,B,C,n){
F(A,C,B,n-1);
move(A,C);
F(B,A,C,n-1);
}
进一步抽象,将ABC三个柱子抽象成三种功能的柱子,分别是源柱子、辅助柱子、目标柱子。要把源柱子上的N个盘子都移到目标柱子需要先将N-1个盘子移到辅助柱子上,然后把第N个盘子移到目标柱子上,然后把辅助柱子上剩余的盘子都移到目标柱子上。我上述的描述中有两处是涉及到子问题的求解,但是我都没有深入考虑,递归N问题规模的解决就建立在N-1问题求解的基础上的。如果我考虑如何将N-1个盘子移到辅助柱子上,那就没必要用递归了。
最后确定问题规模等于1的时候,只需要移到一次即可。就是move一次就可以了。至于从哪挪到哪需要参考f(A,B,C,1)中参数了。
所以完整的程序如下:
void Hannota(A,B,C,n){
if(n==1){
move(A,C);
}
Hannota(A,C,B,n-1);
move(A,C);
Hannota(B,A,C,n-1);
}
递归实战:反转一个单链表。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* recur(ListNode* head,ListNode* p){
if(p==NULL)return head;
ListNode* head1=new ListNode(p->val);
head1->next=head;
return recur(head1,p->next);
}
ListNode* reverseList(ListNode* head) {
ListNode* p=head;
if(p==NULL)return NULL;
while(p->next!=NULL){
p=p->next;
}
return recur(NULL,head);
}
};
递归的计算过程,就是一个栈的计算过程,从最底层开始一层一层地加、开销资源,计算的时候从最顶层开始弹栈、释放堆资源。在实际编程的时候尤其要注意递归的深度,如果过于深了,往往会形成很大的空间开销。
对于递归堆栈空间空间优化,这里给出一个技巧:使用尾递归。尾递归就是在递归函数的最后一个语句执行的时候进行递归调用。下列给出斐波那契数列递归的例子来说明何为尾递归:
//普通递归,调用:fabonacci(n)
int fabonacci(int n){
if(n==0||n==1)
return 1;
return fabonacci(n-1)+fabonacci(n-2);
}
//尾递归,调用:fabonacci(n,1,1)
int fabonacci(int n,int ans1,int ans2){
if(n==1)return ans2;
return fabonacci(n-1,ans2,ans1+ans2);
}
尾递归中由于上一层递归与下一层递归没有任何关系了,所以在递归过程中,一边申请堆栈一边释放堆栈。尾递归到最后一层递归直接输出结果,而普通递归到最后一层还要一层一层返回去。问题规模为n,尾递归的空间是O(1),普通递归的空间是O(n).除此之外,时间上尾递归也比普通递归快。尾递归这么好用,牺牲的代价是代码阅读性以及传参便捷性。