乙個函式直接或間接的呼叫它自己本身,就是遞迴。它通常把乙個大型複雜的問題層層轉化為乙個與原問題相似的規模較小的問題來求解,遞迴策略只需少量的**就可以執行多次重複的計算。
一般來說,遞迴需要有邊界條件、遞迴前進段和遞迴返回段。當邊界條件不滿足時,遞迴前進;當邊界條件滿足時,遞迴返回。
以遞迴方式實現階乘函式的實現:
def factorial(n:int): long =
if(n <= 0) 1是遞迴返回段,else後面部分是遞迴前進段。它的呼叫過程大致如下:
factorial(5)
5 * factorial(4)
5 * (4 * factorial(3))
5 * (4 * (3 * factorial(2)))
5 * (4 * (3 * (2 * factorial(1))))
5 * (4 * (3 * (2 * 1)))
尾遞迴是指遞迴呼叫是函式的最後乙個語句,而且其結果被直接返回,這是一類特殊的遞迴呼叫。由於遞迴結果總是直接返回,尾遞迴比較方便轉換為迴圈,因此編譯器容易對它進行優化。現在很多編譯器都對尾遞迴有優化,程式設計師們不必再手動將它們改寫為迴圈。
我們可以這樣理解尾遞迴
1.2中**最後遞迴呼叫 factorial(n-1) ,但它是整個函式返回值表示式 的一部分,因此它不是尾遞迴。
我們可以使用尾遞迴來實現上面的階乘:
def factorial(n:int):long =
factorial(n,1)
}
呼叫過程大致如下:
factorialtailrec(5, 1)
factorialtailrec(4, 5) // 1 * 5 = 5
factorialtailrec(3, 20) // 5 * 4 = 20
factorialtailrec(3, 60) // 20 * 3 = 60
factorialtailrec(2, 120) // 60 * 2 = 120
factorialtailrec(1, 120) // 120 * 1 = 120120
以上的呼叫,由於呼叫結果都是直接返回,所以之前的遞迴呼叫留在堆疊中的資料可以丟棄,只需要保留最後一次的資料,這就是尾遞迴容易優化的原因所在。
尾遞迴的核心思想是通過引數來傳遞每一次的呼叫結果,達到不壓棧。它維護著乙個迭代器和乙個累加器。
事實上,scala都是將尾遞迴直接編譯成迴圈模式的。所以我們可以大膽的說,所有的迴圈模式都能改寫為尾遞迴的寫法模式。我們可以看一下階乘演算法的遞迴版本
def fibfor(n:int): int =
m}
尾遞迴會維護乙個或多個累計值(aggregate)引數和乙個迭代引數。
def fibonacci(n: int): int =
呼叫過程為:
fibonacci(5)
fibonacci(4) + fibonacci(3)
(fibonacci(3) + fibonacci(2)) + (fibonacci(2) + fibonacci(1))
((fibonacci(2) + fibonacci(1)) + 1) + (1 + 1)
((1 + 1) + 1) + 2
上面顯然不是尾遞迴,如何找到累加器將它改造為尾遞迴?因為需要前兩項的和,所以這裡需要兩個累加器,假設較小的乙個為acc1,較大的乙個為acc2,需要計算下一項時,將acc2的值賦給acc1,而(acc1+acc2)賦值給acc2,這樣,呼叫堆疊中舊有的資料即可丟棄。
def fibonaccitailrec(n: int, acc1: int, acc2: int): int =
呼叫過程為:
fibonaccitailrec(5,0,1)
fibonaccitailrec(4,1,1)
fibonaccitailrec(3,1,2)
fibonaccitailrec(2,2,3)
fibonaccitailrec(1,3,5)
scala對形式上嚴格的尾遞迴進行了優化,對於嚴格的尾遞迴,可以放心使用,不必擔心效能問題。對於是否是嚴格尾遞迴,若不能自行判斷, 可使用scala提供的尾遞迴標註@scala.annotation.tailrec,這個符號除了可以標識尾遞迴外,更重要的是編譯器會檢查該函式是否真的尾遞迴,若不是,會導致如下編譯錯誤:
could not optimize @tailrec annotated method fibonacci: it contains a recursive call not in tail position
當編譯器檢測到乙個函式呼叫是尾遞迴的時候,它就覆蓋當前的活動記錄而不是在棧中去建立乙個新的。scala編譯器會察覺到尾遞迴,並對其進行優化,將它編譯成迴圈的模式。
下面進行fibonacci演算法的普通遞迴和尾遞迴時間測試對比:
測試**:
def main(args: array[string]): unit = ms")
starttime = system.nanotime
println(fibonaccitailrec(n,0,1))
endtime = system.nanotime
println(s"fibonaccitailrec time:$ms")
} def fibonacci(n: int): int =
@tailrec
def fibonaccitailrec(n: int, acc1: int, acc2: int): int =
測試結果:
*****====
n = 20
6765
fibonacci time:0.611397ms
6765
fibonaccitailrec time:0.025869ms
*****====
n = 30
832040
fibonacci time:5.091809ms
832040
fibonaccitailrec time:0.023307ms
*****====
n = 40
102334155
fibonacci time:300.614119ms
102334155
fibonaccitailrec time:0.027578ms
*****====
可以看到,隨著n的增大,尾遞迴帶來的效能優化是非常明顯的。
迴圈呼叫都是乙個累計器和乙個迭代器的作用。同理,尾遞迴也是如此,它也是通過累加和迭代將結果賦值給新一輪的呼叫,使用好尾遞迴能夠給我們的程式帶來很大效能上的優化。
Scala高階之路 尾遞迴優化
scala高階之路 尾遞迴優化 遞迴呼叫有時候能被轉換成迴圈,這樣能節約棧空間。在函式式程式設計中,這是很重要的,我們通常會使用遞迴方法來遍歷集合。而不是所有的遞迴都能被優化。遞迴之所有能被優化是在指在函式的最後一行為遞迴呼叫 即尾遞迴 並且這個遞迴呼叫沒有其它元素參與。一.什麼情況能導致棧的溢位 ...
尾遞迴優化
尾遞迴就是遞迴語句在函式最後執行,且無需對返回值進行進一步操作。編譯器會對這種遞迴進行優化,在進入深層遞迴時候,不是在遞迴棧進行入棧操作,而是直接覆蓋棧頂。線性遞迴與尾遞迴區別如下 線性遞迴 1 2 3 4 5 longrescuvie longn 尾遞迴 1 2 3 4 5 6 7 8 9 10 ...
尾遞迴優化
什麼是尾遞迴 尾遞迴就將遞迴呼叫寫在函式的尾部return 尾遞迴的好處 解決傳統遞迴的棧溢位問題 尾遞迴適合的業務場景 1.需要遞迴優化的函式沒有用timeout等非同步佇列進行遞迴呼叫函式自己 2.需要遞迴優化的遞迴函式的返回值不是每次都返回,而是條件性返回 尾遞迴優化後的遞迴demo meth...