傳說迴圈可以用遞迴寫,code會比較優雅(elegant),遞迴與迴圈的差異在哪,什麼情境用什麼?基本上,兩種方法都是在做流程控制,只是切入點不同,先來比較一下兩種方法的基本思維。
用前輩舉的好例子來簡單說明,就像是你正在遊樂園的一個熱門設施排隊,隊伍非常長,你很想知道還要幾個人才會輪到自己,如果不要離開隊伍,有辦法算出自己到底排第幾個嗎?如果用遞迴的思維去想這個問題,我們可能會採取的作法是...
問前一個人他是第幾個
當然,你前面那個人也不知道他是第幾個,所以他又去問他前面那一個,一直問一直問,直到問到最前面的那一個,他會回答
"我是第一個" ==>這就是最小問題的答案
然後第二個人藉由大問題與小問題間的關係,就會知道自己是第二個,他回頭在告訴第三個人:ㄟ..我是第二個喔,然後後面的人就像骨牌一樣,一個一個獲得答案,最後你(也就是最大的問題)也獲得最後解答了
const 問前面的人=function() {
if(前面沒有人) return 1; //base case
else {
return 問前面的人()+1 //呼叫自己,並利用問題間的關係回傳
}
遞迴利用top-down的方式,把大問題逐漸拆分成小問題,如果是在問題本身具有遞迴性質,可以自然地分解為子問題的情境,比如樹結構、圖算法、拆分問題等,使用遞迴寫的程式會較易讀。
舉個例子,如果我們現在玩一個擲骰子的遊戲,想知道要都幾次才能丟到1(次數最少的就獲勝之類的),這就比較適合用迴圈寫:
const 需丟幾次才丟到1=function() {
let 丟擲總次數=0;
let 丟擲結果;
do{
丟骰子以獲得丟擲結果;
丟擲總次數加1
}
while(丟擲結果不是1)
return 丟擲總次數
迴圈利用bottom-top的方式,一次一次地逼近結果,需要想清楚每次迭代會發生什麼事,同時能更精確控制每次迭代的變化。
有看過兩種遞迴的寫法,主要差別在於終止條件的寫法,來個例子比如說
問題描述
寫個函式可以印出從指定值倒數至零,比如說5,即印出5,4,3,2,1,0
先用for寫是這樣的
const countDown = (num) => {
for (let i = num; i >= 0; i = i - 1) {
console.log(i);
}
};
countDown(5); //5 4 3 2 1 0
const countDownRecu = (num) => {
console.log(num);
num--;
if (num >= 0) {
countDownRecu(num);
}
};
countDownRecu(5); //5 4 3 2 1 0
2.改成遞迴的第二種寫法,是百分百純正血統遞迴寫法:function內的if是終止條件,也就是base case(有特殊解法的最小問題,這邊講的特殊解法指的是與其他層的問題相比)
const countDownRecu = (num) => {
if (num === 0) console.log(0);
else {
console.log(num);
num--;
countDownRecu(num);
}
};
觀察以上能發現兩者重疊之處,在於遞迴呼叫函式自身像是重複執行一段code,與迴圈很像。
但兩者在記憶體的使用方式卻有很大的差別,遞迴會需要在每次函數調用時將當前的狀態壓入函數調用堆疊,所以會累積大量的call stack,在某些情況下,尤其是當遞迴深度很大時,容易發生堆疊溢出而報錯(stack overflow);而迴圈則是使用固定的記憶體,每次循環迭代都可以重複使用相同的變數,而不會佔用額外的記憶體。一般而言,迴圈通常比遞迴更節省記憶體。
何時使用遞迴:
總之,選擇哪種方法取決於問題的性質、性能需求和程式碼的可讀性。在實際應用上,通常需要根據具體情況來決定使用哪種方法。有時候,甚至可以將它們結合起來以充分發揮各自的優勢。
Reference
一次看懂遞迴 (Recursion) 的思維模式(一)