<style> html, body, .ui-content { background-color: #333; color: #ddd; } body > .ui-infobar { display: none; } .ui-view-area > .ui-infobar { display: block; } .markdown-body h1{ color: #9CCEF2; } .markdown-body h2{ color: #B1D6CA; } .markdown-body h3{ color: #F5F6B6; } .markdown-body h4, .markdown-body h5, .markdown-body h6 { color: #ddd; } .markdown-body h1, .markdown-body h2 { border-bottom-color: #ffffff69; } .markdown-body h1 .octicon-link, .markdown-body h2 .octicon-link, .markdown-body h3 .octicon-link, .markdown-body h4 .octicon-link, .markdown-body h5 .octicon-link, .markdown-body h6 .octicon-link { color: #fff; } .markdown-body img { background-color: transparent; } .ui-toc-dropdown .nav>.active:focus>a, .ui-toc-dropdown .nav>.active:hover>a, .ui-toc-dropdown .nav>.active>a { color: white; border-left: 2px solid white; } .expand-toggle:hover, .expand-toggle:focus, .back-to-top:hover, .back-to-top:focus, .go-to-bottom:hover, .go-to-bottom:focus { color: white; } .ui-toc-dropdown { background-color: #333; } .ui-toc-label.btn { background-color: #191919; color: white; } .ui-toc-dropdown .nav>li>a:focus, .ui-toc-dropdown .nav>li>a:hover { color: white; border-left: 1px solid white; } .markdown-body blockquote { color: #bcbcbc; } .markdown-body table tr { background-color: #5f5f5f; } .markdown-body table tr:nth-child(2n) { background-color: #4f4f4f; } .markdown-body code, .markdown-body tt { color: #eee; background-color: rgba(230, 230, 230, 0.36); } a, .open-files-container li.selected a { color: #5EB7E0; } </style> ###### tags: `tgirc早修book` # 遞迴 大雄在房裏,用時光電視看著從前的情況。電視畫面中的那個時候,他正在房裏,用時光電視,看著從前的情況。電視畫面中的電視畫面的那個時候,他正在房裏,用時光電視,看著從前的情況…… (無限重複 on) ## 遞迴基本概念 遞迴是一種將大問題拆解成小問題並分項解決的方式。 其適用於各種「可以整理出與前項的關係式」的狀況。 作法大致上是整理成出一個遞迴通式,使其每項都固定與前 n 項有關。 以數學式表達就像這樣: f(n) = a ‧ f(n-1) + b ‧ f(n-2) + … 每一項都會固定與前幾項有特定關係,這樣整理出來後,要實作遞迴程式就方便的多了 ### 例一:1 ~ n 的總和 有一函數 f(x) = 1+2+3+ … +x 求給定 x 值的 f(x) 程式如下: ```cpp= #include <iostream> using namespace std; int f(int x){ if(x == 1) return 1; //如果x=1,那麼f(x)=1 return f(x-1)+x; //如果x不是1,那麼 f(x) = f(x-1)+x //而 f(x-1) 就是直接將 x-1 作為參數傳入 f(),並回傳 f(x-1) 的值 //即使是在 f() 內也可以呼叫自己 } int main(){ int x; cin >> x; cout << f(x) << "\n"; return 0; } ``` 首先,先觀察前幾項的規律 ``` f(1) = 1 f(2) = 1 + 2 = f(1) + 2 f(3) = 1 + 2 + 3 = f(2) + 3 f(4) = 1 + 2 + 3 + 4 = f(3) + 4 . . . ``` 可以觀察到如下規律: ``` f(x) = 1 (x = 1 時) f(x) = f(x-1) + x (x > 1 時) ``` 第一條式子就是<font color="F5F6B6">**邊界條件**</font>,這項 f(x) 沒有前一項,所以需要獨立討論。 而第二條式子就是一般情況下,f(x) 所遵循的規則。 程式實作常用的作法是在函數中呼叫函數本身,並訂定邊界條件。 流程大致上是主程式呼叫 f(x),而 f(x) 中需要用到 f(x-1),所以呼叫f(x-1),但在 f(x-1) 中又會用到 f(x-2),所以呼叫 f(x-2)…… 就這樣一路呼叫到 f(2)=f(1)+2 時,f(1)=1 所以回傳 1。 這時 f(2) 就有了 1+2 = 3 這個值,並將 3 回傳給 f(3) = f(2)+3 = 3+3 = 6,然後是f(4) = f(3)+4 = 6+4 = 10…… 大部分的遞迴都可以拆成這樣的步驟來進行。 ### 例二:費氏數列 費氏數列(費波那契數列) -> [相關介紹](https://zh.wikipedia.org/wiki/%E6%96%90%E6%B3%A2%E9%82%A3%E5%A5%91%E6%95%B0) 費氏數列是一段第 0 項為 0、第 1 項為 1,之後的項數都由前面兩項相加而成的數列 -> 0 , 1 , 1 , 2 , 3 , 5 , 8 , 13 , 21 , ... 輸入 x,求費氏數列的第 x 項 ```cpp= #include <iostream> using namespace std; int f(int x){ if(x == 0) return 0; else if(x == 1) return 1; return f(x-1) + f(x-2); //前兩項之和 } int main(){ int x; cin >> x; cout << f(x) << '\n'; return 0; } ``` ## 兩種實作方式 這裡以階乘為例,示範兩種實作遞迴的方式,<font color="F5F6B6">Top-down</font> 以及 <font color="F5F6B6">Bottom-up</font>。 階乘的定義如下: ``` !1 = 1 !2 = 1 ‧ 2 !3 = 1 ‧ 2 ‧ 3 !4 = 1 ‧ 2 ‧ 3 ‧ 4 !n = 1 ‧ 2 ‧ 3 ‧ … ‧ n ``` 輸入 n,求 n 的階乘 ### Top-down Top-down 意即 <font color="F5F6B6">**從第n項開始,一路往回求第1項**</font>。 通常 Top-down 會利用副程式自我呼叫來實作。 剛剛上面提到的兩題,其實就是以 Top-down 的方式完成的。 先寫出一個能解決問題的函數,再以遞迴方式完成 ```cpp= int f(int n){ if(n == 0) return 1; return n * f(n-1); //由 f(n-1) 往回推 } ``` :::info 因為系統本身是有限制的,當遞迴過深時會導致 overflow ::: ### Bottom-up Top-down 是由最後方往回推,而 Bottom-up 則是與之相反。 也就是 <font color="F5F6B6">**從第1項開始,一路向後推至第n項**</font>。 通常 Bottom-up 會利用 for迴圈 搭配陣列或是變數來實作。 ```cpp= for(int i=1;i<=n;i++){ num[i]=num[i-1]*i; } ``` ## 相較於 Top-down,Bottom-up 的效率比較好,只有極少數情況才會出錯,並且風險也比較低,所以建議可以多多練習 Bottom-up,但 Top-down 的用法也需學會