深入理解递归函数

1.递归函数概念介绍以及个人对递归的理解

先给出一个概念:

递归函数:我们把一个直接调用自己或通过一系列的调用语句间接地调用自己的函数,称做递归函数。

一直都对递归有种敬畏之情,因为之前在一家公司里是不让写递归的,我记得当时的理由是递归代码比较晦涩,所以自己也就一直没有对递归深入的了解,但是随着时间的推移,我感觉有的时候,用递归写的代码是优雅的,递归应该算作是一种算法,或者编程技巧。当然递归也有自己的缺点,它会有函数间的不断调用,函数调用栈是比较消耗时间和内存的,同时如果递归结束条件写的不对,也会造成内存被爆,程序崩溃的可能,但是凡事都是相对的,你要想用递归的优雅,简洁的代码结构,也就必然要承担她的缺点,凡事都是两面的。但是至于说递归晦涩难懂,现在看来这个感觉说法是站不住脚的,如果,你真正理解递归的过程,我感觉应该会让你恍然大悟,有一种妙哉妙哉的感觉,因为递归能使程序的结构更加的清晰,更简洁,更容易让人理解,从而减少读代码的时间。

所谓递归,就是递 和 归,递就是前行的阶段,归就是退回的阶段。那么问题来了,前行的时候做了什么,什么时候回来,退回的时候又做了什么?那我们带着问题继续阅读吧!

2.递归与栈的关系

简单的说,在递归的前行阶段,对于每一层递归,函数的局部变量,参数值,以及返回地址都被压入栈中,在退回阶段,对于栈顶顶局部变量,参数值,和返回地址被弹出,用于返回调用层次中执行代码的其余部分,也就是恢复了调用的状态。

3.通过一个例子观察递归,通过阶乘观察递归过程

这个例子主要参考了我在文章末尾附的链接。

每个递归函数都有终止条件和递归条件两个部分:终止条件,表示停止调用自己,避免无限循环;递归条件是指继续调自己。

接下来结合调用栈来理解,先看下面一段代码,表示计算阶乘的递归函数:

int fact(x){
    // 如果 x 等于 1,直接返回 1 的阶乘,就是 1 
    if x == 1{
        return 1;
    }	
    // 如果 x 不等于 1,开始递归求解
	else{
        return x * fact(x-1);
    }
}

理解了代码之后,我们假如求 fact (3),看看计算机到底是怎么执行上面一段函数的。
当我们第一次调用fact函数时,传入 x = 3 :

 当程序不满足 if 条件是,进入else,开始递归,第二次调用fact,传入 x = 2 :

 第二次调用结束之后,开始第三次调用,传入 x = 1,满足 if x == 1 的条件,继续执行 return 1,这时我们可以知道 fact(1)这个函数的返回值是 1 ,即fact(1)= 1。

以上的x == 1就是递归的结束条件,else 之后就是递归条件。当满足基准条件之后,笑脸之后表示开始所谓的“归”的过程。
从上图可以看出,fact(1)= 1,fact(2)= 2,fact(3)= 6,这样就实现了 3!= 3 * 2 * 1。

好了,到此位置,我们的第一个简单的递归例子就详细描述完了,这个例子是比较容易接受的例子,后面我会详细的讲述,什么情况下使用递归,以及再来几个,斐波那些数列,以及二叉树的例子来理解递归这个问题。

4.什么情况下使用递归?

递归需要满足的三个条件:

a.一个规模大问题的解可以分解成几个规模较小问题的解

b.规模大问题和规模小问题的求解思路完全一样

c.存在递归终止条件

以上三个条件既涉及到分而治之的思想,又涉及到递归。我们再通过几个例子来加深对三个条件的理解。

4.1 斐波那契数列

说如果兔子在出生两个月后,就有繁殖能力,一对兔子每个月能生出一对小兔子来,假设所有兔都不死,那么一年以后可以繁殖多少对兔子呢?

4.2 判断一个二叉树是否是对称

题干如下:

请实现一个函数,用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样,那么它是对称的。

例如,二叉树 [1,2,2,3,4,4,3] 是对称的。

    1
   /   \
 2     2
 / \    / \
3  4 4  3
但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:

    1
   /  \
  2   2
   \     \
    3     3

