【经典算法】递归解析

  在非负整数集上定义一个函数f,它满足f(0)=0,且f(x)=2f(x-1)+x^2.从这个定义可以看出f(1)=1,f(2)=6,f(3)=21,f(4)=58。当一个函数用自身定义时就称为递归(recursive).即,一个函数直接或间接地调用自身,是为直接或间接递归。C++是允许递归的。但必须记住,C++所做的仅仅是试图遵循递归的思想。不是所有的数学递归函数都能有效的用C++递归模拟来实现。要点在于,递归函数f应该像非递归函数一样只用几行代码就能表示出来。下图给出了函数f的递归实现。

1 int f(int x) {
2     if (x == 0)
3         return 0;
4     else
5         return 2 * f(x - 1) + x * x;
6 }

第2行和第3行处理基准情况(base case),即此时函数的值可以直接算出来而不用递归。正如在没有f(0)=0的前提下。声称f(x) = 2f(x - 1) + x^2.在数学上没有意义一样。C++的递归方法若无基准情况也是毫无意义的。第5行执行的是递归调用。

  编写递归程序的时候,关键是要牢记递归的四条基本法则:

  1. 基准情形。 必须有某些基准情形不用递归就能求解。
  2. 不断推进。 对于那些需要递归求解的情形。递归调用必须总能朝着基准情形的方向迈进。
  3. 设计法则。 假设所有的递归调用都能运行。
  4. 合成效益法则。 在求解一个问题的同一实例时,切勿在不同的递归调动中做重复性的工作。(摊还分析)

递归和循环

  如果我们要重复地多次计算相同的问题,通常可以选择用递归或者循环两种不同的方法。递归是在一个函数的内部调用这个函数自身。而循环这是通过设置计算的初始值及终止条件,在一个范围内重复计算。比如求1+2+3+...+n,我们可以用递归或者循环两种方式求出结果。对应的代码如下:

  

int AddFrom1ToN_Recursive(int n) {
	return n <= 0 ? n + AddFrom1ToN_Recursive(n - 1);
}

int AddFrom1ToN_Iternative(int n) {
	int result = 0;
	for (int i = 0; i <= n; ++i)
		result += i;

	return result;

}

  通常递归的代码比较简洁。在上面的例子中,递归的代码只有一个语句。而循环的则需要四个语句。在树的前序、中序、后序遍历算法的代码中,递归的实现明显要比循环简单的多。

  面试小提示:

    通常基于递归的代码要比基于循环实现的代码要简洁很多,更加容易实现。如果面试官没有特殊要求,应聘者可以优先采用递归的方法编程。

  递归虽然有简洁的优点,但它同时也有显著的缺点。

  递归由于是函数调用自身,而函数调用是有空间和时间的消耗的:每一次函数调用,都需要在内存栈中分配空间以保存参数、返回的地址及临时变量,而且往栈里压入数据和弹出数据都需要时间。这就不难理解上述的例子中递归实现的效率不如循环。

  另外,递归中有可能很多计算都是重复的,从而对性能带来很大的负面影响。递归的本质是把一个问题分解成两个或多个小问题。如果多个小问题存在相互重叠的部分,那么就存在重复的计算。

  除了效率以外,递归还有可能引起更严重的问题:调用栈溢出。前面分析中提到需要为每一次函数调用在内存栈中分配空间,而每个进程的栈的容量是有限的。当递归调用的层级太多时,就会超出栈的容量,从而导致栈溢出。在上述的例子中,如果输入的参数比较小,如10,它们都能返回结果55.但如果输入的参数很大,比如5000,那么递归代码在运行的时间就会出错,但运行循环的代码能得到正确的结果12502500.

 相关资料:

  1. http://community.topcoder.com/tc?module=Static&d1=tutorials&d2=recursionPt1

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

参考资料:

  1. 《数据结构与算法分析C++描述》 Mark Allen Weiss

  2. 《剑指offer》 何海涛

原文地址:https://www.cnblogs.com/vincently/p/4191734.html