一、首先我们讲讲递归
- 递归的本质是,某个方法中调用了自身。本质还是调用一个方法,只是这个方法正好是自身而已
- 递归因为是在自身中调用自身,所以会带来以下三个显著特点:
- 调用的是同一个方法
- 因为1,所以只需要写一个方法,就可以让你轻松调用无数次(不用一个个写,你定个n就能有n个方法),所以调用的方法数可能非常巨大
- 在自身中调用自身,是嵌套调用(栈帧无法回收,开销巨大)
- 因为上面2和3两个特点,所以递归调用最大的诟病就是开销巨大,栈帧和堆一起爆掉,俗称内存泄露
- 一个误区,不是因为调用自身而开销巨大,而是嵌套加上轻易就能无数次调用,使得递归可以很容易开销巨大
既然会导致内存泄露如此,那肯定要想办法了,方法很简单,那就是尾递归优化
二、尾递归优化
什么是尾递归:
如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的。当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,因为大多数现代的编译器会利用这种特点自动生成优化的代码。
- 尾递归优化是利用上面的第一个特点“调用同一个方法”来进行优化的
- 尾递归优化其实包括两个东西:1)尾递归的形式;2)编译器对尾递归的优化
- 尾递归的形式
- 尾递归其实只是一种对递归的特殊写法,这种写法原本并不会带来跟递归不一样的影响,它只是写法不一样而已,写成这样不会有任何优化效果,该爆的栈和帧都还会爆
- 具体不一样在哪里
- 前面说了,递归的本质是某个方法调用了自身,尾递归这种形式就要求:某个方法调用自身这件事,一定是该方法做的最后一件事(所以当有需要返回值的时候会是return f(n),没有返回的话就直接是f(n)了)
- 要求很简单,就一条,但是有一些常见的误区
- 这个f(n)外不能加其他东西,因为这就不是最后一件事了,值返回来后还要再干点其他的活,变量空间还需要保留
- 比如如果有返回值的,你不能:乘个常数 return 3f(n);乘个n return n*f(n);甚至是 f(n)+f(n-1)
- 另外,使用return的尾递归还跟函数式编程有一点关系
- 编译器对尾递归的优化
- 上面说了,你光手动写成尾递归的形式,并没有什么卵用,要实现优化,还需要编译器中加入了对尾递归优化的机制
- 有了这个机制,编译的时候,就会自动利用上面的特点一来进行优化
- 具体是怎么优化的:
- 简单说就是重复利用同一个栈帧,不仅不用释放上一个,连下一个新的都不用开,效率非常高(有人做实验,这个比递推比迭代都要效率高)
- 为什么写成尾递归的形式,编译器就能优化了?或者说【编译器对尾递归的优化】的一些深层思想
- 说是深层思想,其实也是因为正好编译器其实在这里没做什么复杂的事,所以很简单
- 由于这两方面的原因,尾递归优化得以实现,而且效果很好
- 因为在递归调用自身的时候,这一层函数已经没有要做的事情了,虽然被递归调用的函数是在当前的函数里,但是他们之间的关系已经在传参的时候了断了,也就是这一层函数的所有变量什么的都不会再被用到了,所以当前函数虽然没有执行完,不能弹出栈,但它确实已经可以出栈了,这是一方面
- 另一方面,正因为调用的是自身,所以需要的存储空间是一毛一样的,那干脆重新刷新这些空间给下一层利用就好了,不用销毁再另开空间
- 有人对写成尾递归形式的说法是【为了告诉编译器这块要尾递归】,这种说法可能会导致误解,因为不是只告诉编译器就行,而是你需要做优化的前半部分,之后编译器做后半部分
- 所以总结:为了解决递归的开销大问题,使用尾递归优化,具体分两步:1)你把递归调用的形式写成尾递归的形式;2)编译器碰到尾递归,自动按照某种特定的方式进行优化编译
举例:
(没有使用尾递归的形式)
def
recsum(x):
if x == 1
:
return
x
else
:
return x + recsum(x - 1)
(使用尾递归的形式)
def tailrecsum(x, running_total=
0):
if x ==
0:
return
running_total
else
:
return tailrecsum(x - 1, running_total + x)
但不是所有语言的编译器都做了尾递归优化。比如C实现了,JAVA没有去实现
回溯就是让计算机自动的去搜索,碰到符合的情况就结束或者保存起来,在一条路径上走到尽头也不能找出解,就回到原来的岔路口,选择一条以前没有走过的路继续探测,直到找到解或者走完所有路径为止。就这一点,回溯和所谓的DFS(深度优先搜索)是一样的。那现在的关键是,怎么实现搜索呢?回溯既然一般使用递归来实现,那个这个递归调用该如何来写呢?我现在的理解就是,进行回溯搜索都会有一系列的步骤,每一步都会进行一些查找。而每一步的情况除了输入会不一样之外,其他的情况都是一致的。这就刚好满足了递归调用的需求。通过把递归结束的条件设置到搜索的最后一步,就可以借用递归的特性来回溯了。因为合法的递归调用总是要回到它的上一层调用的,那么在回溯搜索中,回到上一层调用就是回到了前一个步骤。当在当前步骤找不到一种符合条件情况时,那么后续情况也就不用考虑了,所以就让递归调用返回上一层(也就是回到前一个步骤)找一个以前没有尝试过的情况继续进行。当然有时候为了能够正常的进行继续搜索,需要恢复以前的调用环境。