1. 递归的定义

编程语言中,函数 Func(Type a,……) 直接或间接调用函数本身,则该函数称为「递归函数」。

在实现递归函数之前,有两件重要的事情需要弄清楚:

  • 递推关系:一个问题的结果与其子问题的结果之间的关系。
  • 基本情况:不需要进一步的递归调用就可以直接计算答案的情况。可理解为递归跳出条件

一旦我们计算出以上两个元素,再想要实现一个递归函数,就只需要根据递推关系调用函数本身,直到其抵达基本情况。

递归函数的编写看起来比较难,其实是有套路可寻的,本文在力扣刷题阶段总结了写递归的一些范式技巧并在后续实战中进行验证,深入理解其中思维过程再去刷题时感觉轻而易举了。

1.1 递推关系

下面的插图给出了一个 5 行的帕斯卡三角,根据上面的定义,我们生成一个具有确定行数的帕斯卡三角形。

首先,我们定义一个函数 f(i,j),它将会返回帕斯卡三角形第 i 行、第 j列的数字。可以看到,每行的最左边和最右边的数字是基本情况,它总是等于 11。
每个数是它左上方和右上方的数的和。

  • 递推关系:f(i,j)=f(i−1,j−1)+f(i−1,j)
  • 基本情况:f(i,j)=1f(i,j)=1 ,当 j=1j=1 或者 i=ji=j 时。
    再看一个「二叉树的最大深度」递推关系推导的案例:

二话不多,先定义一个 f(node),寻找当前节点 node 与当前节点子节点的关系,子节点可能是左、可能是右。

所以有子节点有 f(node.left)、f(node.right) 两种情况,然后寻找 f(node)与它们的关系。当前节点的最大深度 = 他子节点的最大深度 + 1。

  • 递推关系:f(node)=Max(f(node.left),f(node.right))+1

  • 基本情况:当前节点不存在时,高度为 0

对于二叉树的算法题,我们会推导递推关系时,所有相关的算法一下就变得很容易。但是有了递推关系后如何写出递归函数来呢?
介于最近对算法的研究,发现大部分所谓的动态规划、回溯其实都是写递归函数的一些思维过程。本文总结了写递归的范式。有了这些范式,我们直接拿着题目套入就很容易写出一个通过率很高的函数。

1.2 尾递归

尾递归:尾递归函数是递归函数的一种,其中递归调用是递归函数中的最后一条指令。并且在函数中应该只有一次递归调用

尾递归的好处是,它可以避免递归调用期间栈空间开销的累积,因为系统可以为每个递归调用重用栈中的固定空间。形象的理解参考 2.2.3 节内容中关于自顶向下的示例图。

2. 写递归函数的招式

下面我们以累加的示例说明写递归的思路。

1+2+3+4+…+n,函数表达式为 f(n)=f(n−1)+n

2.1 寻找基本情况

累加示例中,基本情况为 n=1 时,f(1)=1。

你也可以设定为 f(2)=1+2=3,只要能正确跳出递归即可。

2.2 寻找递推关系(难点)

累加示例中,递推关系为 f(n)=f(n−1)+n,f(n) 每次计算时依赖 f(n−1) 的结果,所以我们把 f(n−1)的结果看作是中间变量。

中间变量其实就是联系递归函数的纽带,分析出中间变量递归函数也就实现了 80%。

大白话:当前结果必须借助前一个结果,前一个借助前前一个… 一直到时我们找到了「基本情况」。
然后拿到「基本情况」开始往回计算。这个过程我们简称为「自底向上」。

下面我们用 f(5)=1+2+3+4+5=15 这个过程进行分析。

2.2.1 自底向上

自底向上:在每个递归层次上,我们首先递归地调用自身,然后根据返回值进行计算。(依赖返回值)

大白话:将问题细化分解,例如计算 1-n 的和,可以逐步分解为 f(n) = f(n-1) + n

/** * 模拟程序执行过程:* 5 + sum(4)* 5 + (4 + sum(3)* 5 + 4 + (3 + sum(2))* 5 + 4 + 3 + (2 + sum(1))* ------------------> 到达基本情况 sum(1) = 1 ,开始执行 ③ 行代码* 5 + 4 + 3 + (2 + 1)* 5 + 4 + (3 + 3)* 5 + (4 + 6)* (5 + 10)* 15* <p>* 自底向上:最终从 1 + 2 + 3 + 4 + 5 计算...* 递归函数「开始」部分调用自身,这个过程就是找到基本情况),然后根据返回值进行计算。*/
public int sum(int n) {if (n < 2) return n;       // ① 递归基本情况int childSum = sum(n - 1); // ② 寻找基本情况return n + childSum;       // ③ 根据返回值运算
}

自底向上的过程其实是一个先寻找基本情况(跳出条件),然后根据基本情况计算它的父问题,一直到最后一个父问题计算后,返回最终结果。

本示例中基本情况是 sum(1) = 1,基本情况的父问题是 sum(2) = 2 + sum(1)。即从 N 加到 1,到达 1 时触发结果开始逐个返回子问题的结果。

