基础算法——递归

算法思想

递归(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)+jumpFloor(n);
    }
};

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

递归的缺点

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

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

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

总结

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

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值