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

递归强大、优雅、易实现...问题是效率和栈溢出(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.

 


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值