递归-简介

一:递归与循环的区别,举例说明       

递归:你打开面前这扇门,看到屋里面还有一扇门。你走过去,发现手中的钥匙还可以打开它,你推开门,发现里面还有一扇门,你继续打开它。若干次之后,你打开面前的门后,发现只有一间屋子,没有门了。然后,你开始原路返回,每走回一间屋子,你数一次,走到入口的时候,你可以回答出你到底用这你把钥匙打开了几扇门。

           递归也有点类似你在百度上面查一个词的解释,发现解释里面又有另外一个新的名词你不知道,于是又开始查这个新的名词,新的名字里面又有你不知道的新名词,于是你又开始查,这样一直查下去直到最后你终于明白了最后一个名词,于是你也就明白了倒数第二个名词,接着明白倒数第三个名词,这样一直"归"到第一个名词,所有递归其实最先解决的是"递"到最里面的那个,所有一般情况下递归的结束条件也就是这里了,一般是在"n==0"或者"n==1"的情况下设置递归结束的条件

   循环:你打开面前这扇门,看到屋里面还有一扇门。你走过去,发现手中的钥匙还可以打开它,你推开门,发现里面还有一扇门(若前面两扇门都一样,那么这扇门和前两扇门也一样;如果第二扇门比第一扇门小,那么这扇门也比第二扇门小,你继续打开这扇门,一直这样继续下去直到打开所有的门。但是,入口处的人始终等不到你回去告诉他答案。

二. 递归的内涵

1、定义 (什么是递归?)

   在数学与计算机科学中,递归(Recursion)是指在函数的定义中使用函数自身的方法。实际上,递归,顾名思义,其包含了两个意思:递 和 归,这正是递归思想的精华所在。

2、递归思想的内涵(递归的精髓是什么?)

   正如上面所描述的场景,递归就是有去(递去)有回(归来),如下图所示。“有去”是指:递归问题必须可以分解为若干个规模较小,与原问题形式相同的子问题,这些子问题可以用相同的解题思路来解决,就像上面例子中的钥匙可以打开后面所有门上的锁一样;“有回”是指 : 这些问题的演化过程是一个从大到小,由近及远的过程,并且会有一个明确的终点(临界点),一旦到达了这个临界点,就不用再往更小、更远的地方走下去。最后,从这个临界点开始,原路返回到原点,原问题解决。

更直接地说,递归的基本思想就是把规模大的问题转化为规模小的相似的子问题来解决。特别地,在函数实现时,因为解决大问题的方法和解决小问题的方法往往是同一个方法,所以就产生了函数调用它自身的情况,这也正是递归的定义所在。格外重要的是,这个解决问题的函数必须有明确的结束条件,否则就会导致无限递归的情况。

3、用归纳法来理解递归

   数学都不差的我们,第一反应就是递归在数学上的模型是什么,毕竟我们对于问题进行数学建模比起代码建模拿手多了。观察递归,我们会发现,递归的数学模型其实就是 数学归纳法,这个在高中的数列里面是最常用的了,下面回忆一下数学归纳法。

而递归和我们的思维方式正好相反。

那我们怎么判断这个递归计算是否是正确的呢?Paul Graham 提到一种方法,如下:

如果下面这两点是成立的,我们就知道这个递归对于所有的 n 都是正确的。

  1. 当 n=0,1 时,结果正确;

  2. 假设递归对于 n 是正确的,同时对于 n+1 也正确。

  3. 部分递归还需要求出f(n)与f(n-1)之间的关系

这种方法很像数学归纳法,也是递归正确的思考方式,上述的第 1 点称为基本情况,第 2 点称为通用情况。

在递归中,我们通常把第 1 点称为终止条件,因为这样更容易理解,其作用就是终止递归,防止递归无限地运行下去。

4、递归的三要素

   在我们了解了递归的基本思想及其数学模型之后,我们如何才能写出一个漂亮的递归程序呢?笔者认为主要是把握好如下三个方面:

1、明确递归终止条件;

2、给出递归终止时的处理办法;

3、提取重复的逻辑,缩小问题规模。

5、递归的应用场景

   在我们实际学习工作中,递归算法一般用于解决三类问题:

   (1). 问题的定义是按递归定义的(Fibonacci函数,阶乘,…);

   (2). 问题的解法是递归的(有些问题只能使用递归方法来解决,例如,汉诺塔问题,…);

   (3). 数据结构是递归的(链表、树等的操作,包括树的遍历,树的深度,…)。

三. 递归与循环

   递归与循环是两种不同的解决问题的典型思路。递归通常很直白地描述了一个问题的求解过程,因此也是最容易被想到解决方式。循环其实和递归具有相同的特性,即做重复任务,但有时使用循环的算法并不会那么清晰地描述解决问题步骤。单从算法设计上看,递归和循环并无优劣之别。然而,在实际开发中,因为函数调用的开销,递归常常会带来性能问题,特别是在求解规模不确定的情况下;而循环因为没有函数调用开销,所以效率会比递归高。递归求解方式和循环求解方式往往可以互换,也就是说,如果用到递归的地方可以很方便使用循环替换,而不影响程序的阅读,那么替换成循环往往是好的。问题的递归实现转换成非递归实现一般需要两步工作:

   (1). 自己建立“堆栈(一些局部变量)”来保存这些内容以便代替系统栈,比如树的三种非递归遍历方式;

   (2). 把对递归的调用转变为对循环处理。

 

 四:递归的实质

         例如以二叉树的遍历举例

         

 上图是二叉树的遍历示意图,遍历的结果是"A,B,D,G,H,C,E,I,F"用函数来表示如下:

 

public void traverse(Tree t){
        //如果节点不为null,这开始遍历
        if (t != null){
            //1:获取此树节点左边的子树
            traverse(t.getLeftSonTree());
            //2:获取此树节点右边的子树
            traverse(t.getRightSonTree());
        }
    }

我们来分析一下,为啥遍历的结果为"A,B,D,G,H,C,E,I,F"

在分析之前我们首先要明确一点,递归并不是并发执行的,而是有序执行的,也就是说只有前面的函数执行完了,才会执行后面的函数,因此在函数traverse中,有两个递归函数,只有前面的traverse(t.getLeftSonTree());执行完了才会去执行traverse(t.getRightSonTree()),这里我们假定没有子节点的Tree为null;

1.最开始传入进来的是A节点,A包含两个子节点,因此满足"t != null"的条件,执行到 traverse(A.getLeftSonTree());首先通过A.getLeftSonTree()获取的是A节点左边的子树,得到B, traverse(A.getLeftSonTree())就相当于是traverse(B)。

2.执行traverse(B),接着进行if判断,B是包含一个子节点,因此满足"B != null",接着执行traverse(B.getLeftSonTree());,获取到B左边的子节点D(请注意我们现在仍然是在最开始的traverse(A.getLeftSonTree())函数中在进行递归,这个函数并没有执行完)

3.执行traverse(D),进行if判断,D包含G和H两个子节点,因此满足"D != null",接着执行traverse(D.getLeftSonTree()),得到G;

4,执行traverse(G),进行if判断,G不包含任何子节点,不满足if条件,此时traverse(G)执行结束,这里就是递归结束的条件。

5,还记得traverse(G)是从哪里被调用的吗?是的,traverse(G)是在traverse(D))函数中被调用的,我们来看下traverse(D)函数的调用过程

      

