【算法】递归设计以及优化技巧

递归的难点在于构造而不在于求解,一个良好的递归算法的实现,其实是需要很多技巧的。

首先,递归的组成部分包含递归边界与递归式。前者是边界条件,任何递归如果想有结果,就必须要有边界,后者是递推关系,可以理解为状态转移函数。

让我们先看一段代码来体验递归的快感:

//求最大公因数
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).除此之外,时间上尾递归也比普通递归快。尾递归这么好用,牺牲的代价是代码阅读性以及传参便捷性。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值