现在我们来通过我们之前得到的结论来分析这个问题是否可以使用递归:

a.一个规模大问题的解可以分解成几个规模较小问题的解

b.规模大问题和规模小问题的求解思路完全一样

c.存在递归终止条件

首先,这个符合条件a,也就是,这个规模比较大的问题,可以差分成下面规模小的问题。
对称二叉树定义: 对于树中 任意两个对称节点 L和 R,一定有:
L.val = R.val:即此两对称节点值相等。
L.left.val = R.right.val.  :即 L 的 左子节点 和 R 的 右子节点 对称;
L.right.val = R.left.val.  :即 L 的 右子节点 和 R 的 左子节点 对称。
根据以上规律,考虑从顶至底递归,判断每对节点是否对称,从而判断树是否为对称二叉树。

然后我们再看条件b

规模大的问题和规模小的问题求解思路完全相同,但是有一个特例,就是对于根结点的处理,因为根只有一个值,不能有两个参数,所以这个要特殊处理下。

这个也是很关键的一步,在对于这个递归问题的时候,很多时候,一些特例要提取出来,不能放在递归中。

c.存在递归结束条件

当 L 和 R 同时越过叶节点: 此树从顶至底的节点都对称,因此返回 true ;
当 L 或 R 中只有一个越过叶节点: 此树不对称,因此返回 false ;
当节点 L 值 != 节点 R 值: 此树不对称,因此返回 false;

算法流程:

1.特例处理: 若根节点 root 为空,则直接返回 true 。

2.终止条件:
当 L 和 R 同时越过叶节点: 此树从顶至底的节点都对称,因此返回 true ;
当 L 或 R 中只有一个越过叶节点: 此树不对称,因此返回 false ;
当节点 L 值 != 节点 R 值: 此树不对称,因此返回 false ;
3.递推工作:
判断两节点 L.leftL.left 和 R.rightR.right 是否对称,即 recur(L.left, R.right) ;
判断两节点 L.rightL.right 和 R.leftR.left 是否对称,即 recur(L.right, R.left) ;
返回值: 两对节点都对称时,才是对称树,因此用与逻辑符 && 连接。

具体实现代码如下:

class Solution {
public:
    bool isSymmetric(TreeNode* root) {
        // 如果是空树
        if(!root)
            return true;
        else
            return isSymmetric(root->left, root->right);
    }
    // 此函数比较二叉树中位置对称的两个节点
    bool isSymmetric(TreeNode* left, TreeNode* right){
        // 结束条件1:如果对称两个节点都为空,则返回true
        if(!left && !right){
            return true;
        }
        // 结束条件2:如果单独一个节点为空,另一个节点不为空,又或者是对称节点间的val值不等,则返回false
        if(!left || !right ) {
            return false;
        }
            
        if(left->val != right->val){
            return false;
        }
        // 该层符合对称二叉树的要求,开始比较下一层
        return isSymmetric(left->left, right->right) && isSymmetric(left->right, right->left);      
    }
};

4.3  求二叉树的路径和

给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false 。

叶子节点 是指没有子节点的节点。

对于这个问题,我感觉难点是怎么把大问题分解成小的问题。

思路及算法

观察要求我们完成的函数,我们可以归纳出它的功能:询问是否存在从当前节点 root 到叶子节点的路径,满足其路径和为 sum。

假定从根节点到当前节点的值之和为 val,我们可以将这个大问题转化为一个小问题:是否存在从当前节点的子节点到叶子的路径,满足其路径和为 sum - val。

不难发现这满足递归的性质,若当前节点就是叶子节点,那么我们直接判断 sum 是否等于 val 即可(因为路径和已经确定,就是当前节点的值,我们只需要判断该路径和是否满足条件)。若当前节点不是叶子节点,我们只需要递归地询问它的子节点是否能满足条件即可。

代码如下:

class Solution {
public:
    bool hasPathSum(TreeNode *root, int sum) {
        if (root == nullptr) {
            return false;
        }
        if (root->left == nullptr && root->right == nullptr) {
            return sum == root->val;
        }
        return hasPathSum(root->left, sum - root->val) ||
               hasPathSum(root->right, sum - root->val);
    }
};

参考链接:

递归是什么,看完这篇文章保证你能理解递归!_笙南的博客-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值