热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

从“数学归纳法”到理解“递归算法”

每章一点正能量:人的一生可能燃烧也可能腐朽。前言相信大家在面试或者工作中偶尔会遇到递归算法的提问或者编程,我们今天来聊一聊从数学归纳法到理解递归算法。如


每章一点正能量:人的一生可能燃烧也可能腐朽。

前言

相信大家在面试或者工作中偶尔会遇到递归算法的提问或者编程,我们今天来聊一聊从数学归纳法到理解递归算法。如有错误还请大家及时指出~

本文已同步至 GitHub/Gitee/公众号,感兴趣的同学帮忙点波关注~

1. 数学归纳法

1.1 简介

来源百度百科

数学归纳法(Mathematical Induction, MI)是一种数学证明方法,通常被用于证明某个给定命题在整个(或者局部)自然数范围内成立。除了自然数以外,广义上的数学归纳法也可以用于证明一般良基结构,例如:集合论中的树。这种广义的数学归纳法应用于数学逻辑和计算机科学领域,称作结构归纳法。在数论中,数学归纳法是以一种不同的方式来证明任意一个给定的情形都是正确的(第一个,第二个,第三个,一直下去概不例外)的数学定理。虽然数学归纳法名字中有“归纳”,但是数学归纳法并非不严谨的归纳推理法,它属于完全严谨的演绎推理法。事实上,所有数学证明都是演绎法。

自然数是指表示物体个数的数,即由0开始,0,1,2,3,4,……一个接一个,组成一个无穷的集体,即指非负整数。

1.2 推演步骤

简单了解数学归纳法的概念后,我们来看看数学归纳法的推演步骤。

我们知道数学归纳法用来证明任意一个给定的情形都是正确的,也就是说,第一个,第二个,一直到所有情形,概不例外。

其证明步骤如下:

  1. 证明基本情况(通常是N = 1 的时候)是否成立。
    证明对于N=1成立。我们只需要先从最小的自然数开始证明。这一步通常非常简单。关键是证明第二步。

  2. 证明N > 1 时,假设 N - 1 成立,那么对于N成立(N为任意大于1的自然数)。
    这一步并不是直接证明的,而是假设N-1成立,利用这个结论推出N是成立的。如果能够推出的话,就可以说:对于所有的自然数都成立。因为证明了对1成立,那么对2成立,对3也成立。那么就证明了对所有自然数都成立。
    我们会发现数学归纳法它很合适用来证明,例如常见的等差、等比、以及平方、立方数列的求和等等。

1.3 小栗子

我们来举一个小栗子,回顾下我们高中时期所学的数学归纳法是如何进行证明。

例子:

证明: 1+2+3+...+n = n(n+1)/2

我们来将上面 1.2 推演步骤 用起来。

  • 第一步: 证明基本情况(通常是N = 1 的时候)是否成立。

我们把N=1同时代入等号左边和右边,得

1 = 1*(1+1)/2

成立!

  • 第二步: 证明N > 1 时,假设 N - 1 成立,那么对于N成立(N为任意大于1的自然数)。

这里我们需要分两步。

  • ① 假设对于N-1的情况下成立

我们依然将N-1同时代入等号的左边和右边,得:

1+2+3+...+(n-1) = (n-1)n/2

  • ② 将假设结论代入,同时加N

我们假设N-1是成立的,那么我们在等号左边与右边同时加N,肯定也是成立的,得:

 1+2+3...+(n-1)+n = (n-1)n/2+n 

化简右边得:n(n+1)/2,那么我们最后证明的结果就是成立的!

即:1+2+3+...+n = n(n+1)/2 成立。通过以上步骤,我们可以证明这个公式是成立的。

1.4 小结

归纳法适用于想解决一个问题转化为解决他的子问题,而他的子问题又变成子问题的子问题,而且我们发现这些问题其实都是一个模型,也就是说存在相同的逻辑归纳处理项。

接下来我们来看看,我们写程序和数学归纳法的关联。

2. 递归

说起递归算法,其实我们每个开发人员都肯定听过或者写过。记得我最开始接触递归算法的时候,还是大一学习谭浩强老师写的那本C语言时,里面介绍了递归算法。给我的印象就是:自己调用自己。后来在工作中,用到的地方也不多,印象中只有一次写级联菜单的时候用到了递归算法。(是不是我写的代码太水,大家也可以说说哪里用到过递归算法)本章就来通过数学归纳法来回顾下我们曾经学过的递归算法。

