基础算法——递归

算法思想

递归(recursion algorithm):在计算机科学中是指一种通过重复将问题分解为同类的子问题而解决问题的方法。也可以说递归就是在函数定义中再调用自身的做法。好了,虽然其算法思维很简单,但是要使用起来却是很困难。有时候一个递归的算法光是理解起来就很难。(大师 L.Peter Deutsch 说过:To Iterate is Human, to Recurse, Divine.中文译为:人理解迭代,神理解递归。毋庸置疑地,递归确实是一个奇妙的思维方式。)

递归三要素

在这里本菜鸟只说一下对递归的理解~( ̄▽ ̄)~。

1、明确函数功能

我们写一个递归函数,首先要明确这个函数的功能是什么。拿一道题举例子:比如要求二叉树的深度。好的,我们写一个函数框架:

/*
struct TreeNode {
	int val;
	struct TreeNode *left;
	struct TreeNode *right;
	TreeNode(int x) :
			val(x), left(NULL), right(NULL) {
	}
};*/
class Solution {
public:
  	//求二叉树深度
    int TreeDepth(TreeNode* pRoot)
    {
		return ...;
    }
};

函数功能:求二叉树深度。在脑子就想着TreeDepth()——>返回二叉树深度。

2、明确递归出口

递归其实就是loop,如果没有出口,递归将无限循环下去,很快就会栈溢出(可以自己测试)。还是上面的例子,那么出口是什么呢?很显然,空树的时候我们要返回0,而且二叉树的深度是max(左子树深度,右子树深度)+1。因此,把出口代码也写上。

class Solution {
public:
  	//求二叉树深度
    int TreeDepth(TreeNode* pRoot)
    {	if (pRoot==nullptr) return 0;
     	...
		return max(左子树深度, 右子树深度) + 1;;
    }
};

3、找出重复逻辑

这也是最核心的一步。意思就是说我们在哪里使用递归。还是上面的例子,如何求二叉树深度咱们的思路已经很清晰了,就是空树返回0,否则返回左右子树深度较大的+1。在这个过程中我们会发现左右子树的深度我们还没求呢。那么左右子树深度如何求呢?假设有一个函数就是求二叉树的深度的,你是不是很容易想到调用这个函数来求左子树深度。很显然,这个函数就是我们第一步提到的那个函数。因此左子树深度=TreeDepth(左子树);右子树深度=TreeDepth(右子树)。好的,现在补全上述例子的代码:

class Solution {
public:
    int TreeDepth(TreeNode* pRoot)
    {
        if (pRoot==nullptr) return 0;
        int lDepth = TreeDepth(pRoot->left);
        int rDepth = TreeDepth(pRoot->right);
        return max(lDepth, rDepth) + 1;
    
    }
};

看吧,递归就是如此的巧妙,你可能有个疑问,这怎么就能求出二叉树的深度了?你可以在草稿纸上画一个二叉树对这上述代码比划。这有助于你更好的深入理解递归。

实例(一)——求阶乘

alt
这个题是初学递归的经典例题。 好的,现在使用递归三要素来分析一下这个题:

  1. 明确函数功能: fact(n)——>求n的阶乘
  2. 明确递归出口: n=0,返回1。否则返回n乘n-1的阶乘
  3. 找出重复逻辑: 现在只有n-1的阶乘不知道,显然n-1的阶乘=fact(n-1)。

这道题之所以作为初学者的例题,是因为最难的一步———找出重复逻辑,题中已经明确给出了。

实例(二)——跳台阶

alt 这道题也可以用递归的方式去解决,首先我们先写一个函数框架:

class Solution {
public:
    int jumpFloor(int n) {
		return ...;
    }
};

好的,接下来我们找一下这道题的三要素,从而补全我们的代码:

  1. 明确函数功能: jumpFloor(n)————>返回跳上n级台阶的跳法。
  2. 明确递归出口: 当n==1的时候,只有一种跳法,我们返回。
  3. 找出重复逻辑: 这步也是核心的一步。首先我们要知道递归的含义就是把一个问题分解为同类的子问题。那么这道题的子问题是什么呢?有的人可能会把n分解为n-1,直接返回jumpFloor(n-1)+1。但其实跳上n-1级台阶并不能代表跳上n级台阶的前一类状态。(比如青蛙两级两级跳,这时候根本没有n-1这种情况)。那么跳上n级台阶的前一种状态到底是什么呢?我们可以反过来想。从n级台阶往下跳,我们能跳到哪儿呢?显然,可以跳到n-1和n-2的位置。所以跳上n级台阶的前一类子问题就是跳上n-1级台阶和n-2级台阶。因此代码为:
