【Scala】尾遞迴優化
以遞迴方式思考
遞迴通過靈巧的函式定義,告訴計算機做什麼。在函數語言程式設計中,隨處可見遞迴思想的運用。
下面給出幾個遞迴函式的例子:
object RecursiveExample extends App{
// 數列求和例子
def sum(xs: List[Int]): Int =
if (xs.isEmpty)
1
else
xs.head + sum(xs.tail)
// 求最大值例子
def max(xs: List[Int]): Int =
if (xs.isEmpty)
throw new NoSuchElementException
else if (xs.size == 1)// 遞迴的邊界條件
xs.head
else
if (xs.head > max(xs.tail)) xs.head else max(xs.tail)
// 翻轉字串
def str_reverse(xs: String): String =
if (xs.length == 1)
xs
else
str_reverse(xs.tail) + xs.head
// 快速排序例子
def quicksort(ls: List[Int]): List[Int] = {
if (ls.isEmpty)
ls
else
quicksort(ls.filter(_ < ls.head)) ::: ls.head :: quicksort(ls.filter(_ > ls.head))
//quicksort(ls.filter(x => x < ls.head)) ::: ls.head :: quicksort(ls.filter(x => x > ls.head))
}
}
我們以上面程式碼最後一個快速排序函式為例,使用遞迴的方式,其程式碼實現非常的簡潔和通俗易懂。遞迴函式的核心是設計好遞迴表示式,並且確定演算法的邊界條件。上面的快速排序中,認為空列表就是排好序的列表,這就是遞迴的邊界條件,這個條件是遞迴終止的標誌。
尾遞迴
遞迴演算法需要保持呼叫堆疊,效率較低,如果呼叫次數較多,會耗盡記憶體或棧溢位。然而,尾遞迴可以克服這一缺點。
尾遞迴是指遞迴呼叫是函式的最後一個語句,而且其結果被直接返回,這是一類特殊的遞迴呼叫。由於遞迴結果總是直接返回,尾遞迴比較方便轉換為迴圈,因此編譯器容易對它進行優化。
遞迴求階乘的經典例子
普通遞迴求解的程式碼如下:
def factorial(n: BigInt): BigInt = {
if (n <= 1)
1
else
n * factorial(n-1)
}
上面的程式碼,由於每次遞迴呼叫n-1的階乘時,都有一次額外的乘法計算,這使得堆疊中的資料都需要保留。在新的遞迴中要分配新的函式棧。
執行過程就像這樣:
factorial(4)
--------------
4 * factorial(3)
4 * (3 * factorial(2))
4 * (3 * (2 * factorial(1)))
4 * (3 * (2 * 1))
而下面是一個尾遞迴版本,在效率上,和迴圈是等價的:
import scala.annotation.tailrec
def factorialTailRecursive(n: BigInt): BigInt = {
@tailrec
def _loop(acc: BigInt, n: BigInt): BigInt =
if(n <= 1) acc else _loop(acc*n, n-1)
_loop(1, n)
}
這裡的執行過程如下:
factorialTailRecursive(4)
--------------------------
_loop(1, 4)
_loop(4, 3)
_loop(12, 2)
_loop(24, 1)
該函式中的_loop
在最後一步,要麼返回遞迴邊界條件的值,要麼呼叫遞迴函式本身。
改寫成尾遞迴版本的關鍵:
尾遞迴版本最重要的就是找到合適的累加器,該累加器可以保留最後一次遞迴呼叫留在堆疊中的資料,積累之前呼叫的結果,這樣堆疊資料就可以被丟棄,當前的函式棧可以被重複利用。
在這個例子中,變數acc就是累加器,每次遞迴呼叫都會更新該變數,直到遞迴邊界條件滿足時返回該值。
對於尾遞迴,Scala語言特別增加了一個註釋@tailrec
,該註釋可以確保程式設計師寫出的程式是正確的尾遞迴程式,如果由於疏忽大意,寫出的不是一個尾遞迴程式,則編譯器會報告一個編譯錯誤,提醒程式設計師修改自己的程式碼。
菲波那切數列的例子
原始的程式碼很簡單:
def fibonacci(n: Int): Int =
if (n <= 2)
1
else
fibonacci(n-1) + fibonacci(n-2)
尾遞迴版本用了兩個累加器,一個儲存較小的項acc1,另一個儲存較大項acc2:
def fibonacciTailRecursive(n: Int): Int = {
@tailrec
def _loop(n: Int, acc1: Int, acc2: Int): Int =
if(n <= 2)
acc2
else
_loop(n-1, acc2, acc1+acc2)
_loop(n, 1, 1)
}
幾個列表操作中使用尾遞迴的例子
求列表的長度
def lengthTailRecursive[A](ls: List[A]): Int = {
@tailrec
def lengthR(result: Int, curList: List[A]): Int = curList match {
case Nil => result
case _ :: tail => lengthR(result+1, tail)
}
lengthR(0, ls)
}
翻轉列表
def reverseTailRecursive[A](ls: List[A]): List[A] = {
@tailrec
def reverseR(result: List[A], curList: List[A]): List[A] = curList match {
case Nil => result
case h :: tail => reverseR(h :: result, tail)
}
reverseR(Nil, ls)
}
去除列表中多個重複的元素
這裡要求去除列表中多個連續的字元,只保留其中的一個。
// If a list contains repeated elements they should be replaced with
// a single copy of the element.
// The order of the elements should not be changed.
// Example:
// >> compress(List('a, 'a, 'a, 'a, 'b, 'c, 'c, 'a, 'a, 'd, 'e, 'e, 'e, 'e))
// >> List('a, 'b, 'c, 'a, 'd, 'e)
def compressTailRecursive[A](ls: List[A]): List[A] = {
@tailrec
def compressR(result: List[A], curList: List[A]): List[A] = curList match {
case h :: tail => compressR(h :: result, tail.dropWhile(_ == h))
case Nil => result.reverse
}
compressR(Nil, ls)
}