2.1 理解递归

递归的基本思想:以此类推

具体来讲就是把规模大的问题转化为规模小的相似的子问题来解决。在函数实现时,因为解决大问题的方法和解决小问题的方法往往是同一个方法,所以就产生了函数调用它自身的情况。另外这个解决问题的函数必须有明显的结束条件,这样就不会产生无限递归的情况了。仔细观察递归,就会发现:递归的数学模型其实就是归纳法

2.2 递归条件

我们在使用递归的时候需要满足一些基本条件,如果不满足的话,就有可能出现无限递归,最后会导致堆栈溢出了。

满足条件:

  1. 严格定义递归函数作用,包括参数,返回值,其他变量。

  2. 先一般情况,后特殊情况。

  3. 有退出条件。在一般情况下,能让递归正常退出的条件。

  4. 每次调用必须缩小问题规模,且新问题与原问题有着相同的形式,即规律。

上面的条件一环扣一环,也可以缩减成两个主要条件:有规律,有退出条件。我们以上面的条件,来结合案例进行理解。

2.3 小栗子

2.3.1 递归求和

例题:

1+2+3+...+n=? 

第一步: 严格定义递归函数作用,包括参数,返回值,其他变量。

我们初看题目,可以知道这是一个简单的求和,即从1开始:1+2+3+…一直加到n。所以我们可以定义一个入参为n,返回值类型为int的一个方法,既然是递归求和,我们的方法名就叫recursionSum。

public static int recursionSum(int n) { //为了方便调用,我用了static

    return 0;
}

System.out.println("公众号:Coder编程:" + recursionSum(0));

那么我们第一步就做完了。

第二步: 先一般情况,后特殊情况。

我们先用一般的情况进行求和计算,例如代入1,2,3这样的一般情况。即:

public static int recursionSum(int n) { 
    if(n == 1) {
        return 1;
    }

    if(n == 2) {
        return 1+2;
    }

    if(n == 3) {
        return 1+2+3;
    }
    return 0;
}

System.out.println("公众号:Coder编程:" + recursionSum(3));

第三步: 有退出条件。在一般情况下,能让递归正常退出的条件。

其实,我们做完第二步,就会发现已经把第三步做完了。即有了让递归正常退出的条件!

第四步: 每次调用必须缩小问题规模,且新问题与原问题有着相同的形式,即规律。

这一步是最关键的,也是最核心的!我们需要找到其规律,并且能缩小问题的规模。我们会发现,当我们需要求第N个数的和的时候,我们必须知道前N-1个数的和,即 sum(N-1)。前N个数的和就是sum(N-1)+N。找到这个规律后,我们就可以定义一个临时变量sum来接收前N个数的和了。

public static int recursionSum(int n) {

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

    if(n == 2) {
        return 1+2;
    }

    if(n == 3) {
        return 1+2+3;
    }

    int sum = recursionSum(n-1)+n;
    return sum;
}

System.out.println("公众号:Coder编程:前5个数的和" + recursionSum(5));

输出结果:15

我们优化一下:

public static int recursionSum(int n) {

    if (n < 0){
       throw new Exception("参数不能为负&#xff01;");
    }
    if(n &#61;&#61; 1) {
        return 1;
    }

    return recursionSum(n-1)&#43;n;
}

