递归简论
在C/C++这类编译型语言中,递归的定义就是在该函数的内部,调用了自己。与LISP这类函数式语言会有些差异,LISP中在函数内部调用自己也可能成为迭代。
递归的四要素
- 基准情形。
- 不断推进。
- 设计法则。
- 合成效益法则。
对递归最简单的理解我们可以通过一个数学函数来理解对上面的四要素进行理解。假设函数F(X)=F(X-1)+X(当X = 0时,F(X)=0),那么我们用递归来实现一遍。
int function(int x)
{
if(0 == x) //基准情形
{
return 0;
}
else
{
return function(x - 1) + x; //不断推进
}
}
- 基准情形。基准情形必须存在,上例中的X == 0的情况,如果不存在,递归将无限进行下去;
- 不断推进。递归必须不断的想着基准情形方向靠拢,上例中每次递归调用都回向后减一;
- 设计法则。假设所有的递归调用都能运行;
- 合成效益法则。在求解一个问题的同一实例时,切勿在不同的递归调用中做重复性的工作。
用迭代的方法实现一遍
int function_iter(int x)
{
int result = 0;
for(int i = 0; i <= x; i++)
{
result += i;
}
return result;
}
理论上凡是可以用递归实现的都可以用迭代来实现。下面在通过另外一个例子来了解递归与循环的区别。
假设输出设备只存在单个的IO,每次只能输出一个数字,那么如何将75324在屏幕中打印出来?
分析:要输出数字75324,那么只能先输出7,再输出5,依次类推下去。那么要输出75324,必须首先输出7532;要输出7532,必须要先输出753;依次类推下去。
1 //递归实现
2 void Output(Unsigned N)
3 {
4 if(N >= 10)
5 Output(N / 10);
6 OutDigit(N % 10); //输出一个数字
7 }
8
9 //循环实现
10 void Output(Unsigned N)
11 {
12 while(N >= 10)
13 {
14 OutDigit(N % 10);
15 N = N / 10;
16 }
17 OutDigit(N);
18 }
递归与循环
所有的递归都可以用循环实现,但是递归与循环存在不同的应用场合。
递归不适合用于循环次数会很多的情况,因为递归调用的过程中函数一级一级调用,不断产生该函数的副本,占用大量的内存空间。如果数量过于庞大会降低运算效率,甚至使系统崩溃。但是循环不同,循环每次调用,局部变量重新赋值,占用的内存空间不会随着循环的次数的增多而变化。
递归调用能简化代码,使代码看起来很简洁易懂。
递归和循环是从不同的方向去实现。对于例子1来说,递归是从X开始,逐级往下调用,若采用循环的方式,则是从1开始,一直循环到X。对于例子2,也是这样,对于递归的方法是要输出75324,考虑先输出7532,然后依次类推。而循环相反,先输出4,然后输出2,依次类推。找到这个规律,那么我们以后再递归与迭代之间改写时,就更容易找到方法了。
递归与栈的关系
栈是一种数据结构,递归是一种算法,两者本来并没有什么关系,但是由于编译器内部大量采用了stack结构。stack本身具有后进先出的特点这与递归调用的特点不谋而合,因此编译器通过栈来实现递归调用。其实,整个程序的调用都是通过栈来实现的,编译器每调用一个函数就将该函数和的局部变量压入栈中。通过观测VS中的调用堆栈就可以看出。
写个从10输出到1的例程,通过递归实现,来观察一下编译器递归的实现。
1 #include <iostream>
2
3 void fun(int x)
4 {
5 if (x == 0)
6 {
7 std::cout << "end" << std::endl;
8 return;
9 }
10 std::cout << x << std::endl;
11 fun(--x);
12 }
13
14 int main()
15 {
16 fun(10);
17 system("pause");
18 return 0;
19 }
可以看到在VS2012的调用堆栈中fun函数被多次反复压入堆栈中。
树递归
第一次听说树递归是在SICP这本上看到的,我认为提出这个概念的目的是为了让我们避免出现这样的情况,因为它往往违背了递归的第四条准则—合成效益法则。
树递归最经典的例子就是斐波拉契数列了,数学表达式是这样的
递归实现
int fib(int n)
{
if (0 == n)
{
return 0;
}
else if(1 == n)
{
return 1;
}
else
{
return fib(n - 1) + fib(n - 2);
}
}
上述迭代的过程如下,我们可以发现计算fib(5)的过程中,fib(0)、fib(1)、fib(2)等都被重复计算了很多次,因此不是高效的方法。
迭代实现
int fib_iter(int n)
{
if (0 == n)
return 0;
if (1 == n)
return 1;
int ret1 = 1, ret2 = 0, ret;
for (int i = 2; i <= n; i++)
{
ret = ret1 + ret2;
ret2 = ret1;
ret1 =ret;
}
return ret;
}