【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)
}
轉載請註明作者Jason Ding及其出處
Github部落格主頁(http://jasonding1354.github.io/)
GitCafe部落格主頁(http://jasonding1354.gitcafe.io/)
CSDN部落格(http://blog.csdn.net/jasonding1354)
簡書主頁(http://www.jianshu.com/users/2bd9b48f6ea8/latest_articles)
Google搜尋jasonding1354進入我的部落格主頁
相關文章
- 遞迴尾呼叫優化遞迴優化
- 尾遞迴以及優化遞迴優化
- ?30 秒瞭解尾遞迴和尾遞迴優化遞迴優化
- 遞迴優化:尾呼叫和Memoization遞迴優化
- Javascript中的尾遞迴及其優化JavaScript遞迴優化
- JS尾遞迴優化斐波拉契數列JS遞迴優化
- JavaScript, ABAP和Scala裡的尾遞迴(Tail Recursion)JavaScript遞迴AI
- 遞迴和尾遞迴遞迴
- 探索c#之尾遞迴編譯器優化C#遞迴編譯優化
- 面試官:用“尾遞迴”優化斐波那契函式面試遞迴優化函式
- 淺談尾遞迴遞迴
- 演算法小專欄:遞迴與尾遞迴演算法遞迴
- JavaScript和ABAP的尾遞迴JavaScript遞迴
- 尾遞迴實現深複製遞迴
- 直觀理解(尾)遞迴函式遞迴函式
- 尾遞迴(tail recursion) 的簡單使用遞迴AI
- 通過階乘的例子,練習在JavaScript, Scala和ABAP裡實現尾遞迴(Tail Recursion)JavaScript遞迴AI
- 函數語言程式設計之尾呼叫和尾遞迴函數程式設計遞迴
- 尾呼叫優化優化
- 有趣的 Scala 語言: 使用遞迴的方式去思考遞迴
- 圖解尾呼叫優化圖解優化
- Oracle優化案例-定位start with connect by遞迴死迴圈資料(二十二)Oracle優化遞迴
- 尾遞迴 - 杜絕記憶體洩漏溢位爆棧遞迴記憶體
- kingbase SQL最佳化案例 ( union遞迴 改 cte遞迴 )SQL遞迴
- 快速排序【遞迴】【非遞迴】排序遞迴
- 用PostgreSQL找回618秒逝去的青春-遞迴收斂優化SQL遞迴優化
- 尾呼叫、優化和 ES6優化
- 遞迴遞迴
- 快排的優化(非遞迴 (感覺沒變化遞迴和非遞迴)+ 三個隨機數選取準標準值(和相對的最後的位置交換) + 分割區間法)優化遞迴隨機
- 程式分析與優化 - 6 迴圈優化優化
- ACM(遞迴遞推—A)ACM遞迴
- 週而復始,往復迴圈,遞迴、尾遞迴演算法與無限極層級結構的探究和使用(Golang1.18)遞迴演算法Golang
- 舉例說明你對尾遞迴的理解,有哪些應用場景遞迴
- [譯] ES6中的尾呼叫優化優化
- ACM(遞迴遞推—I)ACM遞迴
- JavaScript遞迴JavaScript遞迴
- go 遞迴Go遞迴
- 理解遞迴遞迴