作者:王一哲
第1版:2021年10月21日
第2版:2023年4月9日
第3版:2025年3月28日,補充 C++ 的語法
我們可以利用自訂函式將某一段程式碼取個名字,並設定好它的回傳值,接下來就可以在後續的程式碼中直接呼叫自訂函式,這樣可以使程式碼較為簡潔。自訂函式的語法如下
def 函式名稱(輸入參數):
函式內要執行的程式碼
...
return 回傳值
自訂函式的關鍵字是 def,也就是 define 的前 3 個字母。函式名稱的部分不能使用系統保留字。輸入參數則是在呼叫函式時傳給函式的變數,如果不需要輸入參數時可以什麼都不加。return 之後是函式回傳值,可以沒有回傳值,也可以有多個回傳值,當程式執行完 return 之後就會離開自訂函式。
我們可以用以下的程式碼自訂名為 hello 的函式,這個函式不需要輸入參數,功能就是印出 Hello World!,也沒有回傳值。
def hello():
print("Hello World!")
如果要在後續的程式中呼叫 hello,寫法為
hello()
請注意,hello 後面的 () 不能省略。執行時會輸出
Hello World!
如果用以下方式呼叫
for i in range(5):
hello()
輸出為
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
補充 C++ 的寫法。
#include <iostream>
using namespace std;
void hello() {
cout << "Hello World!\n";
return; // 可以不加
}
int main() {
for(int i=0; i<5; i++) hello();
return 0;
}
我們可以用以下的程式碼自訂名為 sub 的函式,這個函式需要輸入參數a、b,功能是回傳 a-b 的計算結果。
def sub(a, b):
return a-b
如果要在後續的程式中呼叫 sub,則輸入參數為 2、3,寫法為
sub(a=2, b=3)
sub(b=3, a=2)
sub(2, 3)
請注意,如果輸入參數不加上名稱時會按照輸入的順序分別傳給 a、b。執行時回傳值皆為
-1
在自訂函式時也可以設定輸入參數的預設值,例如
def sub(a=2, b=3):
return a-b
呼叫 sub 時可以輸入參數,如果沒有輸入參數則會自動採用預設值,例如以下的寫法回傳值為 4。
sub(a=5, b=1)
若改為這個寫法則回傳值為 -1。
sub()
如果改成以下的寫法
def sub(a, b):
return a-b, a, b
則回傳值會有 3 個。如果用以下方式呼叫 sub
c = sub(a=2, b=3)
則 c 的資料類別為元組 (tuple),變數值為
(-1, 2, 3)
如果想要將回傳值分別指定給變數 d、e、f,寫法為
d, e, f = sub(a=2, b=3)
d、e、f 的變數值分別是 -1、2、3。
補充 C++ 的寫法。
#include <iostream>
using namespace std;
int sub(int a, int b) { // 參數沒有預設值
return a-b;
}
int sub2(int a=2, int b=3) { // 參數有預設值
return a-b;
}
int main() {
cout << sub(2, 3) << "\n"; // 印出 -1
cout << sub2(5, 3) << "\n"; // 印出 2
cout << sub2() << "\n"; // 印出 -1
return 0;
}
函式的遞迴是指於自訂函式中呼叫自己或是其它自訂函式,如果使用得當可以大幅減少程式碼,但是使用不當會讓使用者很難看出程式碼的運作流程。使用函式的遞迴,一定要留下遞迴出口,在符合條件時能夠離開自訂函式,才不會形成無窮迴圈。以下是3個於自訂函式中呼叫自己的例子。
如果要計算從 1 加到 n 的正整數總合,可以定義以下的函式,將一個 for 迴圈包在裡面,回傳加總的結果。
def myfunc(n):
total = 0
for i in range(1, n+1):
total += i
return total
ans = myfunc(995)
print(ans) # 印出 495510
使用函式遞迴的寫法如下:
def myfunc(n):
if n == 1: return 1
return n + myfunc(n-1)
ans = myfunc(995)
print(ans) # 印出 495510
若用
但是遞迴的寫法有一個缺點,如果遞迴次數過多、深度太深,程式會停止運作。
補充 C++ 的寫法。
#include <iostream>
using namespace std;
int myfunc(int n) {
int total = 0;
for(int i=1; i<=n; i++) total += i;
return total;
}
int myfunc2(int n) {
if (n == 1) return 1;
return n + myfunc2(n-1);
}
int main() {
cout << myfunc(995) << "\n"; // 印出 495510
cout << myfunc2(995) << "\n"; // 印出 495510
return 0;
}
正整數的階乘是指小於、等於此正整數的所有正整數乘積,另外定義0的階乘為1,以下是幾個例子
由以上幾個例子可得
如果想要用 Python 寫出計算
def factorial(n):
result = 1
for i in range(1, n+1): result *= i
return result
print(factorial(10)) # 印出 3628800
使用函式遞迴的寫法如下:
def factorial(n):
if n == 0: return 1
return n*factorial(n-1)
print(factorial(10)) # 印出 3628800
這個寫法不需要在自訂函式中包進一個 for 迴圈,若用
補充 C++ 的寫法。
#include <iostream>
using namespace std;
int factorial(int n) {
int result = 1;
for(int i=1; i<=n; i++) result *= i;
return result;
}
int factorial2(int n) {
if (n == 0) return 1;
return n * factorial2(n-1);
}
int main() {
cout << factorial(10) << "\n"; // 印出 3628800
cout << factorial2(10) << "\n"; // 印出 3628800
return 0;
}
若以
若
如果想要用 Python 寫出計算
def fibonacci(n):
a, b = 0, 0
for i in range(1, n+1):
if i == 1 or i == 2:
a, b = 0, 1
else:
a, b = b, a+b
return a+b
print(fibonacci(10)) # 印出 55
使用函式遞迴的寫法如下:
def fibonacci(n):
if n == 0: return 0
if n == 1: return 1
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(10)) # 印出 55
這個寫法不需要在自訂函式中包進一個 for 迴圈,而且幾乎就是按照數學定義寫自訂函式內容,反而比不使用函式遞迴的寫法簡單、易懂。
補充 C++ 的寫法,不使用遞迴。
#include <iostream>
using namespace std;
int fibonacci(int n) {
int a = 0, b = 0;
for(int i=1; i<=n; i++) {
if (i == 1 || i == 2) {
a = 0; b = 1;
} else {
int c = a+b;
a = b; b = c;
}
}
return a+b;
}
int main() {
cout << fibonacci(10) << "\n"; // 印出 55
return 0;
}
遞迴。
#include <iostream>
using namespace std;
int fibonacci(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
return fibonacci(n-1) + fibonacci(n-2);
}
int main() {
cout << fibonacci(10) << "\n"; // 印出 55
return 0;
}
我通常不會這樣寫程式,這會使程式碼難以理解。以下的例子來源為 APCS 105年3月觀念題第14題,再改寫成 Python 格式。
def foo(i):
if i <= 5:
print("foo: {:d}".format(i))
else:
bar(i - 10)
def bar(i):
if i <= 10:
print("bar: {:d}".format(i))
else:
foo(i - 5)
foo(6693)
因為代入的數值很大,要先找出運作規律,例如foo(100) = bar(90) = foo(85) 或是 bar(100) = foo(95) = bar(85),如果經過 foo 及 bar 各1次,代入的數值減15,當代入的數值小於、等於20可能會結束遞迴。由於 6693 % 15 = 3,因此 foo(6693) = foo(18) = bar(8),8 <= 10,印出 bar: 8,結束遞迴。原版的題目中有一項是 foo(15106),如果用 Python 程式碼測試時,會遇到遞迴深度過深的問題,無法得到運算結果,但如果改寫成 C++ 程式碼則可以得到運算結果。
#include <iostream>
using namespace std;
void foo(int);
void bar(int);
int main() {
foo(15106);
bar(3091);
foo(6693);
return 0;
}
void foo(int i) {
if (i <= 5) cout << "foo: " << i << endl;
else bar(i - 10);
}
void bar(int i) {
if (i <= 10) cout << "bar: " << i << endl;
else foo(i - 5);
}
有些題目的測資數量很多,會使得遞迴的次數過多、深度太深,程式碼在測試時可能會超時。以 ZeroJudge 基礎題 a216. 數數愛明明 為例,如果按照題目的定義使用函式遞迴,
會寫出以下的程式碼,測試時會超時。
import sys
def f(n):
if n == 1: return 1
return n + f(n-1)
def g(n):
if n == 1: return 1
return f(n) + g(n-1)
for line in sys.stdin:
n = int(line)
print(f(n), g(n))
但如果改用 C++,變數格式要使用 long,否則會溢位,使用時間約為 0.5 s,記憶體約為 328 kB,通過測試。
#include <iostream>
using namespace std;
long f(long n) {
if (n == 1) return 1;
return n + f(n-1);
}
long g(long n) {
if (n == 1) return 1;
return f(n) + g(n-1);
}
int main() {
long n;
while(cin >> n) cout << f(n) << " " << g(n) << endl;
return 0;
}
所以遇到遞迴的題目時,可以試著找出一般式。先求
將以上式子相加可得
接下來求
將以上式子相加可得
直接將測資指定的
import sys
def f(n):
return int(n*(n+1)/2)
def g(n):
return int(n*(n+1)*(n+2)/6)
for line in sys.stdin:
n = int(line)
print(f(n), g(n))
以下的 C++ 程式碼在 ZeroJudge 測試時,使用時間約為 2 ms,記憶體約為 328 kB,通過測試。
#include <iostream>
using namespace std;
long f(long n) {
return n*(n+1)/2;
}
long g(long n) {
return n*(n+1)*(n+2)/6;
}
int main() {
long n;
while(cin >> n) cout << f(n) << " " << g(n) << endl;
return 0;
}
Python
、C++