Recursive — 遞迴
簡單來說就是函式自己呼叫自己的狀況,遞迴由兩部分組成
- Recursive Case(遞迴情況) — 當函式呼叫自己本身的情況
- Base Case(基本狀況) — 是當函式不再呼叫自己的條件,也就是終止的條件
遞迴提供了較為簡潔的寫法,不過一定要記得設定好終止的條件,避免進入無限循環,把記憶體吃光光。
階乘 Factorial
在學習遞迴的時候,大部分的人都會從階乘開始練習,因為大家高中數學都有學過階乘,來複習一下甚麼是階乘 ? 從 1 連續相乘到 n 為止,也就是 n! = n*(n-1)* …*2*1。
ex. 5!=5*4*3*2*1
用 js 實作階乘
1 | const factorial = (n) => { |
遞迴在呼叫完自己後會先在原地等待,等待下一層將結果回傳,執行順序如下:
- factorial(5)會等待 factorial(4)回傳結果
- factorial(4)會等待 factorial(3)回傳結果
- factorial(3)會等待 factorial(2)回傳結果
- factorial(2)會等待 factorial(1)回傳結果
當 n = 1 的時候滿足終止遞迴的條件並回傳 1
- factorial(1)回傳 1
- factorial(2)回傳 2*1 =2
- factorial(3)回傳 3*2=6
- factorial(4)回傳 4*6=24
- factorial(5)回傳 5*24 = 120
遞迴的執行過程可以參考下圖,紅色箭頭是呼叫函式,綠色箭頭是回傳結果
九九乘法表
以經典的九九乘法表為例,一般我們會直覺地用雙迴圈來寫,其實用遞迴也可以寫出九九乘法表。
1 | //一般寫法 |
斐波那契數列
經典的 fibonacci sequence 斐波那契數列就很適合用遞迴來解
甚麼是斐波那契數列?
斐波那契數列由 0 和 1 開始,之後的斐波那契數列就是由前兩位數字相加而得出 ,也就是第 n 項為第 n-1 和第 n-2 項的總和 — 引用自 wikipedia
ex. 若要取得數列中的第 11 個數字的話公式為: 89 = 55+34 (n11 = n10 + n9)
用 js 實作斐波那契數列
1 | const fibonacci = (i) => { |
單看程式碼應該會覺得很抽象,所以可以搭配下面這張圖來看,由於斐波那契數列的規則是每個數字都是前面兩個數相加的總和,畫成圖的話會發現結構很像 binary tree。
不過眼尖的你可能會發現一個問題,有些函式明明已經有執行過,卻還是重複執行,像是 fib(2) 、fib(1),因為遞迴的本質是將問題拆解成多個小問題,而這些小問題可能會有重疊的狀況,因此並不會記得那些部分是已經運算過的,所以就會發生重複運算的問題,執行效率較差,這就是遞迴其中一個缺點。
除此之外,還可能發生 stack overflow(堆疊溢位 — stack 滿出來)的問題,在討論 stack overflow 之前,先來複習一下執行堆疊(call stack)的觀念
由於 JavaScript 是單執行緒的語言,一次只能做一件事情,所以使用具有後進先出特性的 stack 來儲存函式的執行環境,每次有新的任務加入的時候,就會像疊盤子一樣,一層一層往上疊加,JavaScript 會先處理最頂端的函式,當函式執行完並且 return 之後,該函式就會被移除,接下來處理下一個函式,直到 stack 被清空為止。
因為遞迴會不斷的呼叫自己的特性,就會一直把函式添加到 stack 裡面,如果遞迴太深層的話就會發生 stack overflow。
除了上述的問題,遞迴在呼叫函式的時候,為了保存當下執行的狀況,會將用到的區域變數、參數、返回位址暫存起來,因此會占用比較多的記憶體空間。
尾遞迴(tail recursion)
以往的作法會將運算結果儲存成區域變數,尾遞迴則是改將運算結果當成參數傳給下層的函式,這個情況稱為尾遞迴。
假設現在有個函式的功能是累加數字,用一般的遞迴寫法如下
1 | function recsum(x) { |
假如執行函式recsum(5)
,則 javaScript 的執行順序如下,函式需要等待下一個函式,直到中止條件後才開始回傳結果,然後將值一個一個加總起來
1 | recsum(5); |
改成尾遞迴的話,變成使用傳入參數的方式來記錄當下累積的數字是多少
1 | function tailrecsum(x, running_total = 0) { |
執行的順序如下,可以發現初次呼叫的時候就開始計算累加值,不需要用額外的變數去紀錄,減少 stack 的用量
1 | tailrecsum(5, 0); |
在查遞迴資料的時候一直看到這句話
遞迴只應天上有,凡人應當用迴圈
的確一開始接觸遞迴的時候,覺得有點超出大腦的負荷,因為遞迴不像迴圈那麼直觀好懂,直到看了大量的圖文解說,才比較理解遞迴的運作機制,其實遞迴能做的事,迴圈也可以做到,而且遞迴的執行時間還比較長甚至還比較佔用記憶體空間呢!但是遞迴的寫法通常會比迴圈來的更簡潔,可讀性更好,因此還是看使用的情境來評估要用遞迴或是迴圈來處理問題。
參考資料: 尾呼叫