[Java 8] (9) Lambda表达式对递归的优化(下) - 使用备忘录模式(Memoization Pattern) .

使用备忘录模式(Memoization Pattern)提高性能

这个模式说白了,就是将需要进行大量计算的结果缓存起来,然后在下次需要的时候直接取得就好了。因此,底层只需要使用一个Map就够了。

但是需要注意的是,只有一组参数对应得到的是同一个值时,该模式才有用武之地。

在很多算法中,典型的比如分治法,动态规划(Dynamic Programming)等算法中,这个模式运用的十分广泛。 以动态规划来说,动态规划在求最优解的过程中,会将原有任务分解成若干个子任务,而这些子任务势必还会将自身分解成更小的任务。因此,从整体而言会有相当多的重复的小任务需要被求解。显然,当输入的参数相同时,一个任务只需要被求解一次就好了,求解之后将结果保存起来。待下次需要求解这个任务时,会首先查询这个任务是否已经被解决了,如果答案是肯定的,那么只需要直接返回结果就行了。

就是这么一个简单的优化措施,往往能够将代码的时间复杂度从指数级的变成线性级。

以一个经典的杆切割问题(Rod Cutting Problem)(或者这里也有更加正式的定义:维基百科)为例,来讨论一下如何结合Lambda表达式来实现备忘录模式。

首先,简单交代一下这个问题的背景。

一个公司会批发一些杆(Rod),然后对它们进行零售。但是随着杆的长度不同,能够卖出的价格也是不同的。所以该公司为了将利润最大化,需要结合长度价格信息来决定应该将杆切割成什么长度,才能实现利润最大化。

比如,下面的代码:

final List<Integer> priceValues = Arrays.asList(2, 1, 1, 2, 2, 2, 1, 8, 9, 15);

表达的意思是:长度为1的杆能够卖2元,长度为2的杆能够卖1元,以此类推,长度为10的杆能够卖15元。

当需要被切割的杆长度为5时,存在的切割方法多达16种(2^(5 - 1))。如下所示:

针对这个问题,在不考虑使用备忘录模式的情况下,可以使用动态规划算法实现如下:

public int maxProfit(final int length) {
    int profit = (length <= prices.size()) ? prices.get(length - 1) : 0;
    for(int i = 1; i < length; i++) {
        int priceWhenCut = maxProfit(i) + maxProfit(length - i);
        if(profit < priceWhenCut) profit = priceWhenCut;
    }
    return profit;
}

而从上面的程序可以发现,有很多重复的子问题。对这些重复的子问题进行不断纠结,损失了很多不必要的性能。分别取杆长为5和22时,得到的运行时间分别为:0.001秒和34.612秒。可见当杆的长度增加时,性能的下降时非常非常显著的。

因为备忘录模式的原理十分简单,因此实现起来也很简单,只需要在以上maxProfit方法的头部加上Map的读取操作并判断结果就可以了。但是这样做的话,代码的复用性会不太好。每个需要使用备忘录模式的地方,都需要单独写判断逻辑,那么有没有一种通用的办法呢?答案是肯定的,通过借助Lambda表达式的力量可以轻易办到,以下代码我们假设有一个静态方法callMemoized用来通过传入一个策略和输入值,来求出最优解:

public int maxProfit(final int rodLenth) {
    return callMemoized(
        (final Function<Integer, Integer> func, final Integer length) -> {
        int profit = (length <= prices.size()) ? prices.get(length - 1) : 0;
        for(int i = 1; i < length; i++) {
            int priceWhenCut = func.apply(i) + func.apply(length - i);
            if(profit < priceWhenCut) profit = priceWhenCut;
        }
        return profit;
        }, rodLenth);
}

让我们仔细分析一下这段代码的意图。首先callMemoized方法接受的参数类型是这样的:

public static <T, R> R callMemoized(final BiFunction<Function<T,R>, T, R> function, final T input)

