[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实际上封装了一个策略,其中有三个部分:
- Function:通过传入参数T,来得到解答R。这一点从代码
int priceWhenCut = func.apply(i) + func.apply(length - i)
很明显的就能够看出来。可以把它想象成一个备忘录的入口。 - T:代表求解问题时需要的参数T。
- 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方法来完成下面的逻辑:
- 通过传入的key检查(在以上代码中是input)对应的值是否存在于备忘录的底层Map中
- 如果存在,跳转到步骤4
- 如果不存在,根据computeIfAbsent的第二个参数(是一个Lambda表达式)来计算得到key对应的value
- 返回得到的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) .的更多相关文章
- [Java 8] (8) Lambda表达式对递归的优化(上) - 使用尾递归 .
递归优化 很多算法都依赖于递归,典型的比如分治法(Divide-and-Conquer).但是普通的递归算法在处理规模较大的问题时,常常会出现StackOverflowError.处理这个问题,我们可 ...
- java 8 中lambda表达式学习
转自 http://blog.csdn.net/renfufei/article/details/24600507 http://www.jdon.com/idea/java/10-example-o ...
- Lambda 表达式,Java中应用Lambda 表达式
一.Lambda 表达式 简单来说,编程中提到的 lambda 表达式,通常是在需要一个函数,但是又不想费神去命名一个函数的场合下使用,也就是指匿名函数. 链接:知乎 先举一个普通的 Python 例 ...
- Python爬虫与数据分析之进阶教程:文件操作、lambda表达式、递归、yield生成器
专栏目录: Python爬虫与数据分析之python教学视频.python源码分享,python Python爬虫与数据分析之基础教程:Python的语法.字典.元组.列表 Python爬虫与数据分析 ...
- Java 终于有 Lambda 表达式啦~Java 8 语言变化——Lambda 表达式和接口类更改【转载】
原文地址 en cn 下载 Demo Java™ 8 包含一些重要的新的语言功能,为您提供了构建程序的更简单方式.Lambda 表达式 为内联代码块定义一种新语法,其灵活性与匿名内部类一样,但样板文件 ...
- Java 8中Lambda表达式默认方法的模板方法模式,你够了解么?
为了以更简单的术语描述模板方法,考虑这个场景:假设在一个工作流系统中,为了完成任务,有4个任务必须以给定的执行顺序执行.在这4个任务中,不同工作流系统的实现可以根据自身情况自定义任务的执行内容. 模板 ...
- 备忘录模式-Memento Pattern(Java实现)
备忘录模式-Memento Pattern Memento备忘录设计模式是一个保存另外一个对象内部状态拷贝的对象,这样以后就可以将该对象恢复到以前保存的状态. 本文中的场景: 有一款游戏可以随时存档, ...
- 在Android中使用Java 8的lambda表达式
作为一名Java开发者,或许你时常因为缺乏闭包而产生许多的困扰.幸运的是:Java's 8th version introduced lambda functions给我们带来了好消息;然而,这咩有什 ...
- 【转】Java 8十个lambda表达式案例
1. 实现Runnable线程案例 使用() -> {} 替代匿名类: //Before Java 8: new Thread(new Runnable() { @Override public ...
随机推荐
- H5页面解决左右滑动问题
在head里面加入. <meta name="viewport" content="width=device-width, initial-scale=1.0, u ...
- Linux Cache 机制
在阅读文章前,您应该具备基本的存储器层次结构知识,至少要了解局部性原理.要详细了解cache基本原理,可以参考本书<深入理解计算机系统>中存储器体系结构一章: 带着疑问来看文章,cache ...
- explain之二:Explain 结果解读与实践,分析诊断工具之二
MySQL的EXPLAIN命令用于SQL语句的查询执行计划(QEP).这条命令的输出结果能够让我们了解MySQL 优化器是如何执行SQL 语句的.这条命令并没有提供任何调整建议,但它能够提供重要的信息 ...
- Bootstrap-CL:字体图标(Glyphicons)
ylbtech-Bootstrap-CL:字体图标(Glyphicons) 1.返回顶部 1. Bootstrap 字体图标(Glyphicons) 本章将讲解字体图标(Glyphicons),并通过 ...
- absolute属性与IE6/IE7之间的误会
三.absolute属性与IE6/IE7之间的误会 absolute属性确实存在不少兼容性的问题,首先absolute属性定位相关(left/top)的些bug(例如IE6的奇偶bug)这里不予以讨论 ...
- 移动端网页巧用 margin和padding 的百分比实现自适应
一个基础却又容易混淆的css知识点 本文依赖于一个基础却又容易混淆的css知识点:当margin/padding取形式为百分比的值时,无论是left/right,还是top/bottom,都是以父元素 ...
- API接口文档的撰写
接口文档: 要写:接口简介.请求参数.返回结果.注意事项. 下面以“喜马拉雅的 ‘圈子’ ”为例子: 接口一 (1)接口简介 http://ipservice.mogujie.com/ipservic ...
- html5盒子模型
相关博客: Flex 布局教程:语法篇:http://www.ruanyifeng.com/blog/2015/07/flex-grammar.html Flex 布局教程:实例篇:http://ww ...
- bzoj 1814: Ural 1519 Formula 1【插头dp】
设f[i][j][s]为轮廓线推到格子(i,j),状态为s的方案数 括号表示一段线的左端和右端,表示成左括号和右括号,状压的时候用1和2表示,0表示已经闭合 下面的蓝线是黄色格子的轮廓线,dp转移要把 ...
- 2017.11.7~8模拟测试总结---暨NOIP2017考前对策
最后两天了,第三天就是NOIP2017--Day1了. 刚刚考完了这个学期从开学以来的最后一场模拟赛了.首先要对于这场模拟赛做一次深刻的反思. 考完才猛地惊叹这是最后一场模拟赛了,然而题目并不难,也保 ...