​本期我们一起看几个典型的递归的例子。

递归就是每次调用的时候方法是自己,但是参数变了,就是下面这个样子:

1.斐波那契数列

斐波那契数列的是这样一个数列:1、1、2、3、5、8、13、21、34….,即第一项 f(1) = 1,第二项 f(2) = 1…..,第 n 项目为 f(n) = f(n-1) + f(n-2)。求第 n 项的值是多少。

我们来看看递归该怎么写。

1、确定递归函数功能

假设 f(n) 的功能是求第 n 项的值,代码如下:

int f(int n){}

2、找出递归结束的条件

显然,当 n = 1 或者 n = 2 ,我们可以轻易着知道结果 f(1) = f(2) = 1。所以递归结束条件可以为 n <= 2。代码如下:

int f(int n){    if(n <= 2){        return 1;    }}

3、找出函数的等价关系式

题目已经把等价关系式给我们了,所以我们很容易就能够知道 f(n) = f(n-1) + f(n-2)。我说过,等价关系式是最难找的一个,而这个题目却把关系式给我们了,这也太容易,好吧,我这是为了兼顾几乎零基础的读者。

所以最终代码如下:

int f(int n){    // 1.先写递归结束条件    if(n <= 2){        return 1;    }    // 2.接着写等价关系式    return f(n-1) + f(n - 2);}

搞定,是不是很简单?

2.青蛙跳台阶

一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。

1、确定递归函数功能

假设 f(n) 的功能是求青蛙跳上一个n级的台阶总共有多少种跳法,代码如下:

int f(int n){}

2、找出递归结束的条件

我说了,求递归结束的条件,你直接把 n 压缩到很小很小就行了,因为 n 越小,我们就越容易直观着算出 f(n) 的多少,所以当 n = 1时,你知道 f(1) 为多少吧?够直观吧?即 f(1) = 1。代码如下:

int f(int n){    if(n == 1){        return 1;    }}

3.找出函数的等价关系式

每次跳的时候,小青蛙可以跳一个台阶,也可以跳两个台阶,也就是说,每次跳的时候,小青蛙有两种跳法。

第一种跳法:第一次我跳了一个台阶,那么还剩下n-1个台阶还没跳,剩下的n-1个台阶的跳法有f(n-1)种。

第二种跳法:第一次跳了两个台阶,那么还剩下n-2个台阶还没,剩下的n-2个台阶的跳法有f(n-2)种。

所以,小青蛙的全部跳法就是这两种跳法之和了,即 f(n) = f(n-1) + f(n-2)。至此,等价关系式就求出来了。于是写出代码:

int f(int n){    if(n == 1){        return 1;    }    ruturn f(n-1) + f(n-2);}

代码看上去没问题对不对?

其实是有问题的,当 n = 2 时,显然会有 f(2) = f(1) + f(0)。我们知道,f(0) = 0,按道理是递归结束,不用继续往下调用的,但我们上面的代码逻辑中,会继续调用 f(0) = f(-1) + f(-2)。这会导致无限调用,进入死循环。

关于递归结束条件是否够严谨问题是递归算法的重要一环,有很多人在使用递归的时候,由于结束条件不够严谨,导致出现死循环。也就是说,当我们在第二步找出了一个递归结束条件的时候,可以把结束条件写进代码,然后进行第三步,但是请注意,当我们第三步找出等价函数之后,还得再返回去第二步,根据第三步函数的调用关系,会不会出现一些漏掉的结束条件。就像上面,f(n-2)这个函数的调用,有可能出现 f(0) 的情况,导致死循环,所以我们把它补上。代码如下:

int f(int n){    //f(0) = 0,f(1) = 1,等价于 n<=2时,f(n) = n。    if(n <= 2){        return n;    }    ruturn f(n-1) + f(n-2);}

那结束条件该怎么确定呢?其实最简单的方式就是写几个试一试,n足够小的时候也不过为 0 1 2 3 这几个种情况,或者是执行结束或者开始的时候几个元素,带进去试一试看看对不对就行了。

3.反转链表

反转单链表。例如链表为:1->2->3->4。反转后为 4->3->2->1。我们前面已经介绍过,这里再从递归的角度分析一下。

链表的节点定义如下:

class Node{    int date;    Node next;}

还是老套路,三要素一步一步来。

1、定义递归函数功能

假设函数 reverseList(head) 的功能是反转但链表,其中 head 表示链表的头节点。代码如下:

Node reverseList(Node head){}

2. 寻找结束条件

当链表只有一个节点,或者如果是空表的话,你应该知道结果吧?直接啥也不用干,直接把 head 返回呗。代码如下:

Node reverseList(Node head){    if(head == null || head.next == null){        return head;    }}

3. 寻找等价关系

这个的等价关系不像 n 是个数值那样,比较容易寻找。但是我告诉你,它的等价条件中,一定是范围不断在缩小,对于链表来说,就是链表的节点个数不断在变小,所以,如果你实在找不出,你就先对 reverseList(head.next) 递归走一遍,看看结果是咋样的。例如链表节点如下:

我们就缩小范围,先对 2->3->4递归下试试,即代码如下:

Node reverseList(Node head){    if(head == null || head.next == null){        return head;    }    // 我们先把递归的结果保存起来,先不返回,因为我们还不清楚这样递归是对还是错。,    Node newList = reverseList(head.next);}

我们在第一步的时候,就已经定义了 reverseLis t函数的功能可以把一个单链表反转,所以,我们对 2->3->4反转之后的结果应该是这样:

我们把 2->3->4 递归成 4->3->2。不过,1 这个节点我们并没有去碰它,所以 1 的 next 节点仍然是连接这 2。

接下来呢?该怎么办?

其实,接下来就简单了,我们接下来只需要把节点 2 的 next 指向 1,然后把 1 的 next 指向 null,不就行了?,即通过改变 newList 链表之后的结果如下:

也就是说,reverseList(head) 等价于 reverseList(head.next) + 改变一下1,2两个节点的指向。好了,等价关系找出来了,代码如下(有详细的解释):

//用递归的方法反转链表public static Node reverseList2(Node head){    // 1.递归结束条件    if (head == null || head.next == null) {             return head;         }         // 递归反转 子链表         Node newList = reverseList2(head.next);         // 改变 1,2节点的指向。         // 通过 head.next获取节点2         Node t1  = head.next;         // 让 2 的 next 指向 1         t1.next = head;         // 1 的 next 指向 null.        head.next = null;        // 把调整之后的链表返回。        return newList;    }

上面我们介绍了几个典型的递归题目,那是不是有优化空间呢?我们下期接着看。

4.汉诺塔问题

汉诺塔问题是最经典的递归问题了,如果这个问题理解了,递归基本就理解清楚了。题目是这样:

在经典汉诺塔问题中,有 3 根柱子及 N 个不同大小的穿孔圆盘,盘子可以滑入任意一根柱子。一开始,所有盘子自上而下按升序依次套在第一根柱子上(即每一个盘子只能放在更大的盘子上面)。移动圆盘时受到以下限制:

(1) 每次只能移动一个盘子;

(2) 盘子只能从柱子顶端滑出移到下一根柱子;

(3) 盘子只能叠在比它大的盘子上。

请编写程序,用栈将所有盘子从第一根柱子移到最后一根柱子。

将原来的 A, B, C 重命名为 origin, buffer, destination

这个题目的递归解答简洁地让人惊讶:

class Solution {    public void hanota(List<Integer> origin, List<Integer> buffer, List<Integer> destination) {        // 使用递归函数来完成,需要计数        move(origin.size(), origin, destination, buffer);    }    private void move(int n, List<Integer> origin, List<Integer> destination, List<Integer> buffer) {        // 如果碰到了一个栈的底,那么说明这个位置上已经没有盘子移动了        if (n <= 0) return;        // 从 origin 移动到 buffer 上        move(n - 1, origin, buffer, destination);        // 从 origin 移动到 destination 上        destination.add(origin.get(origin.size() - 1));        origin.remove(origin.size() - 1);        // 从 buffer 移动到 destination 上        move(n - 1, buffer, destination, origin);    }}

这个题目画画图比较好理解,现在我们先不画了,读者可以在网上找找看,文字描述过程是这样的:

首先用盘子数 1 开始模拟: 直接就可以从 origin 移动到 destination.

盘子数为 2 时: 先从 origin 移动 1 到 buffer(origin -> buffer) , 然后再把 2 从 origin 移动到 destination(origin -> destination), 再从 buffer 移动 1 到 destination(buffer -> destination).

从上一步可以知道,我们可以将上俩盘子从 origin 移动到 buffer (origin -> buffer), 下一步只需要把 3 移动到 destination (origin -> destination), 再移动剩余的那俩就好了(buffer -> destination).

从上一步可以知道,我们可以将上面仨盘子从 origin 移动到 buffer...

以此类推, 总是可以移动完的.是一种递归结构.每一步需要完成的参考()括号中的注释。

递归、迭代和分治(2):递归的典型例子相关推荐

  1. 递归——迭代是人,递归是神

    递归,就是自己调用自己. 首先,需要搞清楚函数是如何调用的.在执行被调函数之前,系统需要做3件事: 1.将实参,函数的返回地址等信息传递给被调函数保存. 2.为被掉函数的形参,局部变量分配空间 3.将 ...

  2. Java 二叉树基础概念(递归迭代)

    目录 1. 树型结构 1.1概念 1.2 概念(重要) 2. 二叉树(重点) 2.1 概念 2.2 二叉树的基本形态 2.3 两种特殊的二叉树 2.4 二叉树的性质 a.满二叉树 b.完全二叉树 2. ...

  3. 递归、迭代和分治(1):递归

    ​1.从现实中理解三个概念 可能很多人就已经接触了递归了,不过我敢保证很多人初学者刚开始接触递归的时候,是一脸懵逼的,我当初也是! 可能也有一大部分人知道递归,也能看的懂递归,但在实际做题过程中,却不 ...

  4. 递归、迭代、分治、回溯、动态规划、贪心算法

    今天就简单来谈谈这几者之间的关联和区别 递归 一句话,我认为递归的本质就是将原问题拆分成具有相同性质的子问题. 递归的特点: 1.子问题拆分方程式,比如:f(n) = f(n-1) * n 2.终止条 ...

  5. 迭代是人,递归是神(迭代与递归的总结:比较)

    https://www.cnblogs.com/Renyi-Fan/p/7708012.html 在计算机编程实现中有常常两种方法:一曰迭代(iterate):二曰递归(recursion). 从&q ...

  6. 【Java数据结构与算法】第十七章 二分查找(非递归)和分治算法(汉诺塔)

    第十七章 二分查找(非递归)和分治算法(汉诺塔) 文章目录 第十七章 二分查找(非递归)和分治算法(汉诺塔) 一.二分查找 1.思路 2.代码实现 二.分治算法(汉诺塔) 1.概述 2.汉诺塔 一.二 ...

  7. 算法设计与分析(第三周)递归/迭代求Fibonacci前n项 【以及递归算法速度慢的原因】

    为了理解递归写的.真想求Fibonacci前n项,迭代是更好的选择,简单并且速度快.另外,注意一下溢出问题. 递归算法速度慢的原因 递归调用本身需要使用系统栈,每次分配函数内存以及栈都需要时间.不过这 ...

  8. leetcode144. 二叉树的前序遍历(递归+迭代)

    一:题目 二:上码 1:递归 class Solution {public:void preorder(TreeNode* root,vector<int>&v ) {if(roo ...

  9. [Leedcode][JAVA][第94/144/145题][前中后序遍历][递归][迭代][二叉树]

    [问题描述][] 前序遍历 先输出当前结点的数据,再依次遍历输出左结点和右结点 中序遍历 先遍历输出左结点,再输出当前结点的数据,再遍历输出右结点 后续遍历 先遍历输出左结点,再遍历输出右结点,最后输 ...

  10. [递归|迭代] leetcode 21 合并两个有序链表

    [递归|迭代] leetcode 21 合并两个有序链表 1.题目 题目链接 将两个升序链表合并为一个新的升序链表并返回.新链表是通过拼接给定的两个链表的所有节点组成的. 示例: 输入:1->2 ...

最新文章

  1. 以太网控制芯片DM9000在2440裸机上终于能正确接收数据了(源代码工程已经上传)...
  2. php程序的安全要素
  3. vn.py 2.0.2 发布,全功能交易程序开发框架
  4. C 判断 —— if...else 语句(bool变量、float变量、指针变量与“零值”进行比较)(else 到底与哪个 if 配对呢? if 语句后面的分号?)
  5. 通过企业分布式缓存共享运行时数据
  6. python多进程打印输出_python 多进程日志 logging
  7. python数据类型特点_Python 基础数据类型
  8. 03 php,PHP 03 选择结构
  9. 程序员都用什么来记录知识_1年前的小五都用 Python 来做什么?
  10. MSP430F5529 DriverLib 库函数学习笔记(九)SPI
  11. 真的已经讲烂了!java字符串转对象
  12. 解决vue2.0路由 TypeError: Cannot read property ‘matched‘ of undefined 的错误问题
  13. Java - 常用函数Random函数
  14. 蓝桥杯13年--18年Java组B组省赛题目以及题解汇总
  15. sha1算法源码c版
  16. Error splicing file: No space left on device
  17. 最新 EDK2 实验
  18. 【linux基础1】linux命令行使用技巧
  19. 阿里大数据面试题集合:Hadoop+HBase+Spark+Zookeeper
  20. 解析区块链游戏与GameFi的发展历程

热门文章

  1. 【项目】Vue3+TS 动态路由 面包屑 查询重置 列表
  2. 解决Coursera的课程视频无法观看问题
  3. 感谢各位博友的关注和支持!
  4. 十进制转二进制函数实现(C语言)
  5. Linux的top命令详解
  6. 由于代理原因,联网失败的解决方法
  7. 科研 | 国家自然科学基金委 | 资助项目简介
  8. gpedit.msc打不开的解决办法
  9. 安装ROS使用sudo rosdep init 报错ERROR: cannot download default sources list from:
  10. 上海车展:深蓝汽车首次亮相,全场景电动出行实力圈粉