BiFunction类型的参数function实际上封装了一个策略,其中有三个部分:

  1. Function:通过传入参数T,来得到解答R。这一点从代码int priceWhenCut = func.apply(i) + func.apply(length - i)很明显的就能够看出来。可以把它想象成一个备忘录的入口。
  2. T:代表求解问题时需要的参数T。
  3. R:代表问题的答案R。

以上的T和R都是指的类型。

下面我们看看callMemoized方法的实现:

public class Memoizer {
    public static <T, R> R callMemoized(final BiFunction<Function<T,R>, T, R> function, final T input) {
        Function<T, R> memoized = new Function<T, R>() {
            private final Map<T, R> store = new HashMap<>();
            public R apply(final T input) {
                return store.computeIfAbsent(input, key -> function.apply(this, key));
            }
        };

        return memoized.apply(input);
    }
}

在该方法中,首先声明了一个匿名Function函数接口的实现。其中定义了备忘录模式的核心---Map结构。 然后在它的apply方法中,会借助Java 8中为Map接口新添加的一个computeIfAbsent方法来完成下面的逻辑:

  1. 通过传入的key检查(在以上代码中是input)对应的值是否存在于备忘录的底层Map中
  2. 如果存在,跳转到步骤4
  3. 如果不存在,根据computeIfAbsent的第二个参数(是一个Lambda表达式)来计算得到key对应的value
  4. 返回得到的value

具体到该方法的源码:

default V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
    Objects.requireNonNull(mappingFunction);
    V v;
    if ((v = get(key)) == null) {
        V newValue;
        if ((newValue = mappingFunction.apply(key)) != null) {
            put(key, newValue);
            return newValue;
        }
    }

    return v;
}

也可以很清晰地看出以上的几个步骤是如何体现在代码中的。

关键的地方就在于第三步,如果不存在对应的value,那么需要调用传入的Lambda表达式进行求解。以上代码传入的是key -> function.apply(this, key),这里的this使用的十分巧妙,它实际上指向的就是这个用于容纳Map结构的匿名Function实例。它作为第一个参数传入到算法策略中,同时需要求解的key被当做第二个参数传入到算法策略。这里所谓的算法策略,实际上就是在调用callMemoized方法时,传入的形式为BiFunction<Function<T,R>, T, R>的参数。

因此,所有的子问题仅仅会被求解一次。在得到子问题的答案之后,答案会被放到Map数据结构中,以便将来的使用。这就是借助Lambda表示实现备忘录模式的方法。

以上的代码可能会显得有些怪异,这很正常。在你反复阅读它们后,并且经过自己的思考能够重写它们时,也就是你对Lambda表达式拥有更深理解之时。

使用备忘录模式后,杆长仍然取5和22时,得到的运行时间分别为:0.050秒和0.092秒。可见当杆的长度增加时,性能并没有如之前那样下降的很厉害。这完全是得益于备忘录模式,此时所有的任务都只会被运行一次。

时间: 2024-08-25 15:02:05

[Java 8] (9) Lambda表达式对递归的优化(下) - 使用备忘录模式(Memoization Pattern) .的相关文章

[Java 8] Lambda表达式对递归的优化(下) - 使用备忘录模式(Memoization Pattern)

使用备忘录模式(Memoization Pattern)提高性能 这个模式说白了,就是将需要进行大量计算的结果缓存起来,然后在下次需要的时候直接取得就好了.因此,底层只需要使用一个Map就够了. 但是需要注意的是,只有一组参数对应得到的是同一个值时,该模式才有用武之地. 在很多算法中,典型的比如分治法,动态规划(Dynamic Programming)等算法中,这个模式运用的十分广泛. 以动态规划来说,动态规划在求最优解的过程中,会将原有任务分解成若干个子任务,而这些子任务势必还会将自身分解成更

[Java 8] (8) Lambda表达式对递归的优化(上) - 使用尾递归 .

