zoukankan      html  css  js  c++  java
  • 深入理解递归算法

    对于很多人来说,都知道递归,也能看的懂递归,但在实际项目过程中,却不知道如何使用递归,这里给递归做个总结。

    递归的定义

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

    通俗点讲,我们可以把” 递归 “比喻成 “查字典 “,当你查一个词,发现这个词的解释中某个词仍然不懂,于是你开始查这第二个词。

    可惜,第二个词里仍然有不懂的词,于是查第三个词,这样查下去,直到有一个词的解释是你完全能看懂的,那么递归走到了尽头,然后你开始后退,逐个明白之前查过的每一个词,最终,你明白了最开始那个词的意思。

    递归的思想

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

    递归

    递归的三大要素

    • 明确递归终止条件;
    • 给出递归终止时的处理办法;
    • 提取重复的逻辑,缩小问题规模;

    明确递归终止条件

    我们知道,递归就是有去有回,既然这样,那么必然应该有一个明确的临界点,程序一旦到达了这个临界点,就不用继续往下递去而是开始实实在在的归来。换句话说,该临界点就是一种简单情境,可以防止无限递归。

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

    我们刚刚说到,在递归的临界点存在一种简单情境,在这种简单情境下,我们应该直接给出问题的解决方案。一般地,在这种情境下,问题的解决方案是直观的、容易的。

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

    我们在阐述递归思想内涵时谈到,递归问题必须可以分解为若干个规模较小、与原问题形式相同的子问题,这些子问题可以用相同的解题思路来解决。从程序实现的角度而言,我们需要抽象出一个干净利落的重复的逻辑,以便使用相同的方式解决子问题。

    常见递归算法

    下面总结一下常见的递归问题和实现算法。

    斐波那契数列

    斐波那契数列的排列是:0,1,1,2,3,5,8,13,21,34,55,89,144……依次类推下去,你会发现,它后一个数等于前面两个数的和。在这个数列中的数字,就被称为斐波那契数。

    递归思想:一个数等于前两个数的和。

    首先分析数列的递归表达式:

    递归算法代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    /**
    * 斐波那契数列的递归写法
    * @param n
    * @return
    */
    long F(int n){
    if (n<=1) return n;
    return F(n-1)+F(n-2);
    }

    可以看到,递归写法简单优美,省去考虑很多边界条件的时间。当然,递归算法会保存很多的临时数据,类似于堆栈的过程,如果栈深太深,就会造成内存用尽,程序崩溃的现象。

    阶乘

    递归思想:n! = n * (n-1)!

    首先分析数列的递归表达式:

    递归算法

    代码如下:

    1
    2
    3
    4
    long factorial(int n){
    if (n <=1) return 1;
    return j(n-1)*n;
    }

    倒序输出一个正整数

    例如给出正整数 n=12345,希望以各位数的逆序形式输出,即输出54321。

    递归思想:首先输出这个数的个位数,然后再输出前面数字的个位数,直到之前没数字。

    首先分析数列的递归表达式: 递归算法代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /**
    * 倒序输出正整数的各位数
    * @param n
    */
    void printDigit(int n){
    System.out.print(n%10);
    if (n > 10){
    printDigit(n/10);
    }
    }

    汉诺塔

    数学描述就是:

    有三根杆子X,Y,Z。X杆上有N个(N>1)穿孔圆盘,盘的尺寸由下到上依次变小。要求按下列规则将所有圆盘移至Y杆:

    1. 每次只能移动一个圆盘;
    2. 大盘不能叠在小盘上面。

    递归思想:

    1. 将X杆上的n-1个圆盘都移到空闲的Z杆上,并且满足上面的所有条件
    2. 将X杆上的第n个圆盘移到Y上
    3. 剩下问题就是将Z杆上的n-1个圆盘移动到Y上了

    公式描述有点麻烦,用语言描述下吧:

    1. 以Y杆为中介,将前n-1个圆盘从X杆挪到Z杆上(本身就是一个n-1的汉诺塔问题了!)
    2. 将第n个圆盘移动到Y杆上
    3. 以X杆为中介,将Z杆上的n-1个圆盘移到Y杆上(本身就是一个n-1的汉诺塔问题了!)

    代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    **
    * 汉诺塔
    * 有柱子 x z y,最终将x上的n个圆盘借助z移动到y上
    * 递归思想:
    * 1.将x上的n-1个放入到z上,借助y
    * 2.将x上的n圆盘放到y上
    * 3.将z上的n-1个圆盘放入y
    * @param n
    * @param from
    * @param tmp
    * @param to
    */
    void hanoi(int n,char from,char tmp,char to){
    if (n>0) {
    hanoi(n - 1, from, to, tmp);
    System.out.println("take " + n + " from " + from + " to " + to);
    hanoi(n - 1, tmp, from, to);
    }
    }

    递归的效率

    还是拿斐波那契数列来做例子:

    1
    2
    3
    4
    long Fib(int n){
    if (n<=1) return n;
    return Fib(n-1)+Fib(n-2);
    }

    这段代码应该算是短小精悍(执行代码只有一行),直观清晰,而且非常符合许多程序员的代码美学,是如果用这段代码试试计算Fib(1000)我想就再也爽不起来了,它的运行时间也许会让你抓狂。

    看来好看的代码未必中用,如果程序在效率不能接受那美观神马的就都是浮云了。如果简单分析一下程序的执行流,就会发现问题在哪,以计算Fibonacci(5)为例:

    递归

    从上图可以看出,在计算Fib(5)的过程中,Fib(1)计算了两次、Fib(2)计算了3次,Fib(3)计算了两次,本来只需要5次计算就可以完成的任务却计算了9次。这个问题随着规模的增加会愈发凸显,以至于Fib(1000)已经无法再可接受的时间内算出。

    我们当时使用的是简单的用定义来求 fib(n),也就是使用公式 fib(n) = fib(n-1) + fib(n-2)。这样的想法是很容易想到的,可是仔细分析一下我们发现,当调用fib(n-1)的时候,还要调用fib(n-2),也就是说fib(n-2)调用了两次,同样的道理,调用f(n-2)时f(n-3)也调用了两次,而这些冗余的调用是完全没有必要的。可以计算这个算法的复杂度是指数级的。

    由以上分析我们可以看到,递归在处理问题时要反复调用函数,这增大了它的空间和时间开销,所以在使用迭代可以很容易解决的问题中,使用递归虽然可以简化思维过程,但效率上并不合算。效率和开销问题是递归最大的缺点。

    虽然有这样的缺点,但是递归的力量仍然是巨大而不可忽视的,因为有些问题使用迭代算法是很难甚至无法解决的。这时递归的作用就显示出来了。

  • 相关阅读:
    分析现有 WPF / Windows Forms 程序能否顺利迁移到 .NET Core 3.0(使用 .NET Core 3.0 Desktop API Analyzer )
    .NET 命令行参数包含应用程序路径吗?
    WPF 多线程 UI:设计一个异步加载 UI 的容器
    .NET/C# 使用反射调用含 ref 或 out 参数的方法
    .NET/C# 判断某个类是否是泛型类型或泛型接口的子类型
    .NET/C# 使用反射注册事件
    都是用 DllImport?有没有考虑过自己写一个 extern 方法?
    好的框架需要好的 API 设计 —— API 设计的六个原则
    Netty源码学习(七)FastThreadLocal
    Netty源码学习(六)ChannelPipeline
  • 原文地址:https://www.cnblogs.com/feng9exe/p/11912656.html
Copyright © 2011-2022 走看看