使用备忘录模式(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秒。可见当杆的长度增加时,性能并没有如之前那样下降的很厉害。这完全是得益于备忘录模式,此时所有的任务都只会被运行一次。

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

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

    递归优化 很多算法都依赖于递归,典型的比如分治法(Divide-and-Conquer).但是普通的递归算法在处理规模较大的问题时,常常会出现StackOverflowError.处理这个问题,我们可 ...

  2. java 8 中lambda表达式学习

    转自 http://blog.csdn.net/renfufei/article/details/24600507 http://www.jdon.com/idea/java/10-example-o ...

  3. Lambda 表达式,Java中应用Lambda 表达式

    一.Lambda 表达式 简单来说,编程中提到的 lambda 表达式,通常是在需要一个函数,但是又不想费神去命名一个函数的场合下使用,也就是指匿名函数. 链接:知乎 先举一个普通的 Python 例 ...

  4. Python爬虫与数据分析之进阶教程:文件操作、lambda表达式、递归、yield生成器

    专栏目录: Python爬虫与数据分析之python教学视频.python源码分享,python Python爬虫与数据分析之基础教程:Python的语法.字典.元组.列表 Python爬虫与数据分析 ...

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

    原文地址 en cn 下载 Demo Java™ 8 包含一些重要的新的语言功能,为您提供了构建程序的更简单方式.Lambda 表达式 为内联代码块定义一种新语法,其灵活性与匿名内部类一样,但样板文件 ...

  6. Java 8中Lambda表达式默认方法的模板方法模式,你够了解么?

    为了以更简单的术语描述模板方法,考虑这个场景:假设在一个工作流系统中,为了完成任务,有4个任务必须以给定的执行顺序执行.在这4个任务中,不同工作流系统的实现可以根据自身情况自定义任务的执行内容. 模板 ...

  7. 备忘录模式-Memento Pattern(Java实现)

    备忘录模式-Memento Pattern Memento备忘录设计模式是一个保存另外一个对象内部状态拷贝的对象,这样以后就可以将该对象恢复到以前保存的状态. 本文中的场景: 有一款游戏可以随时存档, ...

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

    作为一名Java开发者,或许你时常因为缺乏闭包而产生许多的困扰.幸运的是:Java's 8th version introduced lambda functions给我们带来了好消息;然而,这咩有什 ...

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

    1. 实现Runnable线程案例 使用() -> {} 替代匿名类: //Before Java 8: new Thread(new Runnable() { @Override public ...

随机推荐

  1. laya在微信小游戏中加载BitmapFont失效的问题

    发布为微信小游戏后,在微信工具中测试时总是提示加载retry to load TheRed.fnt,并以error告终.由于没有任何出错信息,无奈之下只好阅读源码.对BitmapFont的处理分为两个 ...

  2. 【HDU 2089】 不要62

    [题目链接] 点击打开链接 [算法] 数位DP 和上一题 : HDU3555很像 [代码] #include<bits/stdc++.h> using namespace std; #de ...

  3. ng 表单提交验证

    http://www.runoob.com/try/try.php?filename=try_ng_validate

  4. [yii2]Module的Namespace和控制器位置

    namespace和目录对应,否则无法找到控制器类,module文件在根路径 使用gii生成Module为\app\admin,那么 namespace app; class admin extend ...

  5. Code-NFine:下来框和复选框

    ylbtech-Code-NFine:下来框和复选框 1.返回顶部 1. 1.1 html $("#F_OrganizeId").bindSelect({ url: "/ ...

  6. 详述IntelliJ IDEA插件的安装及使用方法(图解)

    intellij idea是一款非常优秀的软件开发工具,它拥有这强大的插件体系,可以帮助开发者完成很多重量级的功能.今天,我们来学习一下如何安装和卸载intellij idea的插件. Intelli ...

  7. python mixin到底是什么 django

    1.什么是Mixin 在面向对象编程中,Mixin是一种类,这种类包含了其他类要使用的方法,但不必充当其他类的父类.其他类是如何获取Mixin中的方法因语言的不同而不同.所以有时候Mixin被描述为' ...

  8. 前端代码规范(转载 http://codeguide.bootcss.com/)

    http://codeguide.bootcss.com/ HTML 语法 HTML5 doctype 语言属性(Language attribute) 字符编码 IE 兼容模式 引入 CSS 和 J ...

  9. IT兄弟连 JavaWeb教程 经典案例3

    案例需求:写一个用户登录的html页面有账号和密码,并在登录的Servlet中获取登录的账号和密码,如果账号是abc密码是123则重定向到main.html,否则重定向到login.html. 案例实 ...

  10. 继续(3n+1)猜想 (25)

    #include <algorithm> #include <iostream> using namespace std; int main(){ ] = { }; ], nu ...