您的位置:首页 > 编程语言 > Java开发

《编程导论(Java) ·10.3》补充:递归的优化

2016-06-21 12:11 429 查看
递归强大、优雅、易实现...问题是效率和栈溢出(java.lang.StackOverflowError)

为什么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.



 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: