算法的分类和经典算法的解析(解读递归算法原理和效率)

算法的分类和经典算法的解析(解读递归算法原理和效率)(1)

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

递归的定义

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

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

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

递归的思想

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

算法的分类和经典算法的解析(解读递归算法原理和效率)(2)

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

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

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

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

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

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

常见递归算法

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

斐波那契数列

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

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

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

算法的分类和经典算法的解析(解读递归算法原理和效率)(3)

代码如下:

/** * 斐波那契数列的递归写法 * @param n * @return */ long F(int n){ if (n<=1) return n; return F(n-1) F(n-2); }

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

阶乘

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

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

算法的分类和经典算法的解析(解读递归算法原理和效率)(4)

代码如下:

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

倒序输出一个正整数

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

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

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

算法的分类和经典算法的解析(解读递归算法原理和效率)(5)

代码如下:

/** * 倒序输出正整数的各位数 * @param n */ void printDigit(int n){ System.out.print(n); 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的汉诺塔问题了!)

代码如下:

/** * 汉诺塔 * 有柱子 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); } }

递归的效率

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

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

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

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

算法的分类和经典算法的解析(解读递归算法原理和效率)(6)

从上图可以看出,在计算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)也调用了两次,而这些冗余的调用是完全没有必要的。可以计算这个算法的复杂度是指数级的。

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

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

,

免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com

    分享
    投诉
    首页