缩小、折叠或扫描(左/右)?
Posted
技术标签:
【中文标题】缩小、折叠或扫描(左/右)?【英文标题】:Reduce, fold or scan (Left/Right)? 【发布时间】:2013-06-28 20:01:36 【问题描述】:什么时候应该使用reduceLeft
、reduceRight
、foldLeft
、foldRight
、scanLeft
或scanRight
?
我想要对它们的差异有一个直观/概述 - 可能有一些简单的例子。
【问题讨论】:
推荐你看***.com/questions/25158780/… 感谢您的指点。这有点超出我的技术知识 :) 我的回答中是否有您认为应该澄清/更改的内容? 不,只是指出一点历史和与 MPP 的相关性。 好吧,严格来说,reduce
和 fold
之间的区别不是起始值的存在,而是更深层的数学原因的结果 .
【参考方案1】:
一般来说,所有 6 个折叠函数都将二元运算符应用于集合的每个元素。每一步的结果被传递到下一步(作为二元运算符的两个参数之一的输入)。这样我们可以累积一个结果。
reduceLeft
和 reduceRight
累积一个结果。
foldLeft
和 foldRight
使用起始值累积单个结果。
scanLeft
和scanRight
使用起始值累积中间累积结果的集合。
积累
从左到右...
通过元素集合abc
和二元运算符add
,我们可以探索从集合的LEFT 元素(从A 到C)前进时不同折叠函数的作用:
val abc = List("A", "B", "C")
def add(res: String, x: String) =
println(s"op: $res + $x = $res + x")
res + x
abc.reduceLeft(add)
// op: A + B = AB
// op: AB + C = ABC // accumulates value AB in *first* operator arg `res`
// res: String = ABC
abc.foldLeft("z")(add) // with start value "z"
// op: z + A = zA // initial extra operation
// op: zA + B = zAB
// op: zAB + C = zABC
// res: String = zABC
abc.scanLeft("z")(add)
// op: z + A = zA // same operations as foldLeft above...
// op: zA + B = zAB
// op: zAB + C = zABC
// res: List[String] = List(z, zA, zAB, zABC) // maps intermediate results
从右到后...
如果我们从 RIGHT 元素开始并向后(从 C 到 A),我们会注意到现在二元运算符的 second 参数会累积结果(运算符相同,我们只是切换了参数名称以使其角色清晰):
def add(x: String, res: String) =
println(s"op: $x + $res = $x + res")
x + res
abc.reduceRight(add)
// op: B + C = BC
// op: A + BC = ABC // accumulates value BC in *second* operator arg `res`
// res: String = ABC
abc.foldRight("z")(add)
// op: C + z = Cz
// op: B + Cz = BCz
// op: A + BCz = ABCz
// res: String = ABCz
abc.scanRight("z")(add)
// op: C + z = Cz
// op: B + Cz = BCz
// op: A + BCz = ABCz
// res: List[String] = List(ABCz, BCz, Cz, z)
.
去累积
从左到右...
如果我们要反累积一些结果,从集合的 LEFT 元素开始,我们将通过二元运算符 minus
的第一个参数 res
来累积结果:
val xs = List(1, 2, 3, 4)
def minus(res: Int, x: Int) =
println(s"op: $res - $x = $res - x")
res - x
xs.reduceLeft(minus)
// op: 1 - 2 = -1
// op: -1 - 3 = -4 // de-cumulates value -1 in *first* operator arg `res`
// op: -4 - 4 = -8
// res: Int = -8
xs.foldLeft(0)(minus)
// op: 0 - 1 = -1
// op: -1 - 2 = -3
// op: -3 - 3 = -6
// op: -6 - 4 = -10
// res: Int = -10
xs.scanLeft(0)(minus)
// op: 0 - 1 = -1
// op: -1 - 2 = -3
// op: -3 - 3 = -6
// op: -6 - 4 = -10
// res: List[Int] = List(0, -1, -3, -6, -10)
从右到后...
但现在要注意 xRight 的变化!请记住,xRight 变体中的(反)累积值被传递给我们的二元运算符minus
的 second 参数res
:
def minus(x: Int, res: Int) =
println(s"op: $x - $res = $x - res")
x - res
xs.reduceRight(minus)
// op: 3 - 4 = -1
// op: 2 - -1 = 3 // de-cumulates value -1 in *second* operator arg `res`
// op: 1 - 3 = -2
// res: Int = -2
xs.foldRight(0)(minus)
// op: 4 - 0 = 4
// op: 3 - 4 = -1
// op: 2 - -1 = 3
// op: 1 - 3 = -2
// res: Int = -2
xs.scanRight(0)(minus)
// op: 4 - 0 = 4
// op: 3 - 4 = -1
// op: 2 - -1 = 3
// op: 1 - 3 = -2
// res: List[Int] = List(-2, 3, -1, 4, 0)
最后一个 List(-2, 3, -1, 4, 0) 可能不是您直觉所期望的!
如您所见,您可以通过简单地运行 scanX 来检查您的 foldX 正在做什么,并在每个步骤中调试累积的结果。
底线
使用reduceLeft
或reduceRight
累积结果。
如果有起始值,则使用 foldLeft
或 foldRight
累积结果。
使用scanLeft
或scanRight
累积中间结果的集合。
如果您想向前浏览集合,请使用 xLeft 变体。
如果您想向后浏览集合,请使用 xRight 变体。【讨论】:
如果我没记错的话,左边的版本可以使用尾调用优化,这意味着它的效率要高得多。 @Marc,我喜欢带字母的例子,它让事情很清楚 @Trylks foldRight 也可以用tailrec实现 @TimothyKim 可以,通过优化的非直接实现来做到这一点。例如。在particular case of Scala lists 中,这种方式包括将List
反转为然后应用foldLeft
。其他集合可能会实施不同的策略。一般来说,如果foldLeft
和foldRight
可以互换使用(应用运算符的关联属性),那么foldLeft
更有效且更可取。
@Trylks xLeft
和 xRight
具有相同的渐近复杂度,并且可以以尾递归的方式实现。然而,Scala 标准库中的实际实现是不纯的(为了提高效率)。【参考方案2】:
通常 REDUCE,FOLD,SCAN 方法通过在 LEFT 上累积数据并不断更改 RIGHT 变量来工作。它们之间的主要区别是REDUCE,FOLD是:-
Fold 将始终以 seed
值开始,即用户定义的起始值。
如果 collection 为空,reduce 将抛出异常,而 fold 会返回种子值。 将始终产生单个值。
扫描用于从左侧或右侧对项目进行一些处理顺序,然后我们可以在后续计算中使用之前的结果。这意味着我们可以扫描项目。 总会产生一个集合。
LEFT_REDUCE 方法的工作原理类似于 REDUCE 方法。RIGHT_REDUCE 与 reduceLeft 相反,即它在 RIGHT 中累积值并不断更改 left 变量。
reduceLeftOption 和 reduceRightOption 类似于 left_reduce 和 right_reduce 唯一的区别是它们在 OPTION 对象中返回结果。
下面提到的代码的一部分输出将是:-
对数字列表使用scan
操作(使用seed
值0
)List(-2,-1,0,1,2)
0,-2=>-2 -2,-1=>-3 -3,0=>-3 -3,1=>-2 -2 ,2=>0 扫描列表(0, -2, -3, -3, -2, 0)
0,-2=>-2 -2,-1=>-3 -3,0=>-3 -3,1=>-2 -2 ,2=>0 scanLeft (a+b) List(0, -2, -3, -3, -2, 0)
0,-2=>-2 -2,-1=>-3 -3,0=>-3 -3,1=>-2 -2 ,2=>0 scanLeft (b+a) List(0, -2, -3, -3, -2, 0)
2,0=>2 1,2=>3 0,3=>3 -1,3=>2 -2,2=>0 scanRight ( a+b) 列表(0, 2, 3, 3, 2, 0)
2,0=>2 1,2=>3 0,3=>3 -1,3=>2 -2,2=>0 scanRight ( b+a) 列表(0, 2, 3, 3, 2, 0)
对字符串列表使用reduce
,fold
操作List("A","B","C","D","E")
代码:
object ScanFoldReduce extends App
val list = List("A","B","C","D","E")
println("reduce (a+b) "+list.reduce((a,b)=>
print(""+a+","+b+"=>"+ (a+b)+" ")
a+b
))
println("reduceLeft (a+b) "+list.reduceLeft((a,b)=>
print(""+a+","+b+"=>"+ (a+b)+" ")
a+b
))
println("reduceLeft (b+a) "+list.reduceLeft((a,b)=>
print(""+a+","+b+"=>"+ (b+a)+" " )
b+a
))
println("reduceRight (a+b) "+list.reduceRight((a,b)=>
print(""+a+","+b+"=>"+ (a+b)+" " )
a+b
))
println("reduceRight (b+a) "+list.reduceRight((a,b)=>
print(""+a+","+b+"=>"+ (b+a)+" ")
b+a
))
println("scan "+list.scan("[")((a,b)=>
print(""+a+","+b+"=>"+ (a+b)+" " )
a+b
))
println("scanLeft (a+b) "+list.scanLeft("[")((a,b)=>
print(""+a+","+b+"=>"+ (a+b)+" " )
a+b
))
println("scanLeft (b+a) "+list.scanLeft("[")((a,b)=>
print(""+a+","+b+"=>"+ (b+a)+" " )
b+a
))
println("scanRight (a+b) "+list.scanRight("[")((a,b)=>
print(""+a+","+b+"=>"+ (a+b)+" " )
a+b
))
println("scanRight (b+a) "+list.scanRight("[")((a,b)=>
print(""+a+","+b+"=>"+ (b+a)+" " )
b+a
))
//Using numbers
val list1 = List(-2,-1,0,1,2)
println("reduce (a+b) "+list1.reduce((a,b)=>
print(""+a+","+b+"=>"+ (a+b)+" ")
a+b
))
println("reduceLeft (a+b) "+list1.reduceLeft((a,b)=>
print(""+a+","+b+"=>"+ (a+b)+" ")
a+b
))
println("reduceLeft (b+a) "+list1.reduceLeft((a,b)=>
print(""+a+","+b+"=>"+ (b+a)+" " )
b+a
))
println(" reduceRight (a+b) "+list1.reduceRight((a,b)=>
print(""+a+","+b+"=>"+ (a+b)+" " )
a+b
))
println(" reduceRight (b+a) "+list1.reduceRight((a,b)=>
print(""+a+","+b+"=>"+ (b+a)+" ")
b+a
))
println("scan "+list1.scan(0)((a,b)=>
print(""+a+","+b+"=>"+ (a+b)+" " )
a+b
))
println("scanLeft (a+b) "+list1.scanLeft(0)((a,b)=>
print(""+a+","+b+"=>"+ (a+b)+" " )
a+b
))
println("scanLeft (b+a) "+list1.scanLeft(0)((a,b)=>
print(""+a+","+b+"=>"+ (b+a)+" " )
b+a
))
println("scanRight (a+b) "+list1.scanRight(0)((a,b)=>
print(""+a+","+b+"=>"+ (a+b)+" " )
a+b))
println("scanRight (b+a) "+list1.scanRight(0)((a,b)=>
print(""+a+","+b+"=>"+ (a+b)+" " )
b+a))
【讨论】:
这篇文章几乎无法阅读。请缩短句子,使用真正的关键字(例如 reduceLeft 而不是 LEFT_REDUCE)。在处理代码时使用真正的数学箭头、代码标签。更喜欢输入/输出示例而不是解释所有内容。中间计算让人难以阅读。【参考方案3】:对于包含元素 x0、x1、x2、x3 和任意函数 f 的集合 x,您有以下内容:
1. x.reduceLeft (f) is f(f(f(x0,x1),x2),x3) - notice 3 function calls
2. x.reduceRight (f) is f(f(f(x3,x2),x1),x0) - notice 3 function calls
3. x.foldLeft (init,f) is f(f(f(f(init,x0),x1),x2),x3) - notice 4 function calls
4. x.foldRight(init,f) is f(f(f(f(init,x3),x2),x1),x0) - notice 4 function calls
5. x.scanLeft (init,f) is f(init,x0)=g0
f(f(init,x0),x1) = f(g0,x1) = g1
f(f(f(init,x0),x1),x2) = f(g1,x2) = g2
f(f(f(f(init,x0),x1),x2),x3) = f(g2,x3) = g3
- notice 4 function calls but also 4 emitted values
- last element is identical with foldLeft
6. x.scanRight (init,f) is f(init,x3)=h0
f(f(init,x3),x2) = f(h0,x2) = h1
f(f(f(init,x3),x2),x1) = f(h1,x1) = h2
f(f(f(f(init,x3),x2),x1),x0) = f(h2,x0) = h3
- notice 4 function calls but also 4 emitted values
- last element is identical with foldRight
总结
scan
类似于 fold
,但也会发出所有中间值
reduce
不需要初始值,有时很难找到
fold
需要一个更难找到的初始值:
0 表示总和
1 代表产品
min 的第一个元素(有些人可能建议使用 Integer.MAX_VALUE)
不是 100% 肯定,但看起来有以下等效实现:
x.reduceLeft(f) === x.drop(1).foldLeft(x.head,f)
x.foldRight(init,f) === x.reverse.foldLeft(init,f)
x.foldLeft(init,f) === x.scanLeft(init,f).last
【讨论】:
以上是关于缩小、折叠或扫描(左/右)?的主要内容,如果未能解决你的问题,请参考以下文章
CodeForces 380C Sereja and Brackets(扫描线+树状数组)