Recursion
Recursion 的定義是一個會呼叫自己的函式。
Recursion 技巧在很多地方都有用到,例如:
JSON.stringify
&JSON.parse
- DOM traversal (
document.getElementsById
) - Object traversal (
Object.keys
)
在解釋 Recursion 之前先補充一下,函式的執行流程。
Call Stack
在大多數的程式語言中,函式的執行是由 Call Stack 控制的。
Stack 是一種資料結構,在之後的文章中會詳細說明。
函式要執行時,會放到 Call Stack 中,當函式執行到 return
或是結束後,會從 Call Stack 中移除 (pop)。
function wakeUp() {
console.log('Wake up!');
return 'Good morning!';
}
function goToWork() {
console.log('Go to work!');
eatFoods();
return 'Good afternoon!';
}
function eatFoods() {
console.log('Eat foods!');
}
function goBackHome() {
console.log('Go back home!');
return 'Good evening!';
}
function workDay() {
console.log('Work day!');
wakeUp();
goToWork();
goBackHome();
}
workDay();
以上函式執行時在 Call Stack 中的順序是:
- [
workDay
] - 執行workDay
- [
wakeUp
,workDay
] - 執行wakeUp
- [
goToWork
,workDay
] -wakeUp
執行結束,移除掉,執行goToWork
- [
eatFoods
,goToWork
,workDay
] - 執行eatFoods
- [
goBackHome
,workDay
] -eatFoods
執行結束,移除掉,goToWork
執行結束,移除掉,執行goBackHome
- [] -
goBackHome
執行結束,移除掉,workDay
執行結束,移除掉
而根據上述 Recursion 的定義,我們寫個粗糙的 Recursion 函式:
function recursion(n) {
console.log('recursion', n);
recursion(n + 1);
}
recursion(0);
呼叫 recursion
後,recursion
會不斷呼叫自己,所以在 Call Stack 中會不斷新增執行的函式:
[recursion
, recursion
, recursion
, recursion
, ....]
此時會遇到 Maximum call stack size exceeded 錯誤,因為 Call Stack 中的函式太多,超過了程式執行的限制。也就是俗稱的 Stack Overflow。
Important Points
- Base Case (終止條件,避免像上述例子一樣無窮進行下去)
- Different Input (每次呼叫的輸入要不一樣,否則會一直回傳同樣輸出造成永遠達不到終止條件)
function sumRange(n) {
if (n === 1) return 1;
return n + sumRange(n - 1);
}
sumRange(10);
// 55
上面 sumRange
例子中:
- 終止條件為
n === 1
- 每次呼叫
sumRange
時,n
都會遞減。
Practice 1 - Factorial
function factorial(n) {
let total = 1;
for (let i = 1; i <= n; i++) {
total *= i;
}
return total
}
上述是階乘函式,在數學中長這樣:4! = 4 * 3 * 2 * 1 = 24。
給定一個數字,計算出這個數字的階乘。
接著我們把這個函式改成 Recursion:
Solution
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
Helper Method Recursion
這是實作 Recursion 時的一種技巧,我們可以在主函式中定義好要使用的變數與寫一個 Helper 函式,並將變數帶入此 Helper 函式,實際上重複呼叫的是這個 Helper Recursion。
因為每次重複執行時,函式作用域內的變數都會重置,所以這技巧可以在外層 (主函式) 先定義好變數,再在內層 (Helper Recursion) 呼叫這個變數。
function collectOddValues(arr) {
let result = [];
function helper(helperInput) {
if (helperInput.length === 0) return;
if (helperInput[0] % 2 !== 0) {
result.push(helperInput[0]);
}
helper(helperInput.slice(1));
}
helper(arr);
return result;
}
collectOddValues([1, 2, 3, 4, 5, 6, 7, 8, 9]);
// [1, 3, 5, 7, 9]
先在主函式中定義好 result
,接著執行 helper
不斷遞迴陣列將奇數值放入 result
中。
Pure Recursion
和 Helper Method Recursion 一樣為了解決變數的重置問題。
function collectOddValues(arr) {
let newArr = [];
if (arr.length === 0) return newArr;
if (arr[0] % 2 !== 0) {
newArr.push(arr[0]);
}
return newArr.concat(collectOddValues(arr.slice(1)));
}
沒有內層函式,取而代之的是用 concat
將每次 collectOddValues
的結果合併起來回傳。
注意實作 Pure Recursion 時使用的陣列或字串方法也要是 Pure Function (無副作用,也就是不會改變 input),像
concat
會回傳新的陣列,並不會改變原本的陣列,splice
則會改變原本的陣列。
Practice 2 - isPalindrome
給定一個字串,判斷這個字串是否為 Palindrome (順寫與逆寫都一樣)。
例如:
isPalindrome('awesome') // false
isPalindrome('foobar') // false
isPalindrome('tacocat') // true
Solution
function isPalindrome(str) {
if (str.length <= 1) return true;
return str[0] === str[str.length -1] ? isPalindrome(str.slice(1, str.length - 1)) : false;
}
// 'tacocat'
// 抓第一個與最後一個比對,如果一樣就把這兩個字元去除接著繼續比對,否則回傳 false
// 'acoca'
// 'coc'
// 'o'
Practice 3 - Fibonacci
費氏數列是一個數列,第一與第二個數字是 1,以後的數字則是前兩個數字的和。 例如:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, …
給定一個數字 n
,計算出費氏數列第 n
個位置是什麼數字。
例如:
fibonacci(4) // 3
fibonacci(10) // 55
fibonacci(28) // 317811
fibonacci(35) // 9227465
Solution
function fibonacci(n) {
if (n <= 2) return 1;
return fibonacci(n - 1) + fibonacci(n - 2);
}
個人原本的解法:
function fib(num){
if (num === 0) return 0;
if (num <= 2) return 1;
function helper(index, previous1, previous2) {
if (index <= 1) return previous1 + previous2;
return helper(index - 1, previous2, previous1 + previous2);
}
return helper(num - 2, 1, 1);
}