自底向上-范式

  • 寻找递归递推关系
  • 寻找递归基本情况,跳出时返回基本情况的结果
  • 修改递归函数的参数
  • 递归调用并返回中间变量
  • 使用递归函数的返回值与当前参数进行计算,并返回最终结果
public 返回值 f(参数) {if (基本情况条件) return 基本情况的结果;       修改参数;返回值 = f(参数); 最终结果 = 根据参数与返回值计算return 最终结果;
}

2.2.2 自顶向下

假如我们换个思路,f(n)=f(n−1)+n 中我们把 f(n−1) 的结果(中间变量)提取出来 f(n,SUM)=SUM+n,
每次计算都带着它,这样我们可以先计算,然后把计算好的结果传递给递归函数进行下一次计算,这个过程我们称为「自顶向下」。

自顶向下:在递归层级中,我们根据当前「函数参数」计算出一些值,并在递归调用函数时将这些值传给自身。(依赖函数参数)

大白话:从最子问题逐步计算出最终问题,例如计算 1-n 的和,可以逐步分解为 n + sum(n-1) = sum(n),即从 1 加到 N。

/*** 模拟程序执行过程:* sum(5, 0)* sum(4, 5)* sum(3, 9)* sum(2, 12)* sum(1, 14)* 15* <p>* 自顶向下:最终从 5 + 4 + 3 + 2 + 1 计算...* 递归函数「末尾」部分调用自身,根据逻辑先进行计算,然后把计算的中间变量传递调用函数。* <p>* 这种在函数末尾调用自身的递归函数叫做「尾递归」*/
public int sum2(int n, int sum) {if (n < 2) return 1 + sum;sum += n;return sum2(n - 1, sum);
}

自顶向下-范式

  • 寻找递归递推关系
  • 创建新函数,将「自底向上-范式」中的最终结果计算依赖的中间变量提取为函数的参数
  • 寻找递归基本情况,跳出时返回基本情况的结果与中间变量的计算结果(最终结果)
  • 根据函数参数与中间变量重新计算出新的中间变量
  • 修改参数
  • 递归调用并返回(该处的返回由基本情况触发)
public 返回值 f(参数,中间变量) {if (基本情况条件) return 基本情况的结果与中间变量的计算结果;中间变量 = 根据参数与中间变量重新计算修改参数;return f(参数,中间变量);
}

2.2.3 自底向上、自顶向下的区别

两者最大的区别在于对中间变量的处理,参与计算的中间变量是参数提供的还是返回值提供的,这个过程最终决定了基本情况的返回值处理逻辑、递归函数的执行位置。

递归函数在计算前先找到基本情况再算还是先算再找基本情况,这个过程也就是「自底向上、自顶向下」的本质差异。

2.3 优化递归函数

优化点总结为:

  • 充分分析基本情况(跳出条件),避免临界值跳不出递归,导致栈溢出。

  • 分析递归深度,太深的递归容易导致栈溢出。

  • 分析是否有重复计算问题,主要分析函数参数值是否会出现重复,直接代入递归的递推关系中运算即可。如果会出现重复使用数据结构记录(记忆化消除重复)。

比如:斐波那契数列 f(n)=f(n−1)+f(n−2),如果直接采用该公式进行递归会重复计算很多表达式。

  • 分析数据溢出问题

  • 因为递归会对栈及中间变量的状态保存有额外的开销,将「自底向上」优化为「自顶向下」,再改写为尾递归,再退化为循环结构。

尾递归是我们可以实现的递归的一种特殊形式。与记忆化技术不同的是,尾递归通过消除递归带来的堆栈开销,优化了算法的空间复杂度。更重要的是,有了尾递归,就可以避免经常伴随一般递归而来的堆栈溢出问题,而尾递归的另一个优点是,与非尾递归相比,尾部递归更容易阅读和理解。这是由于尾递归不存在调用后依赖(即递归调用是函数中的最后一个动作),这一点不同于非尾递归,因此,只要有可能,就应该尽量运用尾递归。

2.4 改为循环

递归本身的风险比较高,实际项目不推荐采用。部分编程语言可以对尾递归进行编译优化(优化为循环结构),比如 Scala 语言。但是部分语言不支持,比如 Java。

函数式编程时推荐尾递归写法并加标识让编译器进行优化,下面是 Scala 语言优化的一个案例:

// Scala 编译前的尾递归写法,并注解为尾递归
@scala.annotation.tailrec
def sum2(n: Int, sum: Int): Int = {if (n < 2) return sum + nsum2(n - 1, sum + n)
}// 编译后优化为循环结果
public int sum2(int n, int sum) {while (true) {if (n < 2) return sum + n; sum += n;n--;}
}

一个不是尾递归的案例:

// 并不是最后一行递归调用就是尾递归,下面例子其实是一个自底向上的递归写法,返回值与 n 有关。
def sum(n: Int): Int = {if (n < 2) return nreturn n + sum(n - 1)
}

3. 案例实战-递归乘法

对于有些算法,递归比循环实现简单,比如二叉树的前中后序遍历。但是大部分时候循环比递归更直观更容易理解。

下面我们以力扣一个算法题 递归乘法 进行实战,实战前请花 10 min 时间尝试自我完成。