class Solution {
public:
    int jumpFloor(int n) {
        if(n<=1) return 1;
        else return jumpFloor(n - 1 )+jumpFloor(n - 2);
    }
};

有的人可能会有疑惑,跳上n-1级台阶和跳上n-2级台阶的跳法直接加起来,不会有重复的吗?其实就算是重复的,只要最后一次跳的不一样(如果在n-1直接跳1步,如果在n-2直接跳两步),就算两种不同的跳法。

递归的缺点

使用递归写出的代码虽然十分优雅,但是由于函数调用的开销。递归表现出的性能往往很差。这个时候如果我们想使用非递归的方式代替递归,可以采取如下操作:

  1. 自定义“栈”(变量)来代替系统栈保存一些内容。
  2. 使用循环代替递归处理重复逻辑。

经典的题就是二叉树的三种非递归遍历实现。

总结

递归算法的思维十分简单,但是想用好必须多练题,在练习中感受递归的巧妙。

数据结构和算法 文章被收录于专栏

该专栏内容包含常见的数据结构和一些算法知识。若有错误请各位指正。 //里面所有代码均通过vscode调试。

全部评论

相关推荐

2025-12-12 19:01
南京航空航天大学 C++
秋招没咋投,准备&nbsp;wxg&nbsp;转正之后摆烂了。结果不堪字节&nbsp;HR&nbsp;的骚扰还是面了一下字节。之前想去字节的时候怎么面都挂。现在想着随便面一下结果三面技术面都意外顺利还有加面。十月中旬字节发了意向,wxg&nbsp;转正结果无响应。十月底字节拉了保温群,wxg&nbsp;口头通过,系统显示考核中。十一月初和字节&nbsp;ld&nbsp;交流之后得知&nbsp;base&nbsp;居然能选海外,甚至能小&nbsp;wlb&nbsp;一下,wxg&nbsp;无响应无人联系。十一月中旬把字节&nbsp;base&nbsp;转到了海外,wxg&nbsp;流程灰了,一问超时忘处理了,过两天又变考核中了。十一月下旬字节换了海外&nbsp;HR&nbsp;对接,问了期望薪资,wxg&nbsp;考核终于显示通过,无&nbsp;HR&nbsp;保温,无其他保温。十一月底给字节报了个天价,想吓吓他们,同时告诉微信字节要开了,微信无响应。同样十一月底字节&nbsp;HR&nbsp;告诉我确实给不到那么高,但是能拿期权补上,问能不能接受。微信无响应。同样十一月底字节&nbsp;HR&nbsp;告知了具体方案,符合预期。&nbsp;微信无响应。十二月上旬催&nbsp;wxg&nbsp;不开我就盲拒了,wxg&nbsp;HR&nbsp;火急火燎的打电话问情况,问期望。我给了一个不算夸张的总包数字,因为今年市场在涨,过了三天还不联系我,我再催,约时间下午打电话,非得在我给出的数字上压下去几万,微信又不差这点,为什么不能满足我,让我没有拒绝的理由呢?一番纠结抗争,求稳还是追求挑战,最终选择接受迎接新的挑战,因为堂吉诃德永远不会停下脚步!回想起来,在&nbsp;wxg&nbsp;谈薪的阶段,我认为并没有给予我一定的重视,即使&nbsp;HR&nbsp;表示我在实习期间的表现和之前的面评都很靠前。也没有感觉到想要争取我,虽然我表示拒了&nbsp;offer&nbsp;之后要给我加面委定&nbsp;t6&nbsp;再涨,但我三个月没面试让我面面委那就是白给,还是算了。有缘再见了我亲爱的&nbsp;wxg,再见了曾经的梦中情厂,再见亲爱的&nbsp;mt,再见亲爱的朋友们。也再见,北京的一切。我想润了。秋招结束,卸载牛客,下一个三年,下一个五年,下一个十年后再来看看。
面试中的大熊猫爱吃薯...:我嫉妒得狗眼通红
点赞 评论 收藏
分享
点赞 评论 收藏
分享
评论
1
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务