递归简介
计算机思维和人的思维最大的不同就是递归。我们日常思考问题方式是正向的递推(iterative),计算机是逆向的递归思维。要想真正理解的计算机解决问题的精髓,递归是必须要学的。
递推是人本能的正向思维,比如数数,从1、2、3一直数到100,这就是典型的递推。如果用递推的方法来计算一个数的阶乘,比如5! = 1×2×3×4×5,一个很自然的做法就是从小到大一个个乘起来。
但是,对于一些难题,递推方法并不好用,比如经典的八皇后问题。
八皇后问题
如何能够在 8×8 的国际象棋棋盘上放置八个皇后,使得任何一个皇后都无法直接吃掉其他的皇后?为了达到此目的,任两个皇后都不能处于同一条横行、纵行或斜线上。
按照我们通常的递推思维,可以在第一行摆好一个棋子,它当然不会和任何棋子冲突,然后第二行摆一个,保证它和第一个棋子没有冲突,然后再摆第三行的,以此类推。
需要指出的是,没有计算机之前找到所有可能的摆放是非常困难的。如果你能在5分钟内找到一个答案,就算非常快了。**大数学家高斯穷其一生只找到76种方案,而八皇后独立解共有92种。**当然,有了计算机,很快就能找到它的全部92种结果。
要更好的求解这个问题,就要用递归的思维。计算机计算阶乘,比如说计算5!,计算机就会把它变成 5×4!,4!不知道,没事,同样转化成4×3!,以此类推。最后到了1!(递归基),计算机知道它等于自身,就不接着往下扩展。接下来,计算机倒推回去所有结果,1!知道了,2!,然后3!,4!,5!。
递归的过程,从上向下层层展开,再从下到上一步步回溯。这和栈这种数据结构是一致的,即先进后出,后进先出,但栈清空的时候,计算就完成了。不过,这带来了新的问题。
递归执行的过程
要理解递归带来的问题,就要理解递归的工作过程,有必要先来看看C语言中函数执行的方式。基于这点,我们需要了解一点关于C程序在内存中的组织方式。基本上来说一个可执行程序由四个区域组成:代码段(.text)、静态数据区(.data)、堆(heap)和栈(stack)。
bss(block started by symbol)由符号开始的块。
- 代码段包含程序运行时所执行的机器指令。
- 静态数据区包含在程序生命周期内一直持久的数据,比如全局变量和静态局部变量。
- 堆包含程序运行时动态分配的存储空间,比如malloc分配的内存。
- 栈包含函调用的信息。
注意:按照惯例,堆的增长方向从程序低地址到高地址向上增长,而栈的增长方向刚好相反。(实际情况可能不是这样,和CPU的体系结构有关)。另外,这里的堆和数据结构的堆没有太大的关系。
当C程序中调用了一个函数时,栈中会分配一块空间来保存与这个调用相关的信息。每一个调用都被当作是活跃的。栈上的那块存储空间称为活跃记录,或者称为栈帧。栈帧由5个区域组成:输入参数、返回值空间、计算表达式用到的临时存储空间、函数调用时保存的状态信息以及输出参数。
输入参数是传递到活跃记录中的参数;输出参数是传递给在活跃记录中调用的函数。一个活跃记录中的输出参数就成为栈中下一个活跃记录的输入参数(到了1!=1,1就作为输出参数输出到调用1!的2!)。函数调用产生的活跃记录将一直存在于栈中直到这个函数调用结束。
// 阶乘的递归实现
int fact(int n) {
if (n < 0) return 0;
else if (n == 0) return 1;
else if (n == 1) return 1;
else
return n * fact(n