递归算法的优化思路和CPS
来源:互联网 发布:淘宝网运动鞋女 编辑:程序博客网 时间:2024/05/20 09:21
递归算法的本质是定义一个规则, 让程序根据规则去帮你完成一件事。然而递归被吐槽的最多的事它感人的性能和爆栈的可能性,有必要整理一下如何对递归程序做优化。
这里先以Fibonacci为例。
Scala代码:
def fib1(n: BigInt): BigInt = { if(n == 0) 0 else if(n == 1) 1 else fib1(n - 1) + fib1(n - 2) }
以上是Fibonacci的一般递归算法, 几乎就是把定义抄了上去, 然后程序在层层递归调用中不知不觉把结果算了出来。当n的数值过大时, 由于递归过深,临时变量塞满了栈空间导致stackoverflow. 有两种比较直观的解决方案: 改写成迭代或者尾递归。
Scala代码
def fib4(n: BigInt): BigInt = { var n0 = 0 var n1 = 1 var i = 1 val l = while(i <= n) { var tmp = n1 n1 = n0 + n1 n0 = tmp i = i + 1 } n0 }以上是用迭代的方式实现Fibonacci。迭代是一种递推的方法, 核心思想是自下而上进行计算, 并将中间结果保存下来, 以避免递归算法对同一个值重复计算的复杂性。 直观的认识是, 我们可以建立一个大小为n的数组, 把f(1) ...f(n - 1)的结果都保存下来。然而经过观察我们发现, 在求f(n)的时候, 我们只需要用到f(n - 1)和f(n - 2), 之前的结果完全可以抛弃。因此这里定义了n1, n2来保存中间结果, 每次循环重新计算n2的值, 并将老的n2赋值给n1。经过这种优化以后, 空间复杂度从o(n)降到了o(1)。 但这只是个特例, 递归改写成循环并不一定能减少空间复杂度。
这里唯一要注意的是边界值。 当循环以 <= n作为条件时, n = 0时不需要进入循环, n = 1时需要计算一次, 所以此处i = 0, 末尾返回较小的值
通过上面的分析, 我们发现通过循环可以将空间复杂度减少为o(1)。这意味着这个递归能在不构造栈的情况下被改写成尾递归。
def fib3(n: BigInt): BigInt = { def fibInner(n: BigInt, acc1: BigInt, acc2: BigInt): BigInt = { if(n == 0) acc1 else { fibInner(n - 1, acc2, acc1 + acc2) } } fibInner(n, 0, 1) }上面是尾递归算法。 可以看出尾递归的思路其实是迭代的思路, acc1和acc2代表迭代算法中的n0和n1, 每次迭代将acc1赋值为acc2, 将acc2赋值为acc1 + acc2, 和迭代算法如出一辙, 最后返回acc1,对应n0。
由上面分析可以得出一个简单结论:尾递归是迭代算法的一种变相实现, 如果我们推导不出迭代算法, 那么尾递归也同样推导不出。 同时形如f(n) = f(n - x) .... f(n - y)的单向递归都可以套用以上模式改写成循环或尾递归, acc(临时变量)的个数取决于x和y之间的跨度。
此外, 我们还可以使用CPS(Continuation-Passing Style)的方式来改写函数使其成为尾递归。
def fibCont(n: BigInt, continuation: BigInt => BigInt = (x => x)): BigInt = { if (n < 2) continuation(n) else { fibCont(n - 1, r1 => fibCont(n - 2, r2 => continuation(r1 + r2))); } }Currying之后的函数如下
def fibCont(n: BigInt)(continuation: BigInt => BigInt = (x => x)): BigInt = { if (n < 2) continuation(n) else { fibCont(n - 1)(r1 => fibCont(n - 2)(r2 => continuation(r1 + r2))); } }Fibonacci的CPS函数可以理解为:要计算f(n), 就要先计算f(n - 1), 再将f(n - 1)的结果放到continuation函数中做接下来的事。而接下来的事无非就是计算f(n - 2), 再将两个数相加的结果作为下一层continuation的参数。
参考了很多大神的博客和资料以及自己的亲身实践以后, 得出如下结论: CPS的方法可以用上述模式把任何递归改写成使用continuation函数的方法, 但:1不能减少程序的复杂度, 2这种形式的尾递归不一定能被编译器优化(如Scala, 在上述函数前加上@tailrec无法通过编译)。CPS体现的是函数式编程的一种思想, 即把变量的值变换替换成函数的定义变换。换句话说, 上述的continuation在层层递归中函数体越拼越长, 到了递归出口才传入真正的参数来计算。 至于为什么空间复杂度并没有减小,是因为这种方式节省了临时变量压栈的空间,是以加长匿名函数体为代价的。 当函数体越来越长时, 该爆栈还是得爆栈。
- 递归算法的优化思路和CPS
- 尾递归、CPS等几种求阶乘的算法
- 关于全排列的递归算法和非递归算法及内存优化
- 用户权限树的建立及递归算法思路原则
- 数据库优化的一些概念和思路
- 锁优化的思路和方法
- 性能优化的思路和步骤
- 递归使用的思路
- 面试中遇到递归算法题别慌--常见递归算法题的解题思路
- 面试中遇到递归算法题别慌--常见递归算法题的解题思路
- 【Mysql 优化 6】mysql优化的内容和思路
- 递归算法和非递归算法的difference和转换
- 递归算法和非递归算法的区别和转换
- 排列和组合简单的递归思路以及C++实现
- EM算法的整体思路和理解
- cps和联合登录
- 递归算法和循环算法的转换
- 母牛生仔的递归算法和非递归算法。
- 线程加入,休眠,中断,礼让操作
- CentOS7 安装 zookeeper
- okhttp用法
- hdu5492(递推+数学)
- Spring Validation(使用Hibernate Validator)
- 递归算法的优化思路和CPS
- iframe加载顺序导致数据访问出现问题
- leetcode : jumpgame
- MAC地址和IP地址
- TYVJ1415 差分约束
- TPL——开始一个Task
- nodejs操作MSSQL两种方式--笔记
- 设计模式学习笔记——观察者模式
- pat1014 Waiting in line