递归、迭代和分治(2):递归的典型例子
本期我们一起看几个典型的递归的例子。
递归就是每次调用的时候方法是自己,但是参数变了,就是下面这个样子:
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):递归的典型例子相关推荐
- 递归——迭代是人,递归是神
递归,就是自己调用自己. 首先,需要搞清楚函数是如何调用的.在执行被调函数之前,系统需要做3件事: 1.将实参,函数的返回地址等信息传递给被调函数保存. 2.为被掉函数的形参,局部变量分配空间 3.将 ...
- Java 二叉树基础概念(递归迭代)
目录 1. 树型结构 1.1概念 1.2 概念(重要) 2. 二叉树(重点) 2.1 概念 2.2 二叉树的基本形态 2.3 两种特殊的二叉树 2.4 二叉树的性质 a.满二叉树 b.完全二叉树 2. ...
- 递归、迭代和分治(1):递归
1.从现实中理解三个概念 可能很多人就已经接触了递归了,不过我敢保证很多人初学者刚开始接触递归的时候,是一脸懵逼的,我当初也是! 可能也有一大部分人知道递归,也能看的懂递归,但在实际做题过程中,却不 ...
- 递归、迭代、分治、回溯、动态规划、贪心算法
今天就简单来谈谈这几者之间的关联和区别 递归 一句话,我认为递归的本质就是将原问题拆分成具有相同性质的子问题. 递归的特点: 1.子问题拆分方程式,比如:f(n) = f(n-1) * n 2.终止条 ...
- 迭代是人,递归是神(迭代与递归的总结:比较)
https://www.cnblogs.com/Renyi-Fan/p/7708012.html 在计算机编程实现中有常常两种方法:一曰迭代(iterate):二曰递归(recursion). 从&q ...
- 【Java数据结构与算法】第十七章 二分查找(非递归)和分治算法(汉诺塔)
第十七章 二分查找(非递归)和分治算法(汉诺塔) 文章目录 第十七章 二分查找(非递归)和分治算法(汉诺塔) 一.二分查找 1.思路 2.代码实现 二.分治算法(汉诺塔) 1.概述 2.汉诺塔 一.二 ...
- 算法设计与分析(第三周)递归/迭代求Fibonacci前n项 【以及递归算法速度慢的原因】
为了理解递归写的.真想求Fibonacci前n项,迭代是更好的选择,简单并且速度快.另外,注意一下溢出问题. 递归算法速度慢的原因 递归调用本身需要使用系统栈,每次分配函数内存以及栈都需要时间.不过这 ...
- leetcode144. 二叉树的前序遍历(递归+迭代)
一:题目 二:上码 1:递归 class Solution {public:void preorder(TreeNode* root,vector<int>&v ) {if(roo ...
- [Leedcode][JAVA][第94/144/145题][前中后序遍历][递归][迭代][二叉树]
[问题描述][] 前序遍历 先输出当前结点的数据,再依次遍历输出左结点和右结点 中序遍历 先遍历输出左结点,再输出当前结点的数据,再遍历输出右结点 后续遍历 先遍历输出左结点,再遍历输出右结点,最后输 ...
- [递归|迭代] leetcode 21 合并两个有序链表
[递归|迭代] leetcode 21 合并两个有序链表 1.题目 题目链接 将两个升序链表合并为一个新的升序链表并返回.新链表是通过拼接给定的两个链表的所有节点组成的. 示例: 输入:1->2 ...
最新文章
- 以太网控制芯片DM9000在2440裸机上终于能正确接收数据了(源代码工程已经上传)...
- php程序的安全要素
- vn.py 2.0.2 发布,全功能交易程序开发框架
- C 判断 —— if...else 语句(bool变量、float变量、指针变量与“零值”进行比较)(else 到底与哪个 if 配对呢? if 语句后面的分号?)
- 通过企业分布式缓存共享运行时数据
- python多进程打印输出_python 多进程日志 logging
- python数据类型特点_Python 基础数据类型
- 03 php,PHP 03 选择结构
- 程序员都用什么来记录知识_1年前的小五都用 Python 来做什么?
- MSP430F5529 DriverLib 库函数学习笔记(九)SPI
- 真的已经讲烂了!java字符串转对象
- 解决vue2.0路由 TypeError: Cannot read property ‘matched‘ of undefined 的错误问题
- Java - 常用函数Random函数
- 蓝桥杯13年--18年Java组B组省赛题目以及题解汇总
- sha1算法源码c版
- Error splicing file: No space left on device
- 最新 EDK2 实验
- 【linux基础1】linux命令行使用技巧
- 阿里大数据面试题集合:Hadoop+HBase+Spark+Zookeeper
- 解析区块链游戏与GameFi的发展历程