阅读目录:
一个函数直接或间接的调用自身,这个函数即可叫做递归函数。
递归最重要的是边界条件,这个边界是整个递归的终止条件。
static int RecFact(int x) { if (x == 0) return 1; return x * RecFact(x - 1); } RecFact(10);
上面是个经典阶乘函数的实现。这里分2步:
这里的x==0就是我们的边界条件(即终止条件),也有的依赖外部变量为边界。
如果一个递归函数没有边界,也就无法停止(无限循环至内存溢出),当然这样也没什么意义。
RecFact调用堆栈:
常见使用场景:
当边界不明确的时候,递归就很容易出现溢出问题。
在阶乘过程中,堆栈需要保存每次(RecFact)调用的返回地址及当时所有的局部变量状态,期间堆栈空间是无法释放的(即容易出现溢出)。
为了优化堆栈占用问题,从而提出尾递归优化的办法。
static void Recurse(int x) { Console.WriteLine(x); if (x == 10) return; Recurse(x + 1); } Recurse(0);
上面这个递归和阶乘那个区别在于没有返回值,也就是说堆栈可以不用保存上一次的返回地址/状态值,这就是尾递归优化的思想。
尾递归可以解决递归过深而引起的溢出问题,因为递归期间堆栈是可以释放/再利用的。
尾递归优化,看起来是蛮美好的,但在net中却有点乱糟糟的感觉。
我们执行 Recurse(0)(x==1000000) 得出如下结论:
C#/64位/Release是有JIT编译器进行尾递归优化的(非C#编译器优化)。
C#/32位或C#/Debug模式中JIT是不进行优化的。
1、 简单的尾递归优化成while循环,如下:
let rec Recurse(x) = if (x = 1000) then true else Recurse(x + 1)
(方便演示C#呈现)优化成:
public static bool Recurse(int x) { while (x != 0x3e8) { x++; } return true; }
2、 复杂的尾递归,F#编译会生成IL指令Tail进行优化,如下:
let Recurse2 a cont = cont (a + 1)
优化成:
如何定义复杂的尾递归呢?通常是后继传递模式(CPS)。
F#中在debug模式下,需要在编译时配置:
在C#语言(过程式编程思想)中,优先考虑的是循环,而不是递归/尾递归。 但在函数式编程思想当中,递归/尾递归使用则是主流用法,就像在C#使用循环一样。
http://volgarev.me/blog/62412678347
http://stackoverflow.com/questions/15864670/generate-tail-call-opcode