06. Fungsi
Fungsi adalah potongan kode yang diberi nama, bisa dipanggil berkali-kali, dan bisa diuji sendiri. Di OSN, fungsi dipakai untuk memecah soal menjadi bagian-bagian kecil yang masing-masing mudah dipastikan benar.
Bentuk sebuah fungsi
int kuadrat(int x) {
return x * x;
}
int main() {
cout << kuadrat(5) << "\n"; // 25
return 0;
}Bagian-bagiannya:
| Bagian | Contoh | Arti |
|---|---|---|
| tipe hasil | int |
tipe nilai yang dikembalikan |
| nama | kuadrat |
nama untuk memanggilnya |
| parameter | (int x) |
masukan fungsi, boleh kosong atau lebih dari satu |
return |
return x * x; |
nilai yang dikirim balik ke pemanggil |
Yang terjadi saat kuadrat(5) dipanggil, berurutan:
- Nilai
5disalin ke dalam parameterx. - Pengerjaan
mainberhenti sementara, dan pengerjaan berpindah ke dalamkuadrat. return x * x;menghitung25dan mengirimkannya kembali.- Pengerjaan kembali ke
main, tepat di tempat pemanggilan tadi, dan seluruh tulisankuadrat(5)digantikan nilai25.
Karena itu pemanggilan fungsi boleh diletakkan di mana pun sebuah
nilai boleh berada: di dalam cout, di dalam
if, bahkan sebagai masukan fungsi lain seperti
kuadrat(kuadrat(2)).
return juga langsung mengakhiri
fungsinya. Baris apa pun sesudah return yang sudah
dijalankan tidak akan dikerjakan:
int uji(int n) {
if (n < 0) return -1; // langsung keluar
return n * 2; // hanya tercapai kalau n >= 0
}Sifat ini dipakai untuk keluar lebih awal dari fungsi pemeriksa, dan
akan sering kamu lihat pada bool prima(...) di PR bab
ini.
Fungsi harus ditulis di atas tempat ia dipanggil, atau dideklarasikan lebih dulu:
int kuadrat(int x); // deklarasi (prototipe)
int main() {
cout << kuadrat(5) << "\n";
}
int kuadrat(int x) { // definisi, boleh di bawah
return x * x;
}Fungsi tanpa nilai kembali
Kalau fungsi hanya mengerjakan sesuatu (biasanya mencetak), tipenya
void:
void garis(int panjang) {
for (int i = 0; i < panjang; i++) cout << "-";
cout << "\n";
}Beberapa parameter
int maksimum(int a, int b) {
return (a > b) ? a : b;
}
bool di_dalam(int x, int bawah, int atas) {
return x >= bawah && x <= atas;
}Fungsi yang mengembalikan bool sangat berguna: namanya
menjelaskan maksud kondisi, sehingga
if (di_dalam(x, 1, 100)) langsung terbaca.
Buka
exercises/06/01-fungsi.cpp.
Tambahkan fungsi int kubik(int x) dan panggil dari
main.
Variabel lokal dan cakupan
Variabel yang dibuat di dalam fungsi hanya hidup di dalam fungsi itu.
void f() {
int x = 10; // hanya ada di dalam f
}
int main() {
// cout << x; // error: 'x' was not declared in this scope
}Hal yang sama berlaku untuk blok { ... } di dalam
if atau for: variabel i pada
for (int i = ...) tidak ada setelah perulangan selesai.
Karena namanya tidak bertabrakan, dua fungsi boleh memakai nama
variabel yang sama tanpa saling mengganggu. int i di dalam
f dan int i di dalam main adalah
dua kotak yang sama sekali berbeda.
Kebalikannya adalah variabel global: variabel yang ditulis di luar semua fungsi, sehingga bisa dibaca dan diubah dari mana saja.
const int MAKS = 100005;
int a[MAKS]; // global: dipakai bersama oleh semua fungsi
int n;
int jumlah() {
int total = 0; // lokal: hanya ada selama fungsi ini berjalan
for (int i = 0; i < n; i++) total += a[i];
return total;
}Di perangkat lunak besar, variabel global dihindari karena membuat
kode sulit ditelusuri. Di lomba, justru itu yang dipakai untuk array
besar dan untuk nilai yang dibutuhkan semua fungsi — kodenya jadi lebih
pendek dan tidak ada vector besar yang tersalin
bolak-balik.
Nilai salinan dan referensi
Secara bawaan, parameter dikirim sebagai salinan. Mengubahnya di dalam fungsi tidak mengubah variabel asli:
void tambah_salah(int x) { x += 100; }
int a = 5;
tambah_salah(a);
cout << a << "\n"; // tetap 5Tanda & membuat parameter menjadi
referensi — nama lain untuk variabel aslinya:
void tambah_benar(int &x) { x += 100; }
int a = 5;
tambah_benar(a);
cout << a << "\n"; // 105Perbedaannya bisa dibaca seperti ini: tanpa &,
fungsi menerima fotokopi; dengan &,
fungsi menerima kotak aslinya.
| Cara ditulis | Yang diterima fungsi | Boleh diubah? | Dipakai untuk |
|---|---|---|---|
void f(int x) |
salinan | ya, tanpa efek ke asalnya | nilai kecil biasa |
void f(int &x) |
variabel aslinya | ya, ikut berubah | mengembalikan lebih dari satu nilai |
void f(vector<int> &v) |
vector aslinya |
ya | menghindari salinan besar |
void f(const vector<int> &v) |
vector aslinya |
tidak | hanya dibaca, tetapi besar |
Referensi adalah cara memulangkan lebih dari satu
nilai dari sebuah fungsi, karena return hanya bisa
memulangkan satu:
void bagi(int a, int b, int &hasil, int &sisa) {
hasil = a / b;
sisa = a % b;
}Buka
exercises/06/02-salinan-dan-referensi.cpp.
Hapus tanda & pada fungsi yang memakai referensi,
compile ulang, dan perhatikan nilai aslinya tidak lagi berubah.
Referensi juga dipakai untuk alasan kecepatan. Mengirim
vector besar sebagai salinan berarti menyalin seluruh
isinya setiap kali fungsi dipanggil. Menulis
void proses(vector<int> &v) menghindari salinan
itu. Bab 07 membahas vector.
Rekursi
Fungsi boleh memanggil dirinya sendiri. Bentuk ini disebut rekursi, dan setiap fungsi rekursif butuh dua hal:
- Kasus dasar — kondisi berhenti, tanpa pemanggilan lagi.
- Langkah mengecil — pemanggilan pada soal yang lebih kecil.
long long faktorial(int n) {
if (n <= 1) return 1; // kasus dasar
return n * faktorial(n - 1); // langkah mengecil
}Menelusuri faktorial(4):
faktorial(4) = 4 * faktorial(3)
= 4 * 3 * faktorial(2)
= 4 * 3 * 2 * faktorial(1)
= 4 * 3 * 2 * 1
= 24
Yang membuat rekursi bisa bekerja adalah tumpukan
pemanggilan (call stack): setiap pemanggilan menyimpan
variabel lokalnya sendiri dan tempat ia harus kembali. Saat
faktorial(4) sedang menunggu faktorial(3),
keduanya ada di memori sekaligus, masing-masing dengan n
sendiri:
faktorial(4) n = 4 menunggu faktorial(3)
faktorial(3) n = 3 menunggu faktorial(2)
faktorial(2) n = 2 menunggu faktorial(1)
faktorial(1) n = 1 -> kembalikan 1
kembalikan 2 * 1 = 2
kembalikan 3 * 2 = 6
kembalikan 4 * 6 = 24
Bacanya: turun ke dalam sampai mengenai kasus dasar, lalu naik kembali sambil mengalikan. Setengah bagian atas adalah "memecah menjadi soal yang lebih kecil", setengah bagian bawah adalah "menggabungkan jawabannya".
Buka
exercises/06/03-rekursi.cpp.
Tambahkan
cerr << "masuk n=" << n << "\n"; di awal
fungsi dan
cerr << "keluar n=" << n << "\n"; tepat
sebelum return, lalu jalankan untuk n = 4.
Urutan pesannya adalah gambar tumpukan di atas.
Tanpa kasus dasar, fungsi memanggil dirinya sampai memori tumpukan habis dan program berhenti mendadak (stack overflow, di penilai muncul sebagai Runtime Error). Selalu tulis kasus dasar lebih dulu.
Ketika rekursi menjadi lambat
Fungsi Fibonacci ditulis rekursif dengan sangat mudah:
long long fib(int n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}Tetapi cobalah fib(45): komputermu akan berpikir lama
sekali. Sebabnya terlihat kalau pemanggilan fib(5) digambar
sebagai pohon:
fib(5)
/ \
fib(4) fib(3)
/ \ / \
fib(3) fib(2) fib(2) fib(1)
/ \ / \ / \
fib(2) fib(1) fib(1) fib(0) fib(1) fib(0)
/ \
fib(1) fib(0)
fib(3) muncul dua kali, fib(2) tiga kali,
fib(1) lima kali — seluruh cabangnya dihitung ulang dari
nol setiap kali. Jumlah pemanggilannya tumbuh seperti deret Fibonacci
itu sendiri:
n |
Jumlah pemanggilan fib |
|---|---|
| 5 | 15 |
| 10 | 177 |
| 20 | 21.891 |
| 30 | 2.692.537 |
| 40 | 331.160.281 |
| 45 | 3.672.623.805 |
Setiap kenaikan n sebesar 5 melipatgandakan pekerjaannya
sekitar sebelas kali.
Bandingkan dengan versi perulangan yang hanya menyimpan dua nilai terakhir:
long long fib_cepat(int n) {
long long a = 0, b = 1;
for (int i = 0; i < n; i++) {
long long c = a + b;
a = b;
b = c;
}
return a;
}Bedanya bukan sedikit: untuk n = 45, versi rekursif
mengerjakan 3,67 miliar pemanggilan fungsi (beberapa detik sampai
belasan detik, tergantung komputernya), sedangkan versi perulangan cukup
45 putaran dan selesai sebelum sempat terasa. Keduanya
mencetak jawaban yang sama, 1134903170.
Yang dibuang versi perulangan bukan rekursinya, melainkan pengulangan pekerjaan. Persoalan "perhitungan yang sama dihitung berkali-kali" inilah yang diselesaikan dynamic programming di Bab 13 — dan tantangan PR bab ini adalah setengah langkah pertamanya.
Buka
exercises/06/04-fibonacci.cpp.
Jalankan untuk n = 30, lalu n = 40, lalu
n = 45, dan rasakan bedanya. Versi perulangan di file yang
sama langsung menjawab.
Kenapa fungsi penting di lomba
- Menguji bagian kecil.
bool prima(long long n)bisa dicoba dengan 1, 2, 9, dan 97 sebelum dipakai di soal besar. - Mengurangi salah tulis. Logika yang dipakai tiga kali cukup ditulis sekali.
- Membuat kode terbaca saat waktu menipis.
if (prima(x))lebih cepat dipahami daripada tujuh baris perulangan di tengahmain.
Pekerjaan rumah
Soal 1 — Fungsi maksimum
Buka
exercises/06/homework/01-maksimum.cpp.
Tulis fungsi int maksimum(int a, int b) dan
int maksimum3(int a, int b, int c) yang memakai fungsi
pertama. Baca tiga bilangan, cetak yang terbesar.
Soal 2 — Cacah bilangan prima
Buka
exercises/06/homework/02-cacah-prima.cpp.
Tulis fungsi bool prima(long long n) (pakai cara akar dari
Bab 05), lalu baca n dan cetak berapa banyak bilangan prima
dari 1 sampai n. Uji dengan n = 100;
jawabannya 25.
Soal 3 — Pangkat rekursif
Buka
exercises/06/homework/03-pangkat.cpp.
Tulis fungsi long long pangkat(long long a, int b) secara
rekursif: a dipangkatkan b, dengan kasus dasar
b == 0 bernilai 1. Baca a dan b,
cetak hasilnya. Jaga agar b tidak lebih dari 60 supaya
hasilnya masih muat di long long.
Soal 4 — Membalik bilangan
Buka
exercises/06/homework/04-balik.cpp.
Tulis fungsi long long balik(long long n) yang
mengembalikan bilangan dengan urutan digit terbalik (4071
menjadi 1704). Pakai fungsi itu untuk memeriksa apakah
sebuah bilangan palindrom, lalu cetak YA
atau TIDAK.
Tantangan — Fibonacci dengan ingatan
Buka
exercises/06/homework/05-fib-ingat.cpp.
Fungsi fib rekursif lambat karena menghitung ulang hal yang
sama. Tambahkan sebuah array global long long ingat[100]
berisi -1, lalu: sebelum menghitung, periksa apakah
jawabannya sudah ada di ingat; sesudah menghitung, simpan
ke sana. Bandingkan waktu fib(45) sebelum dan sesudahnya.
Cara ini bernama memoization, dan merupakan setengah dari isi
Bab 13.
Kesalahan yang paling sering
| Yang ditulis | Akibatnya |
|---|---|
| fungsi dipanggil sebelum ditulis | 'nama' was not declared in this scope |
fungsi bertipe int tanpa return |
nilai kembaliannya tak terduga |
return lupa di salah satu cabang if |
benar untuk sebagian masukan saja |
parameter diubah tanpa & |
variabel asli tidak ikut berubah |
vector besar dikirim tanpa & |
tersalin setiap pemanggilan, program lambat |
| rekursi tanpa kasus dasar | stack overflow, di penilai jadi Runtime Error |
| kasus dasar ada tetapi tidak pernah tercapai | sama saja: langkahnya tidak mengecil |
Tips OSN
Fungsi adalah alat menguji, bukan alat merapikan. Di
lomba, keuntungan terbesar fungsi bukan kerapian kode melainkan
kemampuan menguji satu bagian secara terpisah.
bool prima(long long n) bisa dicoba dengan 1, 2, 9, dan 97
dalam sepuluh detik; logika yang sama yang tertanam di tengah
main hanya bisa diuji lewat seluruh soal.
Rekursi punya batas kedalaman. Setiap pemanggilan
fungsi menyimpan datanya di stack, dan stack yang habis membuat
program berhenti sendiri — dinilai Runtime Error, bukan
Wrong Answer. Rekursi sedalam 10⁶ pemanggilan hampir pasti
gagal. Kalau kedalamannya sebanding dengan N dan
N bisa mencapai 10⁵ atau lebih, ubah rekursinya menjadi
perulangan.
Rekursi tanpa dasar tidak akan berhenti. Setiap fungsi rekursif harus punya dua bagian: kasus dasar yang berhenti, dan pemanggilan yang bergerak menuju kasus dasar. Kalau salah satu hilang, gejalanya sama dengan perulangan tak berujung.
Hitung dulu berapa kali fungsinya dipanggil. Rekursi
yang memanggil dirinya dua kali untuk setiap tingkat membuat jumlah
pemanggilan berlipat ganda. fib(45) versi naif memerlukan
3,67 miliar pemanggilan — bandingkan dengan aturan 10⁸ operasi per detik
di Bab 05. Kalau angkanya tidak masuk akal, penyelesaiannya memang harus
diganti, bukan dioptimalkan sedikit.
Parameter besar dikirim dengan referensi. Mengirim
vector sepanjang 10⁶ sebagai salinan berarti menyalin 10⁶
elemen setiap pemanggilan. Tanda & pada parameter
menghilangkan biaya itu sama sekali. Ini baru terasa mulai Bab 07,
tetapi kebiasaannya dibentuk sekarang.
Beberapa fungsi pustaka yang menghemat waktu ketik:
| Fungsi | Berkas kepala | Gunanya |
|---|---|---|
max(a, b), min(a, b) |
<algorithm> |
nilai terbesar/terkecil dari dua |
abs(x) |
<cstdlib> |
nilai mutlak bilangan bulat |
swap(a, b) |
<algorithm> |
menukar isi dua variabel |
Variabel lokal di dalam fungsi tidak dikembalikan ke
nol dengan sendirinya kalau ditulis static, dan variabel
global membawa sisa nilai dari pemanggilan sebelumnya. Pada soal banyak
kasus uji, pastikan setiap penampung dikosongkan di awal kasus — bukan
di awal program.
Ringkasan
- Fungsi punya tipe hasil, nama, parameter, dan
return.voiduntuk fungsi yang tidak memulangkan nilai. returnlangsung mengakhiri fungsi; pemanggilannya digantikan nilai yang dipulangkan.- Variabel lokal hanya hidup di dalam blok tempat ia dibuat. Array besar di lomba dibuat global.
- Tanpa
¶meter diterima sebagai salinan; dengan&fungsi bekerja pada variabel aslinya — dipakai untuk memulangkan lebih dari satu nilai dan untuk menghindari penyalinanvectorbesar. - Fungsi rekursif wajib punya kasus dasar dan langkah yang mengecil.
- Rekursi yang menghitung ulang submasalah yang sama bisa sangat lambat; menyimpan hasilnya (memoization) adalah pintu masuk ke Bab 13.
Latihan bergaya OSN
Ketiga soal berikut meminta jawabannya dipecah menjadi fungsi. Tulis
fungsinya lebih dulu, uji sendiri dengan beberapa angka, baru sambungkan
ke main.
Kerangka ketiganya ada di
exercises/06/osn/.
Soal 1 — Akar Digit
Akar digit sebuah bilangan didapat dengan menjumlahkan semua digitnya, lalu mengulangi langkah yang sama terhadap hasilnya, sampai tersisa satu digit saja.
Format masukan. Baris pertama berisi bilangan bulat
T, banyaknya kasus uji. T baris berikutnya
masing-masing berisi satu bilangan bulat N.
Format keluaran. T baris berisi akar
digit setiap N.
Batasan.
- 1 ≤
T≤ 100 - 1 ≤
N≤ 10¹⁸
Contoh masukan
3
9875
9
1000000000000000000
Contoh keluaran
2
9
1
Penjelasan. Untuk 9875: 9 + 8 + 7 + 5 = 29, lalu 2 + 9 = 11, lalu 1 + 1 = 2. Bilangan yang sudah satu digit langsung menjadi jawabannya sendiri.
Kerangkanya:
exercises/06/osn/01-akar-digit.cpp
Soal 2 — Menara Hanoi
Ada tiga tiang bernama A, B, dan
C. Pada tiang A tersusun N cakram
dengan ukuran berbeda, terbesar di bawah dan terkecil di atas. Seluruh
cakram harus dipindahkan ke tiang C dengan dua aturan:
- satu kali perpindahan hanya boleh memindahkan satu cakram, yaitu cakram paling atas dari salah satu tiang;
- cakram besar tidak boleh diletakkan di atas cakram yang lebih kecil.
Cetak urutan perpindahan yang menyelesaikan susunan itu dengan jumlah perpindahan sesedikit mungkin.
Format masukan. Satu baris berisi bilangan bulat
N.
Format keluaran. Baris pertama berisi banyaknya perpindahan. Setiap baris berikutnya berisi dua huruf tiang: tiang asal dan tiang tujuan, dipisahkan satu spasi.
Batasan. 1 ≤ N ≤ 15.
Contoh masukan
2
Contoh keluaran
3
A B
A C
B C
Penjelasan. Cakram kecil dipindahkan ke
B supaya cakram besar bisa pindah ke C, lalu
cakram kecil menyusul. Banyaknya perpindahan paling sedikit selalu
2^N − 1.
Kerangkanya:
exercises/06/osn/02-menara-hanoi.cpp
Soal 3 — Bilangan Sempurna
Faktor sejati sebuah bilangan adalah semua faktor positifnya kecuali bilangan itu sendiri. Sebuah bilangan disebut sempurna kalau jumlah faktor sejatinya sama dengan bilangan itu, berlebih kalau jumlahnya lebih besar, dan kurang kalau jumlahnya lebih kecil.
Format masukan. Baris pertama berisi bilangan bulat
T. T baris berikutnya masing-masing berisi
satu bilangan bulat N.
Format keluaran. T baris berisi
SEMPURNA, BERLEBIH, atau
KURANG.
Batasan.
- 1 ≤
T≤ 100 - 1 ≤
N≤ 10⁹
Contoh masukan
4
6
12
8
1
Contoh keluaran
SEMPURNA
BERLEBIH
KURANG
KURANG
Penjelasan. Faktor sejati 6 adalah 1, 2, dan 3, yang berjumlah 6. Faktor sejati 12 adalah 1, 2, 3, 4, 6, yang berjumlah 16 — lebih besar daripada 12. Faktor sejati 8 adalah 1, 2, 4, berjumlah 7. Bilangan 1 tidak punya faktor sejati sama sekali, jadi jumlahnya 0.
Kerangkanya:
exercises/06/osn/03-bilangan-sempurna.cpp
Bingung atau sudah selesai? Buka halaman pembahasan PR.