System.out.println("公众号&#xff1a;Coder编程&#xff1a;前5个数的和" &#43; recursionSum(5));

是不是突然发现递归其实也没想的那么难&#xff1f;

2.3.2 举一反三&#xff1f;

接下来我们难度进行升级&#xff01;看大家能不能都理解了。我就不像上面求和那么啰嗦了&#xff01;

2.3.2.1 求阶乘

例题&#xff1a;求n的阶乘&#xff08;n>1&#xff0c;n是正整数&#xff09;

阶乘的递推公式为&#xff1a;factorial(n)&#61;n*factorial(n-1)&#xff0c;其中n为非负整数,且0!&#61;1,1!&#61;1
这里就不做过多说明&#xff0c;跟求后过程一致&#xff0c;可以模仿求和的过程&#xff0c;大家可以先自己尝试写下&#xff0c;下面我直接贴代码了&#xff1a;

public static int factorial(int n) throws Exception {
    if (n < 0){
        throw new Exception("参数不能为负&#xff01;");
    }else if (n &#61;&#61; 1 || n &#61;&#61; 0) {
        return 1;
    }else {
        return n * factorial(n - 1);
    }
}

System.out.println("公众号&#xff1a;Coder编程&#xff1a;3的阶乘:" &#43; factorial(3));

输出结果&#xff1a; 公众号&#xff1a;Coder编程&#xff1a;3的阶乘:6

2.3.2.2 斐波那契数列

斐波那契数列 我想大家同样熟悉了解&#xff0c;下面我们继续回顾一下斐波那契数列到底是什么&#xff1f;

斐波那契数列图

斐波那契数列: 1、1、2、3、5、8、13、21…..

可以看出从第三位起&#xff1a;第三项等于前两项之和。总结递推公式&#xff1a;:Fib(n)&#61;Fib(n-1)&#43;Fib(n-2)。所以我们可以将前两位作为退出递归的条件。即&#xff1a;if(n&#61;&#61;1) retrun 1 if(n&#61;&#61;2) return 1

因此我们可以直接用公式&#xff08;规律&#xff09;和退出条件&#xff0c;写出编程代码&#xff1a;

public static int fib(int n) throws Exception {
    if (n < 0) {
        throw new Exception("参数不能为负&#xff01;");
    }else if (n &#61;&#61; 0 || n &#61;&#61; 1){
        return n;
    }else {
        return fib(n - 1) &#43; fib(n - 2);
    }
}

System.out.println("公众号&#xff1a;Coder编程&#xff1a;斐波那契数列:" &#43; fib(3));

2.3.2.3 汉诺塔问题

相传在古印度圣庙中&#xff0c;有一种被称为汉诺塔(Hanoi)的游戏。该游戏是在一块铜板装置上&#xff0c;有三根杆(编号A、B、C)&#xff0c;在A杆自下而上、由大到小按顺序放置不同个数的金盘(如下图)。

游戏的目标&#xff1a;把A杆上的金盘全部移到C杆上&#xff0c;并仍保持原有顺序叠好。

操作规则&#xff1a;每次只能移动一个盘子&#xff0c;并且在移动过程中三根杆上都始终保持大盘在下&#xff0c;小盘在上&#xff0c;操作过程中盘子可以置于A、B、C任一杆上。

汉诺塔图

在总结规律和写代码之前&#xff0c;我们先来玩几把简单的&#xff08;先一般后特殊&#xff09;&#xff1a;

注&#xff1a;我们以数字的大小作为盘子的大小。

  1. 一个盘子的情况&#xff1a;

    1.1 将A柱子的1号盘子直接移动到C柱子中。
    1.2 结束。

  2. 两个盘子的情况&#xff1a;

    2.1 将A柱子的1号盘子移动到B柱子。
    2.2 将A柱子的2号盘子移动到C柱子。
    2.3 将B柱子的1号盘子移动到C柱子。
    2.4 结束。

  3. 三个盘子的情况&#xff1a;

    3.1 将A柱子的1号盘子移动到C柱子。
    3.2 将A柱子的2号盘子移动到B柱子。
    3.3 将C柱子的1号盘子移动到B柱子。
    3.4 将A柱子的3号盘子移动到C柱子。
    3.5 将B柱子的1号盘子移动到A柱子。
    3.6 将B柱子的2号盘子移动到C柱子。
    3.7 将A柱子的1号盘子移动到C柱子。
    3.8 结束。


我们会发现&#xff0c;随着盘子数量的增加&#xff0c;盘子移动的难度也开始加大。

这时候不要害怕&#xff0c;我们回过头再来看这个问题&#xff1a;当盘子的数量是4个、5个…N个的时候&#xff0c;我们该如何解决呢&#xff1f;我们是不是可以用数学归纳法的思想或者递归的思想去解决呢&#xff1f;答案是&#xff1a;肯定的。这时候我们需要去找到他们的规律在哪&#xff1f;

我们再观察下上面在一般情况下移动盘子的规律在哪&#xff1f;

  • 1.当只有一个盘子的时候&#xff0c;可以将盘子直接移动到目标柱子C中。即退出条件

  • 2.当只有两个盘子的时候&#xff0c;我们只需要将B柱子作为中介&#xff0c;将盘子1先放到中介柱子B上&#xff0c;然后将盘子2放到目标柱子C上&#xff0c;最后将中介柱子B上的盘子放到目标柱子C上即可。

第二点可以看成&#xff1a;当我们有N个盘子的时候&#xff0c;第N个盘子看成一个盘子&#xff0c;(N-1)个盘子看做成一个盘子。需要将(N-1)个盘子放在中介柱子B上&#xff0c;N个盘子放在目标柱子C即可。即规律

当我们有三个盘子的时候&#xff0c;我们会发现一个问题&#xff1a; 角色变化

  1. 将A塔座的第(N-1)~1个盘子看成是一个盘子&#xff0c;放到中柱子B上&#xff0c;然后将第N个盘子放到目标柱子C上。这时候柱子A空了!柱子A成为中介柱子&#xff0c;柱子B成为起始柱子

  2. 柱子B这时候有N-1个盘子&#xff0c;将第(N-2)~1个盘子看成是一个盘子&#xff0c;放到中介柱子A上&#xff0c;然后将柱子B的第(N-1)号盘子放到目标柱子C上。这时候柱子B空了!柱子B又成为了中介柱子&#xff0c;A成为了起始柱子!

重复1、2步骤&#xff0c;直到所有盘子都放到目标塔座C上结束。

总结一下&#xff1a;

  1. 从初始柱子A上移动包含n-1个盘子到中介柱子B上。

  2. 将初始柱子A上剩余的一个盘子&#xff08;最大的一个盘子&#xff09;放到目标柱子C上。

  3. 将中介柱子B上n-1个盘子移动到目标柱子C上。

move(3,"A","B","C");

/**
 * 汉诺塔问题
 * &#64;param dish 盘子个数(也表示名称)
 * &#64;param from 初始柱子
 * &#64;param temp 中介柱子
 * &#64;param to   目标柱子
 */
public static void move(int dish,String from,String temp,String to){
    if(dish &#61;&#61; 1){
        System.out.println("将盘子"&#43;dish&#43;"从柱子"&#43;from&#43;"移动到目标柱子"&#43;to);
    }else{
        move(dish-1,from,to,temp);//A为初始柱子&#xff0c;B为目标柱子&#xff0c;C为中介柱子
        System.out.println("将盘子"&#43;dish&#43;"从柱子"&#43;from&#43;"移动到目标柱子"&#43;to);
        move(dish-1,temp,from,to);//B为初始柱子&#xff0c;C为目标柱子&#xff0c;A为中介柱子
    }
}

  • move(dish-1,from,to,temp);//A为初始柱子&#xff0c;B为目标柱子&#xff0c;C为中介柱子
    这里需要将n-1之前的盘子都放到B柱子上&#xff0c;最后第n个盘子放到C柱子。

  • move(dish-1,temp,from,to);//B为初始柱子&#xff0c;C为目标柱子&#xff0c;A为中介柱子
    这时候B变为了初始柱子&#xff0c;A成为了目标柱子。将之前n-1个盘子放到C目标柱子中。

打印结果&#xff1a;

打印结果

文末

本章节主要简单介绍了数学归纳法与递归算法的一些思想。希望对大家有所帮助&#xff01;
今后我会在每张文章开头增加 每章一点正能量 &#xff0c;文末增加5个编程相关的英语单词 学点英语。希望大家和我一样每天都能积极向上&#xff0c;一起学习一同进步&#xff01;

学点英语

  • JRE  Java Runtime Environment&#xff08;Java运行环境&#xff09;&#xff0c;运行 JAVA程序所必须的环境的集合&#xff0c;包含JVM标准实现及Java核心类库。  

  • JSDK  Java Software Development Kit&#xff0c;和JDK以及J2SE 等同。  

  • JDK  Java Development Kit(Java开发工具包):包括运行环境 、编译工具及其它工具、源代码等&#xff0c;基本上和J2SE等同。  

  • J2ME  Java 2 Micro Edition&#xff08;JAVA2精简版&#xff09;API规格基 于J2SE &#xff0c;但是被修改为可以适合某种产品的单一要求。J2ME使JAVA程序可以很方便的应用于电话卡、寻呼机等小型设备&#xff0c;它包括两种类型的组件&#xff0c;即配置 &#xff08;configuration&#xff09;和描述&#xff08;profile&#xff09;。

欢迎关注公众号&#xff1a;Coder编程
获取最新原创技术文章和相关免费学习资料&#xff0c;随时随地学习技术知识&#xff01;

参考文章&#xff1a;

https://www.cnblogs.com/ysocean/p/8005694.html

http://www.nowamagic.net/librarys/veda/detail/2314

微信公众号

推荐阅读

一篇带你读懂TCP之“滑动窗口”协议

带你了解数据库中JOIN的用法




推荐阅读
  • 微软头条实习生分享深度学习自学指南
    本文介绍了一位微软头条实习生自学深度学习的经验分享,包括学习资源推荐、重要基础知识的学习要点等。作者强调了学好Python和数学基础的重要性,并提供了一些建议。 ... [详细]
  • 本文介绍了数据库的存储结构及其重要性,强调了关系数据库范例中将逻辑存储与物理存储分开的必要性。通过逻辑结构和物理结构的分离,可以实现对物理存储的重新组织和数据库的迁移,而应用程序不会察觉到任何更改。文章还展示了Oracle数据库的逻辑结构和物理结构,并介绍了表空间的概念和作用。 ... [详细]
  • 本文介绍了使用Java实现大数乘法的分治算法,包括输入数据的处理、普通大数乘法的结果和Karatsuba大数乘法的结果。通过改变long类型可以适应不同范围的大数乘法计算。 ... [详细]
  • 本文讨论了如何优化解决hdu 1003 java题目的动态规划方法,通过分析加法规则和最大和的性质,提出了一种优化的思路。具体方法是,当从1加到n为负时,即sum(1,n)sum(n,s),可以继续加法计算。同时,还考虑了两种特殊情况:都是负数的情况和有0的情况。最后,通过使用Scanner类来获取输入数据。 ... [详细]
  • 本文介绍了C#中数据集DataSet对象的使用及相关方法详解,包括DataSet对象的概述、与数据关系对象的互联、Rows集合和Columns集合的组成,以及DataSet对象常用的方法之一——Merge方法的使用。通过本文的阅读,读者可以了解到DataSet对象在C#中的重要性和使用方法。 ... [详细]
  • 知识图谱——机器大脑中的知识库
    本文介绍了知识图谱在机器大脑中的应用,以及搜索引擎在知识图谱方面的发展。以谷歌知识图谱为例,说明了知识图谱的智能化特点。通过搜索引擎用户可以获取更加智能化的答案,如搜索关键词"Marie Curie",会得到居里夫人的详细信息以及与之相关的历史人物。知识图谱的出现引起了搜索引擎行业的变革,不仅美国的微软必应,中国的百度、搜狗等搜索引擎公司也纷纷推出了自己的知识图谱。 ... [详细]
  • 猜字母游戏
    猜字母游戏猜字母游戏——设计数据结构猜字母游戏——设计程序结构猜字母游戏——实现字母生成方法猜字母游戏——实现字母检测方法猜字母游戏——实现主方法1猜字母游戏——设计数据结构1.1 ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • EPICS Archiver Appliance存储waveform记录的尝试及资源需求分析
    本文介绍了EPICS Archiver Appliance存储waveform记录的尝试过程,并分析了其所需的资源容量。通过解决错误提示和调整内存大小,成功存储了波形数据。然后,讨论了储存环逐束团信号的意义,以及通过记录多圈的束团信号进行参数分析的可能性。波形数据的存储需求巨大,每天需要近250G,一年需要90T。然而,储存环逐束团信号具有重要意义,可以揭示出每个束团的纵向振荡频率和模式。 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • HDU 2372 El Dorado(DP)的最长上升子序列长度求解方法
    本文介绍了解决HDU 2372 El Dorado问题的一种动态规划方法,通过循环k的方式求解最长上升子序列的长度。具体实现过程包括初始化dp数组、读取数列、计算最长上升子序列长度等步骤。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
author-avatar
我本僐悢
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有