# Ch12 副函式與遞迴 > 搭配[green judge解題系統](http://www.tcgs.tc.edu.tw:1218/) > Special thanks to [台中女中sagit老師](http://www.tcgs.tc.edu.tw/~sagit/index.htm) \<\(\_ \_\)\> ## > 上一章:[大數運算](https://hackmd.io/s/r1KtmuMdf) > 下一章:[排序](https://hackmd.io/s/ry9twDVpW) > 回目錄:[國立科學園區實驗中學C++程式語言自學講義](https://hackmd.io/s/B18yT_i5Z) ## <font color='darkblue'>什麼是函式</font> 函式的英文是function 你可以把它想像成一塊有特定功能的程式 它不會被寫在main裡面 因為main本身也是個函式 話不多說 直接看這個例子 ```cpp= void say_hello() { cout<<"Hello"<<endl; } ``` 在上面的程式裡 我們 **宣告** 了一個函式,取名為say_hello 前面的 **void** 代表它並沒有要算出甚麼答案來(後面會有詳細解釋); 後面的 **小括弧()** ,是用來盛裝 **參數** 的,不過這個例子裡沒有參數; 底下大括弧裡包著的 ```cpp cout<<"Hello"<<endl; ``` 就是這個函式的 **定義** 簡而言之這個函式的功能呢 就是印出一句"Hello"而已 接下來只要把剛才那一塊函式放到main上面 我們就可以在main裡面 **呼叫** 它了 ```cpp= void say_hello() { cout<<"Hello"<<endl; } int main() { say_hello(); say_hello(); say_hello(); } ``` 這樣一來,就會在小黑窗上看到三個 Hello 至於為什麼說它是「副函式」呢? 因為main本身也是一個函式!! 你可以看到main跟它的格式是一樣的 而main就是一個程式的「主函式」(它都叫main了當然是主角) 相較於它,其他的就是「副」函式囉 ## <font color='darkblue'>關於函式的小括弧</font> 上一段有提到 **小括弧()** 是用來盛裝 **參數** 的 至於什麼是參數 可以看看以下的例子 ```cpp= void OAO(int x) { cout<<x<<endl; } int main() { OAO(123); OAO(1000); OAO(-1); } ``` 小括弧內的「參數」就是那個int x 程式裡面會用到的x就來自於小括弧裡的x 所以OAO這整個函式的功能就是 「拿一個數字x,並且把這個數字x給印出來」 在main裡面 首先我寫了OAO(123) 那麼OAO這個函式就被我「呼叫」了 並且我在呼叫的時候讓它的x先變成123 所以他就會幫我印出123這個數字 而接下來的OAO(1000)與OAO(-1)也是一樣的 所以你最後會在小黑窗上看到 123 1000 -1 在剛才的例子中,只用了一個 **參數** x 但其實不管用幾個都可以 要用int或是long long或是double或是string或是各種型態都可以 以下示範兩個int參數 ```cpp= void add(int x, int y) { cout << x+y <<endl; } int main() { add(1,2); add(4,5); add(100,200); } ``` add這個函式呢 他有兩個 **參數** 分別是 **int x** 與 **int y** 而這個函式的 **定義** 則是 把傳入的兩個參數x與y加起來後印出來 在main裡面 **呼叫** 函式的時候 就記得要傳兩個參數進小括弧裡 如果只傳一個 例如呼叫add(1) 或是傳了三個 例如呼叫add(1,2,3) 就會噴錯誤喔 這樣的程式執行起來 會在小黑窗上看到印出的 3 9 300 ## <font color='darkblue'>關於函式的型態與答案</font> 在剛才的例子中 我們函式最前面都是寫 **void** 它的意思就是說 「這個函式並沒有要**回傳**什麼答案回來」 你可以看到上面的例子就只是單純印出一些東西而已 接下來要介紹讓函式 **回傳** 答案 既然他要回傳答案 <font color='red'>那就依照他要回傳答案的型態來把void取代掉</font> 例如我想寫個函式,傳入x之後,讓函式把他加三之後的結果 **回傳** 給我 既然要回傳的是整數 那麼我這個函式的型態就是個整數 也就是函是的最一開始我要寫個int ```cpp= int add3(int x) { int answer = x+3; return answer; } int main() { int Q = 5; int P = add3(Q); cout << P << endl; } ``` 上面那個add3的函式 傳入一個x之後 他會用answer來儲存x+3之後的結果 並且把answer**回傳**過去 <font color='red'>也就是要寫個**return** 加上答案</font> 在main裡面呼叫add3這個函式時 將Q傳進去 並且把add3回傳的結果儲存在P裡面 由於Q是5 所以add3回傳的答案會是8 因此在cout<<P之後 我們會在小黑窗上看到一個數字8 當然也可以傳兩個以上的變數進去 不過回傳只能回傳一個 以下示範將兩數字相加的函式 ```cpp= int add(x, y) { int ans = x+y; return ans; } int main() { int a, b; cin >> a; cin >> b; cout << add(a, b); } ``` 它的本質上會和下面這個是一樣的 ```cpp= int main() { int a, b; cin >> a; cin >> b; cout << a+b; } ``` 要練習使用函式 我們來複習很久以前寫過的題目:輸入H與W,輸出H\*W 以前是直接在main裡面算答案 現在練習看看把運算的過程包到別的函式裡 讓main來呼叫它就好 > <font color="darkgreen"> 【學生練習題】</font> > - [ ] [Green Judge a005: 矩形面積 ](http://www.tcgs.tc.edu.tw:1218/ShowProblem?problemid=a005) 函式裡面當然不是只能傻傻地做加減乘除 我們以往學過的條件判斷、迴圈等等 在函式內都能寫 簡而言之函式裡面一切都可以正常寫程式 <font color='darkorange'>【例題】</font> > 輸入一個數字N > 如果N為1,那就印出1 > 否則,如果N是奇數,那就印出3N+1 > 否則,如果N是偶數,那就印出N/2 ```cpp= int f(int N) { if(N==1) return 1; else if(N%2==1) return (3*N+1); else return N/2; } int main() { int n; cin >> n; cout << f(n); } ``` 來試試用相同的f函式來解3N+1 > <font color="darkgreen"> 【學生練習題】</font> > - [ ] [Green Judge a023: 3N+1 ](http://www.tcgs.tc.edu.tw:1218/ShowProblem?problemid=a023) 總之 如果函式的功能是拿來算出答案的 就必須要在最前面寫上要算的答案的形態 該是int就是int 該是string就是string...等等 且函式的最後面要記得寫return 加上答案 如果沒有要算出答案 就在最開頭寫void就好 並且最後不需return答案 ## <font color='darkblue'>關於函式的特別性質</font> 猜猜看,執行以下的程式時,會跑出甚麼 ```cpp= void to100(int x) { x=100; } int main() { int a=3; to100(a); cout << a << endl; } ``` 3? 或是100? 答案是,還是會跑出3喔 因為執行函式時 函式裡面的int x算是自己另外宣告的變數 所以就算把x變成100了 傳進去的數字也不會變成100 所以不用擔心變數傳進函式裡會被改變掉!! ## <font color='darkblue'>函式的呼叫</font> 前面的範例都是在main裡面呼叫函式 但其實函式裡面也可以呼叫函式喔 像這樣 ```cpp= void IPass() { cout<<"PASS"; } void IFail() { cout<<"FAIL"; } void sayResult(int score) { if(score>=60){ IPass(); } else{ IFail(); } } int main() { int score; cin >> score; sayResult(score); } ``` 在main裡面呼叫了sayResult 而在sayResult裡面會呼叫IPass或是IFail 總而言之 函式不一定要被main呼叫 被別的函式呼叫也可以 那麼 在自己裡面呼叫自己可以嗎!? 像是這樣 ```cpp= void sayHi() { cout<<"Hi"; sayHi(); } ``` 當然可以喔!! 雖然有點不直覺 但是一旦熟悉之後可以用它來達成很多神奇的事情 他就是 - - 接下來要介紹的 **遞迴** ## <font color='darkblue'>遞迴的概念</font> 呼叫副函式就像做夢一樣 又因為副函式的內容是定義好的 所以你可以把它想成套路固定的夢 每次作夢的時候帶一些物品進去(參數) 夢醒後會帶回一個答案(回傳) 而遞迴就像是夢中夢 你已經帶著物品到了夢中 在夢中的套路卻還要再做個夢 在程式中的寫法 就是讓函式裡面有「自己呼叫自己」的橋段 像是這樣 ```cpp= void sayhello() { cout << "hello" << endl; sayhello(); } ``` 這個函式印出一個hello之後 會再呼叫自己一次 就像是一個說哈囉的夢 在夢中說完哈囉還會躺上床再做一個一模一樣的夢中夢 在夢中夢說哈囉 然後在夢中夢躺床做夢中夢中夢... 你可能會懷疑 夢的套路中都有再做一個夢 那不就一直夢中夢下去不會醒來了嗎 對 沒錯 你把這段程式拿去執行 ```cpp= void sayhello() { cout << "hello" << endl; sayhello(); } int main() { sayhello(); } ``` 會發現它一直印hello印個沒完 直到程式當掉為止 我們必須修改程式 讓它在某些條件下才要呼叫自己(夢中做夢) 這樣一來才有醒來的契機 至於要甚麼條件呢 這也是在程式裡面決定的 接下來要示範的程式是「限定夢中夢做到5層就結束」 所以它的行為應該要像是 夢->夢->夢->夢->夢->醒->醒->醒->醒->醒 那怎麼知道5次了沒呢 就用傳進來的 **參數** 來記錄吧 ```cpp= void sayhello(int x) { cout << "hello" << endl; if(x<5) sayhello(x+1); } int main() { sayhello(1); } ``` <font color='darkorange'>【例題】</font> > 輸入一個N > 輸出N! 既然想算出N! 假設已經知道(N-1)!是多少了 我們就直接把那個(N-1)!拿去乘以N就是N! 為了算出(N-1)! 只需要呼叫自己一次就好 只要自己的函式內容是對的 就不需要擔心(N-1)!會不會被算錯了 就像每次睡覺(呼叫函式)時 不需要為了夢的內容怎麼運作而操心 只要帶正確的物品進去 並把它回傳的答案接住就好 這個夢要算出N! 那就在這個夢中再做個夢算出(N-1)! 把那個夢中夢的答案拿來乘以N就是我這個夢的答案 但若是N已經是1了 實在沒有必要再利用做夢來算出答案 直接回傳1就好 所以算N!的程式可以寫成這樣 ```cpp= int fac(int N) { if(N==1) return 1; else return N*fac(N-1); } ``` 我不需要知道N!的本質是甚麼 我只需要知道N!和(N-1)!之間的關係就好 也就是算出(N-1)!之後再乘以N就是N! 遞迴也是這樣 我根本不需要知道整個運算怎麼變出來的 我只需要知道怎麼利用我呼叫自己以後回傳的結果就好 ## <font color='darkblue'>經典問題:費波那契數列</font> <font color='darkorange'>【例題】</font> > 費波那契數列的定義如下 > 第0項是0 > 第1項是1 > 之後每一項都是他前一項和前兩項的和 > 例如它的前10項為:0,1,1,2,3,5,8,13,21,34 它定義都告訴你了 那程式就照寫就好囉 ```cpp= int f(int n) { if(n==0) return 0; else if(n==1) return 1; else return f(n-1)+f(n-2); } ``` 遞迴的好處在這裡 例如f(5),我並不需要知道怎麼憑空生出f(5)的答案 我只需要知道它是f(3)+f(4)就好 至於f(3)和f(4)我也無法憑空生出來 反正呼叫自己來算就好 既然既然有把函式的定義寫對 就可以完全相信它回傳的答案會是對的!! <font color='darkorange'>【例題】</font> > 輸入一個n > 輸出在算出n的費波那契時,總共呼叫了幾次函式 > 例如n為0或1時,因為沒有夢中夢,所以答案都是1次 > n為2時,需要分成n為0和n為1各算一次,再加上自己這次,所以是1+1+1=3 > n為3時,需要分成n為1和n為2各算一次,再加上自己這次,所以是1+3+1=5 n若不是0或1,就會是n-1的答案加上n-2的答案,再加上這次的1 ```cpp= int Q(int n) { if(n==0) return 1; else if(n==1) return 1; else return Q(n-1)+Q(n-2)+1; } ``` > <font color="darkgreen"> 【學生練習題】</font> > - [ ] [Green Judge b022: 費氏數列 ](http://www.tcgs.tc.edu.tw:1218/ShowProblem?problemid=b022) 這題就是剛才介紹的f和Q兩個函式喔 ## <font color='darkblue'>經典問題:河內塔</font> ![](https://i.imgur.com/k2SUSDs.png) 圖片來自維基百科 這個玩具的玩法是這樣的 有三根柱子 第一根柱子放滿了圓盤 最小的在上面 最大的在下面 想要每次移動一個圓盤 來把所有的圓盤都移到第3根柱子上 其中移動的過程中每次只能拿一個 且絕對不能有大圓盤蓋在小圓盤上的情況發生 由於為了讓大圓盤不蓋在小圓盤上 會需要有第2根柱子來當成中繼站 例如可以把小圓盤先放在中繼站 把大圓盤放到目的地後 在把小圓盤疊到目的地上 這樣一來就可以移動這兩個大小圓盤 又可以不會讓大的蓋小的 搬的步驟由你來設計 但希望讓步驟最少 給你圓盤的總量 請你印出移動的步驟 例如當圓盤的總量是2時 印出 Ring 1 from 1 to 2 Ring 2 from 1 to 3 Ring 1 from 2 to 3 意思是 先把一號圓盤(最小的圓盤)搬到中繼站 再把二號圓盤(最大的圓盤)搬到目的地 最後再把一號圓盤從中繼站搬到目的地 雖然看起來很簡單 但n大起來時會變非常複雜喔 像是這個圖片是n為3時的解答 但它怎麼知道該這樣做呢? 當然是程式拉 ![](https://i.imgur.com/y3s3sNO.png) (取自家象) 「將3個盤子從A移到C」 可以想成 「將上面2個盤子用『不知名神奇方法』從A移到B」 「將第3個盤子直接從A移到C」 「將上面2個盤子用『不知名神奇方法』從B移到C」 我不需要知道上面2個盤子是怎麼移的 我只需要相信它會神奇的幫我移好 所以我要做的事情是 1. 呼叫自己函式,把第1至第n-1個盤子從起點移到中繼站 2. 手動移動第n個盤子,從起點到終點 3. 呼叫自己函式,把第1至第n-1個盤子從中繼站移到終點 還要考慮最邊界的條件: 當然如果盤子只有一個 那直接移到目的地就好 其中哪根柱子是起點、哪根是目的地、哪根是中繼站 也要從參數傳個仔細,來填填看吧 ```cpp= void hanoi(int n, int source, int destination, int temp) { if(n==1){ cout << "Ring 1 from " << source << " to " << destination << endl; } else{ hanoi(n-1, ___, ___, ___); cout << "Ring " << n << " from " << source << " to " << destination << endl; hanoi(n-1, ___, ___, ___); } } int main() { int n; cin >> n; hanoi(n, ___, ___, ___); } ``` > <font color="darkgreen"> 【學生練習題】</font> > - [ ] [Green Judge b023: 河內塔 ](http://www.tcgs.tc.edu.tw:1218/ShowProblem?problemid=b023) 盡量不要複製變填充題 自己寫更有意思哦! ## > 上一章:[大數運算](https://hackmd.io/s/r1KtmuMdf) > 下一章:[排序](https://hackmd.io/s/ry9twDVpW) > 回目錄:[國立科學園區實驗中學C++程式語言自學講義](https://hackmd.io/s/B18yT_i5Z)