[英]Reverse list Scala
給出以下代碼:
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)
}
}
為什么第一個版本的函數失敗(對於大輸入),而第二個版本的作用。 我懷疑它與尾遞歸有關,但我不太確定。 有人可以給我“傻瓜”的解釋嗎?
好吧,讓我試試傻瓜的尾遞歸
如果您按照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
完成時,結果可用。 沒有別的事可做,沒有什么可記住的,不需要堆棧。 關鍵是在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))
val a = recursiveCall(x); doSomethingElse()
相同doSomethingElseWith(recursiveCall(x))
,所以它不是尾調用)
當你有一個遞歸尾調用
def f(x1,...., xn)
...
return f(y1, ...yn)
...
實際上,當第二個f
將返回時,實際上不需要記住第一個f
的上下文。 所以它可以重寫
def f(x1....xn)
start:
...
x1 = y1, .... xn = yn
goto start
...
這就是編譯器的作用,因此可以避免堆棧溢出。
當然,函數f
需要返回某個不是遞歸調用的地方。 這就是goto start
創建的循環將退出的地方,就像遞歸調用序列停止的地方一樣。
當函數將其自身稱為最后一個動作時,函數稱為tail recursive
。 您可以通過添加@tailrec
注釋來檢查函數是否為tail recursive
。
通過使用默認參數為結果提供初始值,您可以使尾遞歸版本與非尾遞歸版本一樣簡單:
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
參數公開了一個實現細節。 目前似乎沒有辦法隱藏它。 這可能會或可能不會讓您擔心。
正如其他人所提到的,尾部調用消除避免了在不需要時增加堆棧。 如果您對優化的作用感到好奇,那么您可以運行
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的“調用”將被編譯為字節碼中的跳轉。
第一種方法不是尾遞歸。 看到:
case (x :: xs) => reverseList(xs) ::: List(x)
調用的最后一個操作是:::
,而不是遞歸調用reverseList
。 另一個是尾遞歸。
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)
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.