2011-09-08 83 views
17

考虑下面的代码:反向列表斯卡拉

import scala.util.Random 

object Reverser { 

    // Fails for big list 
    def reverseList[A](list : List[A]) : List[A] = { 
    list match { 
     case Nil => list 
     case (x :: xs) => reverseList(xs) ::: List(x) 
    } 
    } 

    // Works 
    def reverseList2[A](list : List[A]) : List[A] = { 
    def rlRec[A](result : List[A], list : List[A]) : List[A] = { 
     list match { 
     case Nil => result 
     case (x :: xs) => { rlRec(x :: result, xs) } 
     } 
    } 
    rlRec(Nil, list) 
    } 

    def main(args : Array[String]) : Unit = { 
    val random = new Random 
    val testList = (for (_ <- 1 to 2000000) yield (random.nextInt)).toList 
    // val testListRev = reverseList(testList) <--- Fails 
    val testListRev = reverseList2(testList) 
    println(testList.head) 
    println(testListRev.last) 
    } 
} 

为什么函数的第一个版本出现问题(大投入),而第二个变型的作品。我怀疑这与尾递归有关,但我不太确定。有人可以给我“假人”的解释吗?

+0

为什么不使用 'VAL testListRev = testList.reverse'? – Lutz

+1

这个问题在很久以前就被问到了,但这里是你的尾递归问题的答案。是的,这是因为尾部递归优化。在你的第一个实现中,case(x :: xs)=> reverseList(xs)::: List(x),在递归调用reverseList之后,程序必须向它添加List(x)。这意味着它不能被优化成循环,在你的第二个例子中:case(x :: xs)=> {rlRec(x :: result,xs)} rlRec是最后一次调用,退出后没有任何操作,这就是为什么它不必为它创建一个新的Stack框架。 – loteq

回答

27

好让我试试傻瓜

尾递归如果你关注你必须与reverseList完成后,你会得到

reverseList(List(1,2,3, 4)) 
reverseList(List(2,3,4):::List(1) 
(reverseList(List(3,4):::List(2)):::List(1) 
((reverseList(List(4):::List(3)):::List(2)):::List(1) 
Nil:::List(4):::List(3):::List(2):::List(1) 
List(4,3,2,1) 

随着rlRec,你有

rlRec(List(1,2,3,4), Nil) 
rlRec(List(2,3,4), List(1)) 
rlREc(List(3,4), List(2,1)) 
rlRec(List(4), List(3,2,1)) 
rlRec(Nil, List(4,3,2,1)) 
List(4,3,2,1) 

的不同的是,在第一种情况下,重写不断变长。在最后递归调用reverseList之后,您必须记住要完成的操作:要添加到结果中的元素。堆栈用于记住这一点。当这太过分时,你会发生堆栈溢出。相反,与rlRec一样,重写的大小始终相同。当最后一个rlRec完成时,结果可用。没有别的事可做,没有什么需要记住的,不需要堆叠。关键是在rlRec,递归调用是return rlRec(something else),而在reverseList它是return f(reverseList(somethingElse)),与f beging _ ::: List(x)。你要记住,你将不得不调用f(这意味着记忆x太)(返回不需要在Scala中,只是增加了清晰度。另外请注意,val a = recursiveCall(x); doSomethingElse()相同doSomethingElseWith(recursiveCall(x)),所以它不是尾调用)

当你有一个递归尾调用

def f(x1,...., xn) 
    ... 
    return f(y1, ...yn) 
    ... 

其实没有必要记住第一f为当第二个将返回上下文。所以它可以被重写

def f(x1....xn) 
start: 
    ... 
    x1 = y1, .... xn = yn 
    goto start 
    ... 

这就是编译器所做的,因此避免了堆栈溢出。

当然,函数f需要返回某处,而不是递归调用。这就是goto start创建的循环将退出的地方,就像递归调用系列停止的地方一样。

+0

喜欢你的解释。谢谢! –

5

第一种方法不是尾递归。请参阅:

case (x :: xs) => reverseList(xs) ::: List(x) 

调用的最后操作是:::,不是递归调用reverseList。另一个是尾递归。

17

功能被称为tail recursive它称为自己的最后一个动作。您可以通过添加@tailrec注释来检查功能是否为tail recursive

+3

感谢@tailrec。 –

8

正如其他人已经提到的那样,tail-call消除避免了在不需要时增加堆栈。如果您对优化的内容感到好奇,您可以运行

scalac -Xprint:tailcalls MyFile.scala 

...在消除阶段之后显示编译器中间表示。 (请注意,您可以在任何阶段之后做到这一点,你可以使用Scala -Xshow-阶段打印阶段的列表)

例如,对于你的内心,尾递归函数rlRec,它给了我:

def rlRec[A >: Nothing <: Any](result: List[A], list: List[A]): List[A] = { 
    <synthetic> val _$this: $line2.$read.$iw.$iw.type = $iw.this; 
    _rlRec(_$this,result,list){ 
    list match { 
     case immutable.this.Nil => result 
     case (hd: A, tl: List[A])collection.immutable.::[A]((x @ _), (xs @ _)) => _rlRec($iw.this, { 
     <synthetic> val x$1: A = x; 
     result.::[A](x$1) 
     }, xs) 
    } 
    } 
} 

没有人在那里合成的东西,重要的是_rlRec是一个标签(即使它看起来像一个函数),并且模式匹配的第二个分支中的_rlRec的“调用”将被编译为字节码跳转。

+0

很高兴知道!谢谢! –

9

可以使你的尾递归版本那么简单,通过使用默认参数给予对结果的初始值非尾递归版本:

def reverseList[A](list : List[A], result: List[A] = Nil) : List[A] = list match { 
    case Nil => result 
    case (x :: xs) => reverseList(xs, x :: result) 
} 

虽然你可以使用这个以与其他方式相同的方式,即reverseList(List(1,2,3,4)),不幸的是,您正在使用可选的result参数公开实现细节。目前似乎没有办法隐藏它。这可能会也可能不会担心你。

+0

不知道这是可能的,谢谢! –

+2

Scala'List'类提供了一种名为'reverse _ :::'的方法,几乎​​可以完全实现这一点。文档描述了它如此操作:“在列表前面以相反顺序添加给定列表的元素”。突然之间,“额外”的说法是一个特点!我们可以通过'someList reverse _ ::: Nil'来反转它,或者'someList reverse _ ::: otherList'来将'someList'反转到'otherList'的前面。通常情况下,为了支持尾递归(称为累加器),为了支持尾递归,添加到一个函数中的额外参数实际上概括了您的方法的目的。 – Ben

-1
def reverse(n: List[Int]): List[Int] = { 
    var a = n 
    var b: List[Int] = List() 
    while (a.length != 0) { 
    b = a.head :: b 
    a = a.tail 
    } 
    b 
} 

当调用函数调用它,

reverse(List(1,2,3,4,5,6)) 

然后它会给这样回答,

res0: List[Int] = List(6, 5, 4, 3, 2, 1) 
+2

两个'var's和一个'while()'循环?这是非常可怜的斯卡拉风格。不好。 – jwvh