《编程导论(Java) ·10.3》补充:递归的优化
2016-06-21 12:11
429 查看
递归强大、优雅、易实现...问题是效率和栈溢出(java.lang.StackOverflowError)。
为什么Scheme不需要迭代结构如while、for呢?
在Java编译器不直接支持尾调用优化 tail-call optimization (TCO)的情况下,如何使用lambda表达式的延迟计算或者直接使用流来优化递归?
流会导致Java中迭代的消失吗?
常规的递归代码:
按照SICP1.2.1,我们可以使用状态变量写出尾调用的代码:
Q:为什么Scheme不需要迭代结构如while、for呢?
A:程序员编写尾递归,其解释器支持TCO。
第一步,就是代码中不能够递归调用!因为Java对任何调用会创建新的栈帧。不用迭代结构又不能够递归调用,听起来很高端、很邪恶。不用迭代结构是说坚决不用迭代结构;不递归调用,就像Java对象的按值传递一样——引用按值传递——玩花招,函数sum_TailRec的代码中不直接调用sum_TailRec而是在作为实参的lambda表达式中调用sum_TailRec。对照上面尾调用的private
static long iter(int result,int i,int n)代码,我们希望的代码为:
private static ??1 sum_TailRec(long sum,int i) {
if(i == 0) return ??2(sum);
else return ??3(() -> sum_TailRec(sum+i, i - 1));
}
希望的代码需要两个类型。(1)lambda表达式的目标类型如TailRec,因此??1也就是TailRec;(2)包裹lambda表达式的方法即??2和??3以及它所属的类型。下面逐步实现它们(代码不断的添加)
(1)lambda表达式的目标类型如TailRec<T>
sum_Rec2Iter的辅助方法 long iter(int result,int i,int n)在代码中调用自己,导致栈的增长;sum_Rec4Iter的辅助方法TailRec<Long> sum_TailRec(long sum,int i)在代码中以TailRec作为参数和返回值。设计之初,TailRec是作为MySum的next( TailRec<Long> next)形参考虑的,TailRec作为lambda表达式的 ()
-> sum_TailRec()的占位符。(1)现在,TailRec是辅助方法TailRec<Long> sum_TailRec()的返回值,需要一个方法??4()从TailRec返回一个T(这里为Long)类型的值,这个方法是TailRec计算的核心;取名run或invoke或(2) MySum的baseCase( long value)中,需要TailRec提供标记完成情况的方法和提取结果的方法。
System.out.println(sum_Rec4Iter(10000000));
输出:50000005000000
System.out.println(sum_Rec2Iter(10000000)); //StackOverflowError
注:这里的内容改编自《Functional Programming in Java·CHAPTER 7 Optimizing Recursions》,为了讲解的方便替换了一些方法和类名。阅读该书时注意‘手工模拟TCO’,记住Java编译器不支持尾调用优化,所以要借用作为实参的lambda表达式调用sum_TailRec。
public static void test(){
int n=10000;
System.out.println(sum_iter(n));//
System.out.println(sum_Rec(n));//
System.out.println(sum_Rec2Iter(n));//
System.out.println(sum_Rec4Iter(n));//
System.out.println(sum_Stream(n));
}//50005000
n=100000,
System.out.println(sum_iter(n));// 705082704
System.out.println(sum_Rec(n));// StackOverflowError
System.out.println(sum_Rec2Iter(n));// StackOverflowError
System.out.println(sum_Rec4Iter(n));// 5000050000
System.out.println(sum_Stream(n)); //5000050000
n=1000000000
System.out.println(sum_Rec4Iter(n));// 500000000500000000,慢
System.out.println(sum_Stream(n)); //500000000500000000
阿莲,你能不能够接受
那个从前的for 流会导致Java中迭代的消失吗?
4.memoization and dynamic programming.
为什么Scheme不需要迭代结构如while、for呢?
在Java编译器不直接支持尾调用优化 tail-call optimization (TCO)的情况下,如何使用lambda表达式的延迟计算或者直接使用流来优化递归?
流会导致Java中迭代的消失吗?
1.尾调用优化
Scheme依仗其解释器的TCO,只要程序是尾递归的实现,就可以通过常规的函数调用达到迭代的效果。因此“使得各种复杂的专用迭代结构变成不过是一些语法糖衣了”(SICP1.2.1)(得瑟)。常规的递归代码:
public static long sum_Rec(int i) { return (i == 1)? 1:(i + sum(i - 1)); }注意它的else部分:return前的最后计算为加法。设置断点运行,可以看到方法调用栈的增长。
按照SICP1.2.1,我们可以使用状态变量写出尾调用的代码:
//使用状态变量的递归 public static long sum__Rec2Iter(int n){ return iter(0,0,n); } private static long iter(int result,int i,int n){ if(i>n) return result; else return iter(result+i,i+1,n); }
(define (fact-iter product counter max-count) (if (> counter max-count) product (fact-iter (* counter product) (+ counter 1) max-count))) (define (factorial n) (fact-iter 1 1 n))注意它的else部分:return的是递归函数的调用。在Scheme中它达到迭代的效果,但是Java编译器不支持尾调用优化,sum_2(100000)还是StackOverflowError。
Q:为什么Scheme不需要迭代结构如while、for呢?
A:程序员编写尾递归,其解释器支持TCO。
2.手工模拟TCO
Java编译器不支持TCO,Java中编写尾调用没有什么好处。我们所谓模拟尾调用,其实就是达到不用迭代结构却具有避免StackOverflowError的效果。第一步,就是代码中不能够递归调用!因为Java对任何调用会创建新的栈帧。不用迭代结构又不能够递归调用,听起来很高端、很邪恶。不用迭代结构是说坚决不用迭代结构;不递归调用,就像Java对象的按值传递一样——引用按值传递——玩花招,函数sum_TailRec的代码中不直接调用sum_TailRec而是在作为实参的lambda表达式中调用sum_TailRec。对照上面尾调用的private
static long iter(int result,int i,int n)代码,我们希望的代码为:
private static ??1 sum_TailRec(long sum,int i) {
if(i == 0) return ??2(sum);
else return ??3(() -> sum_TailRec(sum+i, i - 1));
}
希望的代码需要两个类型。(1)lambda表达式的目标类型如TailRec,因此??1也就是TailRec;(2)包裹lambda表达式的方法即??2和??3以及它所属的类型。下面逐步实现它们(代码不断的添加)
(1)lambda表达式的目标类型如TailRec<T>
package java8.recursion; @FunctionalInterface public interface TailRec<T> { TailRec<T> abcd();//lambda表达式不在乎方法名,随便取名 }2)包裹lambda表达式的方法即??2和??3以及它所属的类型。方法??2和??3按照递归的通则,取名baseCase和next。注意:这个MySum可以作为TailRec<T>的配套工具类,目前我们把它作为Add的专用工具。
package java8.recursion; public class Add{ public static long sum_Rec2Iter(int n){ return iter(0,0,n); } private static long iter(int result,int i,int n){ if(i>n) return result; else return iter(result+i,i+1,n); }////////////////////////////////////////////////////////////////////////////////上面的代码 用于比较 public static long sum_Rec4Iter(int n){ return sum_TailRec(0,n).??4(); } private static TailRec<Long> sum_TailRec(long sum,int i) { if(i == 0) return MySum.baseCase(sum); else return MySum.next(() -> sum_TailRec(sum+i, i - 1)); } static class MySum { public static TailRec<Long> next(TailRec<Long> next) { return next; } public static TailRec<Long> baseCase(long value) { return new TailRec<Long>() { //??? }; } } }上面代码中的??,我们没有搞定。先比较sum_Rec2Iter和sum_Rec4Iter,了解代码中的??,再回头完成TailRec<T>的其他代码。
sum_Rec2Iter的辅助方法 long iter(int result,int i,int n)在代码中调用自己,导致栈的增长;sum_Rec4Iter的辅助方法TailRec<Long> sum_TailRec(long sum,int i)在代码中以TailRec作为参数和返回值。设计之初,TailRec是作为MySum的next( TailRec<Long> next)形参考虑的,TailRec作为lambda表达式的 ()
-> sum_TailRec()的占位符。(1)现在,TailRec是辅助方法TailRec<Long> sum_TailRec()的返回值,需要一个方法??4()从TailRec返回一个T(这里为Long)类型的值,这个方法是TailRec计算的核心;取名run或invoke或(2) MySum的baseCase( long value)中,需要TailRec提供标记完成情况的方法和提取结果的方法。
package java8.recursion; import java.util.stream.Stream; @FunctionalInterface public interface TailRec<T> { TailRec<T> abcd();//lambda表达式不在乎方法名,随便取名 default boolean isComplete() { return false; } default T result() { throw new Error("not implemented"); } default T run() { return Stream.iterate(this, TailRec::abcd) .filter(TailRec::isComplete) .findFirst() .get() .result(); } }相应完成baseCase( long value)的代码
public static TailRec<Long> baseCase( long value) { return new TailRec<Long>() { @Override public boolean isComplete() { return true; } @Override public Long result() { return value; } @Override public TailRec<Long>abcd() { throw new Error("not implemented"); } }; }
System.out.println(sum_Rec4Iter(10000000));
输出:50000005000000
System.out.println(sum_Rec2Iter(10000000)); //StackOverflowError
注:这里的内容改编自《Functional Programming in Java·CHAPTER 7 Optimizing Recursions》,为了讲解的方便替换了一些方法和类名。阅读该书时注意‘手工模拟TCO’,记住Java编译器不支持尾调用优化,所以要借用作为实参的lambda表达式调用sum_TailRec。
3.流
上面的例子,TailRec计算的核心代码使用了流,费那么大的劲,真的好吗?public static long sum_Stream(int n){ return LongStream.rangeClosed(0, n).sum(); }
public static void test(){
int n=10000;
System.out.println(sum_iter(n));//
System.out.println(sum_Rec(n));//
System.out.println(sum_Rec2Iter(n));//
System.out.println(sum_Rec4Iter(n));//
System.out.println(sum_Stream(n));
}//50005000
n=100000,
System.out.println(sum_iter(n));// 705082704
System.out.println(sum_Rec(n));// StackOverflowError
System.out.println(sum_Rec2Iter(n));// StackOverflowError
System.out.println(sum_Rec4Iter(n));// 5000050000
System.out.println(sum_Stream(n)); //5000050000
n=1000000000
System.out.println(sum_Rec4Iter(n));// 500000000500000000,慢
System.out.println(sum_Stream(n)); //500000000500000000
阿莲,你能不能够接受
那个从前的for 流会导致Java中迭代的消失吗?
4.memoization and dynamic programming.
相关文章推荐
- 【java】java实现动态时钟并可以设置闹钟
- 关于一对多的关联关系 spring json出现死循环的问题
- Spring-boot生成可执行jar包
- 遍历Map的四种方法
- 事务的定义,事务的作用以及Spring事务原理
- java中方法参数的传递机制
- JAVA和C#之间SOCKET通信的问题
- mac设置java环境变量, 使用oh-my-zsh
- [Java]邮件发送
- Java中常用的类型转换(推荐)
- java教程:Java开发桌面应用程序的优劣
- Java垃圾回收机制
- 用java写的马踏棋盘算法
- java垃圾回收
- java 文件加密 用的是md5值进行文件加密
- Java 并发工具包 java.util.concurrent 用户指南
- Java NIO:浅析I/O模型
- java基础第十九天_QQ、多线程下载
- xml文件数据和java对象互转
- java基础第十八天_项目完善