public void traverse(Tree D){
        //如果D节点是否为null
        if (D != null){
            //1:获取D节点左边的子树
            traverse(D.getLeftSonTree());
            //2:获取D节点右边的子树
            traverse(D.getRightSonTree());
        }
    }

   从代码中我们可以看到,traverse(D.getLeftSonTree())函数执行完毕了,而traverse(D)函数并没有执行完毕,接下来要执行traverse(D.getRightSonTree());,此时获取到H节点

6.执行traverse(H),同traverse(G)一样,H节点也没有任何子节点,因此不满足if条件,traverse(H)执行结束,当traverse(H)执行结束时,也就意味着traverse(D)函数执行完毕了。

7,traverse(D)实在traverse(B)函数中调用的,也就意味这traverse(B.getLeftSonTree())函数执行完毕,接着应该执行traverse(B.getRightSonTree()),由于B没有右子节点,因此获取到的Tree为null,不满足if条件,没有进入if也就不能再递归,因此travese(B.getRightSonTree())也执行完毕了,也就是traverse(B)执行完毕了

8,traverse(B)执行完毕,也就是traverse(A.getLeftSonTree())执行完毕了,至此最开始传入函数traverse的节点A的traverse(A.getLeftSonTree())才执行完毕,接着才去执行traverse(A.getRightSonTree()),去获取A右边的子节点。

        右边的过程和上面的道理是一样的,这里不在赘述,也是先获取完左边的子节点,再获取到右边的子节点,因此右边获取的顺序为“C,E,I,F”

 

参考:https://blog.csdn.net/weixin_43025071/article/details/89149695

展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客
应支付0元
点击重新获取
扫码支付

支付成功即可阅读