如果只是局限于看小说似的阅读,现在就可以「ALT+F4」了。

[递归]一文看懂递归相关推荐

  1. angular 字符串转换成数字_一文看懂Python列表、元组和字符串操作

    好文推荐,转自CSDN,原作星辰StarDust,感觉写的比自己清晰-大江狗荐语. 序列 序列是具有索引和切片能力的集合. 列表.元组和字符串具有通过索引访问某个具体的值,或通过切片返回一段切片的能力 ...

  2. 一文看懂深度学习——人工智能系列学习笔记

    深度学习有很好的表现,引领了第三次人工智能的浪潮.目前大部分表现优异的应用都用到了深度学习,大红大紫的 AlphaGo 就使用到了深度学习. 本文将详细的给大家介绍深度学习的基本概念.优缺点和主流的几 ...

  3. 一文看懂25个神经网络模型

    引言 在深度学习十分火热的今天,不时会涌现出各种新型的人工神经网络,想要实时了解这些新型神经网络的架构还真是不容易.光是知道各式各样的神经网络模型缩写(如:DCIGN.BiLSTM.DCGAN--还有 ...

  4. 一文看懂NLP神经网络发展历史中最重要的8个里程碑!

    一文看懂NLP神经网络发展历史中最重要的8个里程碑! https://mp.weixin.qq.com/s/gNZiiEfsQjlF9tArNDIt5Q 作者|Sebastian Ruder 译者|小 ...

  5. 一文看懂人工智能里的算法(4个特征+3个算法选择 Tips)

    一文看懂人工智能里的算法(4个特征+3个算法选择 Tips) 人工智能有三驾马车:数据.算法.算力.本文重点介绍算法相关的知识. 本文将介绍算法在人工智能里的概念,算法的4个特征.6个通用方法.以及在 ...

  6. 一文看懂 AI 训练集、验证集、测试集(附:分割方法+交叉验证)

    2019-12-20 20:01:00 数据在人工智能技术里是非常重要的!本篇文章将详细给大家介绍3种数据集:训练集.验证集.测试集. 同时还会介绍如何更合理的讲数据划分为3种数据集.最后给大家介绍一 ...

  7. 一文看懂计算机视觉-CV(基本原理+2大挑战+8大任务+4个应用)

    2020-03-06 20:00:00 计算机视觉(Computer Vision)是人工智能领域的一个重要分支.它的目的是:看懂图片里的内容. 本文将介绍计算机视觉的基本概念.实现原理.8 个任务和 ...

  8. 一文看懂人脸识别(4个特点+4个实现步骤+5个难点+算法发展轨迹)

    2020-03-09 20:01:00 人脸识别是身份识别的一种方式,目的就是要判断图片和视频中人脸的身份时什么. 本文将详细介绍人脸识别的4个特点.4个步骤.5个难点及算法的发展轨迹. 什么是人脸识 ...

  9. 一文看懂卷积神经网络-CNN(基本原理+独特价值+实际应用)

    http://blog.itpub.net/29829936/viewspace-2648775/ 2019-06-25 21:31:18 卷积神经网络 – CNN 最擅长的就是图片的处理.它受到人类 ...

最新文章

  1. Java 学习笔记 ------第二章 从JDK到IDE
  2. 在报文摘要算法MD5中,首先要进行明文分组与填充,其中分组时明文报文摘要按照(42)位分组。【答案】C
  3. tensorflow计算图_简单谈谈Tensorflow的运行机制
  4. 参加了 Go 贡献者大会
  5. python基础知识纵览(下)
  6. devops_面向内向的人的DevOps
  7. 检验密码强度的JS类(from thin's blog)
  8. 月薪一万在石家庄能过什么样的生活?
  9. 百度对数据的要求很高,智能音箱的难点是远场语音识别
  10. Security+ 学习笔记29 虚拟化
  11. GPUImage详细解析- 实时美颜滤镜
  12. 数据结构——中国邮递员问题
  13. html设置文字在背景图上,css如何实现文字在背景图片之上 css实现文字在背景图片之上代码...
  14. 【按键精灵学习记录】以DOTA2人机为例
  15. Template for Publisher and Subscriber
  16. 内存池简单实现(一)
  17. KDD2022推荐系统论文集锦
  18. STM32(六)——串口通信原理
  19. JAVA面试(关于技术深耕方向和职业规划)
  20. java离职证明上的职位写的是什么_2020年java离职证明范本

热门文章

  1. 总结css中单位px和em,rem的区别
  2. scrollview 与 listView 的显示不全问题
  3. 360发布穿戴设备“儿童卫士”手环
  4. MySql命令——命令行客户机的分隔符
  5. 计算机的双一流学校,分数不够上双一流大学计算机专业,上这些大学也不错,实力非常强...
  6. linux 2行数据为一条记录 该如何操作这一条记录_Linux 日志文件系统原来是这样工作的...
  7. linux:vi 替换命令
  8. 华强北三代悦虎1562A怎么样?
  9. android.os.BinderProxy cannot be cast to
  10. android Dialog提示框。单选项dialog,多选项dialog