递归适用的范畴:
既然的递归的思想是把问题分解成规模更小但和原问题有着相同解法的问题,那是不是所有具有这样特性的问题都能用递归来解决呢?答案是否定的。除了这个特性,能用递归解决的问题还必须具有一个特性:存在一种简单情境,能让递归在简单情境下退出,也就是要有一个递归出口。总结一下就是,能用递归解决的问题,必须满足以下两个条件:
- 一个问题能够分解成规模更小,且与原问题有着相同解的问题;
- 存在一个能让递归调用退出的简单出口
递归导致一个函数反复调用自己,我们知道函数调用是通过一个工作栈来实现的,在大多数机器上,每次调用函数时大致要做三个工作:调用前先保存寄存器,并在返回时恢复;复制实参;程序必须转向一个新位置执行。其中,具体要保存的内容包括:局部变量、形参、调用函数地址、返回值。那么,如果递归调用N次,就要分配N*局部变量、N*形参、N*调用函数地址、N*返回值。这势必是影响效率的。在C++中,inline函数就是为了改善函数调用所带来的效率问题而做的一种优化。递归就是利用系统的堆栈保存函数当中的局部变量来解决问题的,说白了就是利用堆栈上的一堆指针指向内存中的对象,并且这些对象一直不被释放,直到遇到简单情境时才一一出栈释放,所以总的开销就很大。栈空间都是有限的,如果没有设置好出口,或者调用层级太多,有可能导致栈空间不够,而出现栈溢出的问题。为了防止无穷递归现象,有些语言是规定栈的长度的,比如python语言规定堆栈的长度不能超过1000。还有就是当规模很大的时候,尽量不使用递归,而改为非递归的形式,或者优化成尾递归的形式(后面讲)。
尾递归:递归转尾递归
有些简单的递归问题,可以不借助堆栈结构而改成循环的非递归问题。这里说的简单,是指可以通过一个简单的数学公式来进行推导,如阶乘问题和斐波那契数列数列问题。这些可以转换成循环结构的递归问题,一般都可以优化成尾递归的形式。很多编译器都能够将尾递归的形式优化成循环的形式。那什么是尾递归呢?
我们先讨论一个概念:尾调用。顾名思义,一个函数的调用返回都集中在尾部,单个函数调用就是最简单的尾调用。如果两个函数调用:函数A调用函数B,当函数B返回时,函数A也返回了。同理,多个函数也是同样的情况。这就相当于执行完函数B后,函数A也执行完了,从数据结构上看,在执行函数B时,函数A的堆栈已经大部分被函数B修改或替换了,所以,栈空间没有递增或者说递增的程度没有普通递归那么大。这样在效率上就大大降低了。
递归转非递归
不可否认,递归便于算法的理解,代码精炼,容易阅读,但递归的效率往往是我们最在意的问题。如果能用循环解决递归问题,就尽可能使用循环;如果用循环解决不了,或者能解决但代码很冗长且晦涩,则尽可能使用递归。另外,有些低级语言(如汇编)一般不支持递归。很多时候我们需要把递归转化成非递归形式,这不仅能让我们加深对递归的理解,而且能提升问题解决的效率。这时候就需要掌握一些转化的技巧,便于我们在用到时信手捏来。
一般来说,递归转化为非递归有两种情况:
第一种方法:借助堆栈模拟递归的执行过程。这种方法几乎是通用的方法,因为递归本身就是通过堆栈实现的,我们只要把递归函数调用的局部变量和相应的状态放入到一个栈结构中,在函数调用和返回时做好push和pop操作,就可以了(后面有一个模拟快排的例子)。
第二种方法:借助堆栈的循环结构算法。这种方法常常适用于某些局部变量有依赖关系,且需要重复执行的场景,例如二叉树的遍历算法,就采用的这种方法。
更一般的递归,想要转化为非递归,就需要模拟栈的行为。
首先需要自己建个栈。栈保存的东西是一个记录,包括所有局部变量的值,执行到的代码位置。
首先讲局部变量初始化位一开始的状态,然后进入一个循环
执行代码时,遇到递归,就制作状态压栈保存,然后更新局部变量进入下一层。
如果一个调用结束了,就要返回上层状态。直接讲栈里的记录弹出,拿来更新当前状态即可。
某个调用结束时如果栈为空则所有调用都结束,退出主循环。
下面的代码给出中序遍历的非递归实现:
#include <stack> using namespace std; typedef struct node { node* left; node* right; int x; }; struct record{ node* a; int state; record(node* a, int state) :a(a), state(state){} }; //中序遍历的非递归实现 void non_recursive_inorder(node* root){ stack<record> s; node* cur = root; //初始化状态 int state = 0; while (1){ if (!cur){ //如果遇到null结点,返回上一层 if (cur == root)break;//如果没有上一层,退出循环 cur = s.top().a; state = s.top().state; //返回上层状态 s.pop(); } else if (state == 0){ //状态位0,执行第一个递归inorder(cur->left); s.push(record(cur, 1));//保存本层状态 cur = cur->left; //更新到下层状态 state = 0; } else if (state == 1){ //状态为1,执行print和inorder(cur->right) printf("%d ", cur->x); s.push(record(cur, 2)); //保存本层状态 cur = cur->right; //进入下层状态 state = 0; } else if (state == 2){ //状态2,函数结束,返回上层状态 if (cur == root)break; //初始结点的退出状态,遍历结束 cur = s.top().a; //返回上层状态 state = s.top().state; s.pop(); } } putchar(10); }
最后,通过一个用堆栈模拟快排的例子来结束本文。通过一个结构体record来记录函数的局部变量和相应的状态。