递归优化 很多算法都依赖于递归,典型的比如分治法(Divide-and-Conquer).但是普通的递归算法在处理规模较大的问题时,常常会出现StackOverflowError.处理这个问题,我们可以使用一种叫做尾调用(Tail-Call Optimization)的技术来对递归进行优化.同时,还可以通过暂存子问题的结果来避免对子问题的重复求解,这个优化方法叫做备忘录(Memoization). 本文首先对尾递归进行介绍,下一票文章中会对备忘录模式进行介绍. 使用尾调用优化 当递归算法应用于大

[Java 8] Lambda表达式对递归的优化 - 使用尾递归

递归优化 很多算法都依赖于递归,典型的比如分治法(Divide-and-Conquer).但是普通的递归算法在处理规模较大的问题时,常常会出现StackOverflowError.处理这个问题,我们可以使用一种叫做尾调用(Tail-Call Optimization)的技术来对递归进行优化.同时,还可以通过暂存子问题的结果来避免对子问题的重复求解,这个优化方法叫做备忘录(Memoization). 本文首先对尾递归进行介绍,下一票文章中会对备忘录模式进行介绍. 使用尾调用优化 当递归算法应用于大

java 8 中lambda表达式学习

转自 http://blog.csdn.net/renfufei/article/details/24600507 http://www.jdon.com/idea/java/10-example-of-lambda-expressions-in-java8.html Lambda表达式的语法基本语法:(parameters) -> expression或(parameters) ->{ statements; } 下面是Java lambda表达式的简单例子: // 1. 不需要参数,返回值

Java 终于有 Lambda 表达式啦~Java 8 语言变化&mdash;&mdash;Lambda 表达式和接口类更改【转载】

原文地址 en cn 下载 Demo Java? 8 包含一些重要的新的语言功能,为您提供了构建程序的更简单方式.Lambda 表达式 为内联代码块定义一种新语法,其灵活性与匿名内部类一样,但样板文件要少得多.接口更改使得接口可以添加到现有接口中,同时又不会破坏与现有代码的兼容性.本文将了解这些更改是如何协同工作的. Java 8 的最大变化在于添加了对 lambda 表达式 的支持.Lambda 表达式是可按引用传递的代码块.类似于一些其他编程语言中的闭包:它们是实现某项功能的代码,可接受一个

在Android中使用Java 8的lambda表达式

译自http://www.coshx.com/blog/2015/03/23/use-java-8s-lambda-functions-in-android/ 在Android中使用Java 8的lambda表达式 作为一名Java开发者,或许你时常因为信息的封闭性而产生许多的困扰.幸运的是:Java's 8th version introduced lambda functions给我们带来了好消息:然而,这咩有什么卵用,在android上面,我们仍旧只能使用Java7. 那么现在情况如何?哈

[Java 8] 使用Lambda表达式进行设计

使用Lambda表达式进行设计 在前面的几篇文章中,我们已经见识到了Lambda表达式是如何让代码变的更加紧凑和简洁的. 这一篇文章主要会介绍Lambda表达式如何改变程序的设计,如何让程序变的更加轻量级和简洁.如何让接口的使用变得更加流畅和直观. 使用Lambda表达式来实现策略模式 假设现在有一个Asset类型是这样的: public class Asset { public enum AssetType { BOND, STOCK }; private final AssetType ty

【转】Java 8十个lambda表达式案例

1. 实现Runnable线程案例 使用() -> {} 替代匿名类: //Before Java 8: new Thread(new Runnable() { @Override public void run() { System.out.println("Before Java8 "); } }).start(); //Java 8 way: new Thread( () -> System.out.println("In Java8!") ).s

Java里的lambda表达式

在上一篇文章<Java里的函数式接口>介绍了关于函数式接口的内容,那么本文基于函数式接口来继续学习lambda表达式. 语法结构 Runnable runnable = () -> System.out.println("Runnable Instance"); 这种使用箭头符号->分开参数列表和方法体的语法就是lambda表达式.在Java 里没有和lambda 表达式对应的类定义或者代码库,lambda表达式总是在定义后赋值给某个函数式接口类型变量. 一个