iT邦幫忙

2023 iThome 鐵人賽

DAY 6
0

傳說迴圈可以用遞迴寫,code會比較優雅(elegant),遞迴與迴圈的差異在哪,什麼情境用什麼?基本上,兩種方法都是在做流程控制,只是切入點不同,先來比較一下兩種方法的基本思維。

遞迴的思維:我們不解決問題,我們讓問題解決它自己...

  • 把原始大問題拆成小問題,且小問題可以按相同模式拆成更小的問題
  • 大問題與小問題(視為上下層)有一定的關係
  • 最小的問題已經有答案,或與其他層不同的特殊解法
  • 用"小問題的答案"與"大問題和小問題間的關係"去解答最大的問題

用前輩舉的好例子來簡單說明,就像是你正在遊樂園的一個熱門設施排隊,隊伍非常長,你很想知道還要幾個人才會輪到自己,如果不要離開隊伍,有辦法算出自己到底排第幾個嗎?如果用遞迴的思維去想這個問題,我們可能會採取的作法是...

問前一個人他是第幾個

當然,你前面那個人也不知道他是第幾個,所以他又去問他前面那一個,一直問一直問,直到問到最前面的那一個,他會回答

"我是第一個" ==>這就是最小問題的答案

然後第二個人藉由大問題與小問題間的關係,就會知道自己是第二個,他回頭在告訴第三個人:ㄟ..我是第二個喔,然後後面的人就像骨牌一樣,一個一個獲得答案,最後你(也就是最大的問題)也獲得最後解答了

const 問前面的人=function() {
  if(前面沒有人) return 1; //base case
  else {
  return 問前面的人()+1 //呼叫自己,並利用問題間的關係回傳
  }

遞迴利用top-down的方式,把大問題逐漸拆分成小問題,如果是在問題本身具有遞迴性質,可以自然地分解為子問題的情境,比如樹結構、圖算法、拆分問題等,使用遞迴寫的程式會較易讀。

迴圈的思維:同樣的code不要讓我貼第二次!!

  • "大致相同"的事重複作好幾次
  • 想清楚什麼狀況下,會啟動一個迴圈。比如每圈都會改變的變數i的範圍(for迴圈);或者符合哪種條件下,會進入迴圈(while/ do while)

舉個例子,如果我們現在玩一個擲骰子的遊戲,想知道要都幾次才能丟到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
  1. 改成遞迴的第一種寫法,我個人稱為迴圈式遞迴:function內的if其實是執行條件,也就是符合條件時,重複呼叫自己
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) 的思維模式(一)


上一篇
各種迴圈for/while/do while的使用時機
下一篇
有關這個(this)的二三事(上)
系列文
超低腦容量學習法遇到javascript30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
Vic
iT邦新手 3 級 ‧ 2023-09-29 19:53:25

我要留言

立即登入留言