尾遞迴的那點事
文章目錄
尾遞迴的那點事
學習就是不斷解惑的過程:
先找個問題:前幾天偶然有人問到一個關於棧溢位的問題,說遞迴時為什麼scala不容易比python出現棧溢位的問題?大概是這個意思,原話記不住了,當時也沒多想,只是聯想到遞迴是從上到下的計算流程,所以估計可能是記憶遞迴不必儲存那麼多棧幀或者是從下到上的計算流程,今天突然想到這個問題,記錄一下。
回憶只需要看這裡
- 一句話結論:
scala編譯器會將<尾遞迴>優化為<累加器+迭代器>的迴圈計算
- @tailrec:用來驗證這個方法是否會使用尾遞迴優化進行編譯,如果不能會報
Recursive call not in tail position
筆記
問題有了,需要將問題拆分,然後一個個尋找答案
- 棧溢位是什麼意思?
- 什麼是遞迴?
- 什麼是尾遞迴?
- 遞迴的優缺點分別是什麼?
- 遞迴如何優化?
1.棧溢位是什麼意思
棧溢位
指的就是程式中定義的引用、指標等超過了棧結構預先設定的記憶體大小,最常見的就是遞迴呼叫,常見的報錯如下:
Exception in thread "main" java.lang.StackOverflowError
2.什麼是遞迴
遞迴
: 直接或間接的呼叫自己,通過引數迭代,減小原問題的規模/量級,進而實現[大問題->小問題]的轉換思想
[Recursion in Wiki]In computer science, recursion is a method of solving a problem where the solution depends on solutions to smaller instances of the same problem
3.什麼是尾遞迴
尾遞迴
: 顧名思義,遞迴呼叫放在尾部的遞迴方式,如:
def bee(num: Int, acc: Long): Long = {
if (num <= 1) acc
else bee(num - 1, acc + num)
}
4.遞迴的優缺點
優點
- 書寫優雅、簡單
- 邏輯符合動態規劃的思路,清晰明瞭
缺點
- 需要保留大量的棧幀,經常出現棧溢位
5.如何優化
背景案例:求解1+2+3+4+…+n的和
def foo(num: Int): Long = {
if (num <= 1) num.toLong
else num + foo(num - 1)
}
遞迴的思想可以理解為:[從上到下的思維]要求解1->n的和,只需要將1->n-1的和求出來,再加上n即可<稱為相關關係
>,當n=1時,和可以很輕鬆計算為1<稱為base case
>;然後就開始按照儲存的呼叫關係<即1-4的步驟>,計算最終結果<即5->8的步驟>
1->
2 ->
3 ->
4 ->
5 <-
6 <-
7 <-
8<-
很明顯,上面儲存的呼叫關係[1-4]是可以優化掉的,只需要我們每次將上次的計算結果儲存並傳遞給後續計算[累加器],然後通過計數等方式界定計算次數[迭代器/計數器]
def bar(num: Int): Long = {
var acc = 0L;
for (i <- 1 to num) { acc += i }
acc
}
為了保持遞迴的優雅,將上述寫法修改成尾遞迴的形式,尾遞迴中不得出現遞迴呼叫賦值、計算等操作。
@tailrec
def bee(num: Int, acc: Long): Long = {
if (num <= 1) acc
else bee(num - 1, acc + num)
}