Tuesday, December 20, 2016

Bab 6. Java Struktur Data dan Pemrograman GUI



Bab.6 Rekursi

Tujuan Instruksional
·         Menjelaskan apa itu metode rekursif dan keuntungan menggunakannya.
·         Mengembangkan metode rekursif untuk fungsi-fungsi matematik rekursif.
·         Menjelaskan bagaimana pemanggilan metode rekursif ditangani di dalam tumpukan pemanggilan.
·         Menggunakan suatu metode helper teroverload untuk menderivasi suatu metode rekursif.
·         Menyelesaikan pengurutan seleksi menggunakan rekursi.
·         Menyelesaikan pencarian biner menggunakan rekursi.
·         Mendapatkan ukuran direktori menggunakan rekursi.
·         Menyelesaikan masalah Menara Hanoi menggunakan rekursi.
·         Menggambar fraktal menggunakan rekursi.
·         Menyelesaikan masalah Delapan Ratu menggunakan rekursi.



6.1 Introduksi

Dimisalkan Anda sedang mencari seluruh file di bawah suatu direktori yang memuat suatu kata tertentu. Bagaimana Anda menyelesaikan masalah ini? Ada beberapa cara untuk melakukannya. Solusi efektif adalah menggunakan rekursi dengan mencari file-file yang diinginkan di dalam semua subdirektori secara rekursif.

Teka-teki Delapan Ratu klasik adalah bagaimana menempatkan delapan ratu dalam suatu papan catur sehingga tidak ada dua ratu yang saling serang (yaitu, tidak ada dua ratu berada dalam baris sama, kolom sama, atau diagonal sama), seperti tertampil pada Gambar 6.1. Bagaimana Anda menulis program untuk menyelesaikan masalah ini? Pendekatan yang efektif adalah menggunakan rekursi.


Gambar 6.1 Teka-teki Delapan Ratu dapat diselesaikan menggunakan rekursi


Menggunakan rekursi berarti memprogram menggunakan metode rekursif, yaitu metode, yang secara langsung ataupun tidak langsung, memanggil dirinya sendiri. Rekursi merupakan suatu teknik pemrograman yang sangat bermanfaat. Dalam beberapa kasus, rekursi memampukan Anda untuk solusi efektif dan sederhana atas masalah-masalah yang kompleks. Bab ini akan mengenalkan Anda konsep dan teknik pemrograman rekursif dan memberikan beberapa ilustrasi untuk membantu Anda untuk berpikir secara rekursif.


6.2 Masalah: Menghitung Faktorial
Banyak fungsi matematik yang didefinisikan menggunakan rekursi. Akan dimulai dari suatu contoh sederhana. Faktorial suatu angkan n dapat didefinisikan secara rekursif sebagai berikut:

 

 

Bagaimana Anda mencari n! atas suatu n? Untuk mencari 1! mudah, karena Anda mengetahui bahwa 0! sama dengan 1, dan 1! adalah 1  0!. Bila diasumsikan bahwa Anda mengetahui (n - 1)!, maka Anda bisa mendapatkan n!, menggunakan n  (n - 1)!. Jadi, masalah menghitung n! direduksi menjadi masalah menghitung (n – 1)!. Ketika menghitung (n – 1)!, Anda bisa menerapkan ide yang sama secara rekursif sampai n direduksi menjadi 0.

Dimisalkan bahwa faktorial(n) merupakan metode untuk menghitung n!. Jika Anda memanggil metode tersebut dengan n = 0, maka metode tersebut dengan segera memberikan nilai balik. Metode tersebut tentunya bisa menyelesaikan kasus yang paling sederhana, yang dikenal dengan kasus basis atau kondisi penghenti. Jika Anda memanggil metode tersebut dengan n > 0, maka masalah akan direduksi menjadi submasalah untuk menghitung faktorial atas n – 1. Submasalah ini pada intinya sama dengan masalah pokok, tetapi hanya lebih sederhana dan lebih kecil. Karena submasalah memiliki watak yang sama dengan masalah asli, Anda bisa memanggil metode dengan argumen yang berbeda, yang dikenal dengan suatu pemanggilan rekursif.

Algoritma rekursif untuk menghitung faktorial(n) dapat dengan sederhana dijelaskan sebagai berikut:

if (n == 0)
  return 1;
else
  return n * faktorial(n - 1);

Suatu pemanggilan rekursif dapat menghasilkan lebih banyak lagi pemanggilan rekursif, karena metode dapat terus membagi suatu submasalah menjadi sub-submasalah yang baru. Untuk menghentikan metode rekursif, masalah akhirnya harus direduksi menjadi suatu kasus basis atau kasus penghenti, dimana pada saat itu metode mengembalikan hasil kepada pemanggilnya. Proses rekursif berlanjut sampai hasil dilewatkan kembali kepada pemanggil awal. Masalah awal dapat diselesaikan dengan mengalikan n dengan faktorial(n-1).


Kode6.1 HitungFaktorial.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.Scanner;

public class HitungFaktorial {
  /** Metode utama */
  public static void main(String[] args) {
    // Menciptakan suatu Scanner
    Scanner masukan = new Scanner(System.in);
    System.out.print("Masukkan suatu integer non-negatif: ");
    int n = masukan.nextInt();

    // Menampilkan faktorial
    System.out.println("Faktorial dari " + n + " adalah "+ faktorial(n));
  }

  /** Mengembalikan nilai faktorial untuk suatu nilai tertentu */
  public static long faktorial(int n){
    if (n == 0) // Kasus basis
      return 1;
    else
      return n * faktorial(n - 1); // Pemanggilan rekursif
  }
}

Keluaran

Masukkan suatu integer non-negatif: 5
Faktorial dari 5 adalah 120

Masukkan suatu integer non-negatif: 10
Faktorial dari 10 adalah 3628800

Metode faktorial (baris 16-21) secara esensi merupakan penterjemahan atas definisi matematik atas faktorial menjadi kode JAVA. Pemanggilan terhadap faktorial bersifat rekursif karena memanggil dirinya sendiri. Parameter yang dilewatkan kepada metode faktorial didekremen sampai dicapainya kasus basis.

Gambar 6.2 mengilustrasikan eksekusi atas beberapa pemanggilan rekursif, dimulai dari n = 4. Kegunaan memori tumpukan ditampilkan pada Gambar 6.3.

Jika rekursi tidak mereduksi masalah dengan suatu cara sehingga tidak terjadi konvergensi menjadi kasus basis, maka rekursi tak-berhingga akan terjadi. Sebagai contoh, dimisalkan Anda menuliskan secara salah metode faktorial sebagai berikut:

public static long faktorial(int n) {
  return n * faktorial(n - 1);
}
Pada kasus ini, metode faktorial akan berjalan secara tak-berhingga dan menyebabkan suatu StackOverflowError.

Gambar 6.2 Pemanggilan faktorial(4) menyebabkan beberapa pemanggilan rekursif terhadap metode faktorial



Gambar 6.3 Ketika faktorial(4) sedang dieksekusi, metode faktorial dipanggil secara rekursif, menyebabkan memori tumpukan berubah secara dinamis


6.3 Masalah: Menghitung Bilangan Fibonacci
Metode faktorial pada bagian terdahulu dapat dengan mudah ditulis-ulang tanpa menggunakan rekursi. Dalam beberapa kasus, penggunaan rekursi memampukan Anda untuk menyajikan solusi yang efektif dan sederhana atas permasalahan yang sulit untuk diselesaikan. Perhatikan deret Fibonacci berikut ini:

deret:  0  1  1  2  3  5  8  13  21  34  55  89 ...
indeks:  0  1  2  3  4  5  6  7   8   9  10  11

Deret Fibonacci dimulai dengan 0 dan 1, dengan setiap subruntun angka merupakan penjumlahan dari dua angka sebelumnya. Runtun ini dapat secara rekursif didefinisikan sebagai berikut:

fib(0) = 0;
fib(1) = 1;
fib(indeks) = fib(indeks - 2) + fib(indeks - 1); indeks >= 2

Deret Fibonacci dinamai oleh Leonardo Fibonacci, seorang matematikawan abad pertengahan, yang mulai memodelkannya untuk menghitung pertumbuhan populasi kelinci. Deret ini dapat pula digunakan untuk optimisasi numerik dan berbagai aplikasi lain.

Bagaimana Anda mencari fib(indeks) atau suatu indeks yang diberikan? Adalah hal mudah untuk menghitung fib(2), karena Anda mengetahui fib(1) dan fib(0). Diasumsikan bahwa Anda telah mengetahui fib(indeks-2) dan fib(indeks-1), maka Anda bisa menghitung fib(indeks) dengan mudah. Oleh karena itu, masalah menghitung fib(indeks) direduksi menjadi masalah menghitung fib(indeks-2) dan fib(indeks-2). Ketika melakukannya, Anda menerapkan ide rekursif sampai indeks direduksi menjadi 1 dan 0.

Kasus basis adalah indeks = 0 atau indeks = 1. Jika Anda memanggil metode fib dengan indeks = 0 atau indeks = 1, maka akan menghasilkan nilai balik dengan segera. Jika Anda memanggil metode tersebut dengan indeks >= 2, maka masalah akan dibagi menjadi dua submasalah untuk menghitung fib(indeks-1) dan fib(indeks-2) menggunakan pemanggilan rekursif. Algoritma rekursif untuk menghitung fib(indeks) dapat dengan sederhana dideskripsikan sebagai berikut:

if (indeks == 0)
  return 0;
else if (indeks == 1)
  return 1;
else
  return fib(indeks - 1) + fib(indeks - 2);

Kode6.20 menyajikan suatu program utuh yang meminta pengguna untuk memasukkan suatu indeks dan yang menghitung bilangan Fibonacci atas indeks tersebut.

Kode6.2 HitungFibonacci.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.util.Scanner;

public class HitungFibonacci {
  /** Metode utama */
  public static void main(String args[]) {
    // Menciptakan suatu Scanner
    Scanner masukan = new Scanner(System.in);
    System.out.print("Masukkan suatu indeks untuk bilangan Fibonacci: ");
    int indeks = masukan.nextInt();

    // Mencari dan menampilkan bilangan Fibonacci
    System.out.println(
      "Bilangan Fibonacci pada indeks " + indeks + " adalah " + fib(indeks));
  }

  /** Metode untuk mencari bilangan Fibonacci */
  public static long fib(long indeks){
    if (indeks == 0) // Kasus basis
      return 0;
    else if (indeks == 1) // Kasus basis
      return 1;
    else // Reduksi dan pemanggilan rekursif
      return fib(indeks - 1) + fib(indeks - 2);
  }
}

Keluaran

Masukkan suatu indeks untuk bilangan Fibonacci: 10
Bilangan Fibonacci pada indeks 10 adalah 55

Masukkan suatu indeks untuk bilangan Fibonacci: 20
Bilangan Fibonacci pada indeks 20 adalah 6765

Program tidak menampilkan beban kerja di belakang layar oleh komputer. Gambar 6.4 menyajikan pemanggilan rekursif berurutan dalam mengevaluasi fib(4). Metode awal, fib(4), melakukan dua pemanggilan rekursif, fib(3) dan fib(2), dan menghasilkan nilai balik fib(3) + fib(2). Tetapi dengan urutan bagaimana metode-metode ini dipanggil? Pada JAVA, operand-operand dievaluasi dari kiri ke kanan. fib(2) dipanggil setelah fib(3) selesai dievaluasi. Label-label pada Gambar 6.4 menunjukkan urutan metode-metode tersebut dipanggil.

Seperti tertampil pada Gambar 6.4, terdapat banyak pemanggilan rekursif terduplikasi. Sebagai contoh, fib(2) dipanggil dua kali, fib(1) tiga kali, dan fib(0) dua kali. Pada umumnya, penghitungan fib(indeks) memerlukan kira-kira dua kali pemanggilan rekursif daripada penghitungan fib(indeks-1). Jika Anda mencoba indeks yang jauh lebih besar, maka jumlah pemanggilan bertambah secara signifikan. Di samping jumlah pemanggilan yang bertambah, komputer juga memerlukan waktu dan memori untuk menjalankan metode-metode rekursif.
  

Gambar 6.4 Pemanggilan fib(4) menyebabkan beberapa pemanggilan rekursif terhadap metode fib


6.4 Menyelesaikan Masalah Menggunakan Rekursi
Bagian-bagian terdahulu menyajikan dua rekursi klasik. Semua metode rekursi memiliki beberapa karakteristik sebagai berikut:
·         Metode diimplementasikan menggunakan stateman if-else atau switch yang mengarah pada kasus-kasus yang berbeda.
·         Satu atau lebih kasus basis (kasus paling sederhana) digunakan untuk menghentikan rekursi.
·         Setiap rekursi mereduksi masalah awal, membawanya secara menaik lebih dekat ke suatu kelas basis sampai menjadi kasus tersebut.
Pada umumnya, untuk menyelesaikan suatu masalah menggunakan rekursi, Anda memecahnya menjadi beberapa submasalah. Setiap submasalah hampir sama dengan masalah awal tetapi lebih kecil dalam ukuran. Anda bisa menerapkan pendekatan yang sama terhadap setiap submasalah untuk menyelesaikannya secara rekursif.

Sekarang pertimbangkan masalah sederhana dalam menampilkan suatu pesan sebanyak n kali. Anda memecah masalah ini menjadi dua submasalah: Satu untuk menampilkan pesan sebanyak sekali dan submasalah lainnya adalah untuk menampilkan sebanyak n - 1 kali. Masalah kedua sama dengan masalah awal namun dengan ukuran yang lebih kecil. Kasus basis untuk masalah ini adalah n==0. Anda dapat menyelesaikan masalah ini menggunakan rekursi sebagai berikut:

public static void nPrintln(String pesan, int kali) {
  if (kali >= 1) {
    System.out.println(pesan);
    nPrintln(pesan, kali - 1);
  } // Kasus basis adalah kali == 0
}

Perhatikan bahwa metode fib pada contoh terdahulu mengembalikan suatu nilai kepada pemanggilnya, tetapi metode nPrintln bertipe void, jadi tidak mengembalikan nilai balik.
Jika Anda berpikir secara rekursif, Anda dapat menggunakan rekursi untuk menyelesaikan banyak masalah yang disajikan. Perhatikan kasus palindrome. Ingat bahwa string dikatakan suatu palindrome bila menghasilkan suatu string yang sama jika dibaca dari kiri maupun dari kanan. Sebagai contoh, ada dan apa merupakan palindrome, tetapi ayah dan ibu bukan palindrome. Masalah memeriksa apakah suatu string palindrome atau tidak dapat dibagi menjadi dua submasalah:
·         Memeriksa apakah karakter pertama dan karakter terakhir string adalah sama atau tidak.
·         Mengabaikan dua karakter akhir dan memeriksa apakah sisa substring adalah suatu palindrome atau tidak.
Submasalah kedua sama dengan masalah awal dengan ukuran yang lebih kecil. Terdapat dua kasus basis: (1) dua karakter akhir tidak sama; (2) ukuran string adalah 0 atau 1. Pada kasus (1), string bukan palindrome; dan pada kasus (2), string adalah palindrome. Metode rekursif untuk masalah ini dapat diimplementasikan pada kode6.3.

Kode6.3 PlindromeRekursifMenggunakanSubstring.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class PalindromeRekursifMenggunakanSubstring {
  public static boolean apaPalindrome(String s) {
    if (s.length() <= 1) // Kasus basis
      return true;
    else if (s.charAt(0) != s.charAt(s.length() - 1)) // Kasus basis
      return false;
    else
      return apaPalindrome(s.substring(1, s.length() - 1));
  }

  public static void main(String[] args) {
    System.out.println("Apakah mama suatu palindrome? "
      + apaPalindrome("mama"));
    System.out.println("Apakah papa suatu palindrome? "
      + apaPalindrome("papa"));
    System.out.println("Apakah a suatu palindrome? " + apaPalindrome("a"));
    System.out.println("Apakah aba suatu palindrome? "
      + apaPalindrome("aba"));
    System.out.println("Apakah ab suatu palindrome? " + apaPalindrome("ab"));
  }
}

Keluaran

Apakah mama suatu palindrome? false
Apakah papa suatu palindrome? false
Apakah a suatu palindrome? true
Apakah aba suatu palindrome? true
Apakah ab suatu palindrome? false
Metode substring pada baris 8 menciptakan suatu string baru yang sama dengan string awal kecuali bahwa tidak terdapat karakter awal dan akhir. Pemeriksaan apakah suatu string palindrome atau tidak ekivalen dengan apakah substring palindrome atau tidak jika kedua karakter ujung pada string awal adalah sama.


6.5 Metode Helper Rekursif
Metode apaPalindrome rekursif terdahulu tidak efisien, karena menciptakan suatu string baru untuk setiap pemanggilan rekursif. Untuk menghindarinya, Anda dapat menggunakan indeks tinggi dan rendah untuk mengindikasikan rentang substring. Kedua indeks tersebut kemudian dilewatkan kepada metode rekursif. Karena metode awal adalah apaPalindrome(String s), Anda perlu menciptakan suatu metode baru apaPalindrome(String s, int rendah, int tinggi) untuk menerima informasi tambahan pada string, seperti ditunjukkan pada kode6.4.

Kode6.4 PalindromeRekursif.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class PalindromeRekursif {
  public static boolean apaPalindrome(String s) {
    return apaPalindrome(s, 0, s.length() - 1);
  }

  public static boolean apaPalindrome(String s, int rendah, int tinggi) {
    if (tinggi <= rendah)// Kasus basis
      return true;
    else if (s.charAt(rendah) != s.charAt(tinggi)) // Kasus basis
      return false;
    else
      return apaPalindrome(s, rendah + 1, tinggi - 1);
  }

  public static void main(String[] args) {
    System.out.println("Apakah mama adalah suatu palindrome? "
      + apaPalindrome("mama"));
    System.out.println("Apakah papa suatu palindrome? "
      + apaPalindrome("papa"));
    System.out.println("Apakah a suatu palindrome? " + apaPalindrome("a"));
    System.out.println("Apakah aba suatu palindrome? " + apaPalindrome("aba"));
    System.out.println("Apakah ab suatu palindrome? " + apaPalindrome("ab"));
  }
}

Keluaran

Apakah mama suatu palindrome? false
Apakah papa suatu palindrome? false
Apakah a suatu palindrome? true
Apakah aba suatu palindrome? true
Apakah ab suatu palindrome? false
Dua metode apaPalindrome teroeverload didefinisikan. Yang pertama, apaPalindrome(String s), memeriksa apakah suatu string palindrome atau tidak, dan yang kedua, apaPalindrome(String s, int rendah, int tinggi), memeriksa apakah suatu substring s(rendah..tinggi) palindrome atau tidak. Metode pertama melewatkan string s dengan rendah = 0 dan tinggi = s.length() - 1 kepada metode kedua. Metode kedua dapat dipanggil secara rekursif. 

Metode helper sangat berguna dalam solusi rekursif untuk masalah-masalah yang melibatkan string dan array. Beberapa bagian ke depan akan menyajikan dua contoh lagi.


6.5.1 Pengurutan Seleksi
Pengurutan seleksi digunakan untuk mencari angka terkecil di dalam suatu daftar dan kemudian menempatkannya di awal daftar tersebut. Kemudian pengurutan ini mencari angka terkecil yang tersisa dan menempatkannya setelah angka terkecil pertama, dan seterusnya sampai daftar hanya memuat satu angka yang tersisa. Masalah ini dapat dibagi menjadi dua submasalah:
·         Mencari angka terkecil di dalam daftar dan menukarnya dengan angka pertama.
·         Mengabaikan angka pertama dan mengurutkan daftar sisa yang lebih kecil secara rekursif.
Kelas basis adalah ketika daftar hanya memuat satu angka yang tersisa. Kode6.5 menyajikan contoh metode pengurutan seleksi.

Kode6.5 PengurutanSeleksiRekursif.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class PengurutanSeleksiRekursif {
  public static void urut(double[] daftar) {
    urut(daftar, 0, daftar.length - 1);// Mengurutkan seluruh isi daftar
  }

  public static void urut(double[] daftar, int rendah, int tinggi) {
    if (rendah < tinggi) {
      // Mencari angka terkecil dan indeksnya di dalam daftar(rendah .. tinggi)
      int indeksDariMin = rendah;
      double min = daftar[rendah];
      for (int i = rendah + 1; i <= tinggi; i++) {
        if (daftar[i] < min) {
          min = daftar[i];
          indeksDariMin = i;
        }
      }

      // Menukar angka terkecil di dalam daftar(rendah .. tinggi) dengan daftar(rendah)
      daftar[indeksDariMin] = daftar[rendah];
      daftar[rendah] = min;

      // Mengurutkan sisa daftar(rendah+1 .. tinggi)
      urut(daftar, rendah + 1, tinggi);
    }
  }
}

Dua metode urut teroverload didefinisikan. Metode pertama, urut(double[] daftar), mengurutkan suatu array di dalam daftar[0..daftar.length()-1] dan metode kedua, urut(double[] daftar, int rendah, int tinggi) mengurutkan suatu array di dalam daftar[rendah..tinggi]. Metode kedua dapat dipanggil secara rekursif untuk mengurutkan suatu subarray menyusut.


6.5.2 Pencarian Biner
Untuk melakukan pencarian biner, elemen-elemen di dalam array harus diurutkan. Pencarian biner pertama-tama membandingkan kunci di dalam elemen di tengah array. Perhatikan tiga kasus berikut:
·         Kasus 1: Jika kunci lebih rendah dari elemen tengah, kunci di dalam setengah array pertama dicari secara rekursif.
·         Kasus 2: Jika kunci sama dengan elemen tengah, pencarian biner berakhir dengan suatu kecocokan.
·         Kasus 3: Jika kunci lebih besar dari elemen tengah, kunci di dalam setengah array kedua dicari secara rekursif.
Kasus 1 dan kasus 3 mereduksi pencarian menjadi suatu daftar pencarian yang lebih kecil. Kasus 2 merupakan kasus basis ketika ditemukan suatu kecocokan. Kasus basis lainnya adalah bahwa pencarian berakhir tanpa kecocokan. Kode6.6 menyajikan suatu contoh solusi sederhana dan jelas atas masalah pencarian biner menggunakan rekursi.

Kode6.6 PencarianBinerRekursif.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class PencarianBinerRekursif {
  public static int PencarianBinerRekursif(int[] daftar, int kunci) {
    int rendah = 0;
    int tinggi = daftar.length - 1;
    return PencarianBinerRekursif(daftar, kunci, rendah, tinggi);
  }

  public static int PencarianBinerRekursif(int[] daftar, int kunci,
     int rendah, int tinggi) {
    if (rendah > tinggi)// Daftar telah dicari tanpa ada kecocokan
      return -rendah - 1;

    int tengah = (rendah + tinggi) / 2;
    if (kunci < daftar[tengah])
      return PencarianBinerRekursif(daftar, kunci, rendah, tengah - 1);
    else if (kunci == daftar[tengah])
      return tengah;
    else
      return PencarianBinerRekursif(daftar, kunci, tengah + 1, tinggi);
  }
}

Metode pertama mencari suatu kunci di dalam keseluruhan daftar. Metode kedua mencari suatu kunci di dalam daftar dengan indeks rendah sampai tinggi.

Metode PencarianBinerRekursif pertama melewatkan array awal dengan rendah = 0 dan tinggi = daftar.length() – 1 kepada metode PencarianBinerRekursif kedua. Metode kedua dapat dipanggil secara rekursif untuk mencari kunci di dalam subarray menyusut.


6.6 Masalah: Mencari Kapasitas Suatu Direktori
Beberapa contoh terdahulu dapat dengan mudah diselesaikan tanpa menggunakan rekursi. Bagian ini akan menyajikan suatu masalah yang sulit diselesaikan tanpa menggunakan rekursi. Tantangan di sini adalah mencari kapasitas atau ukuran suatu direktori. Kapasitas suatu direktori adalah penjumlahan ukuran semua file di dalam direktori. Suatu direktori d bisa saja memuat beberapa subdirektori. Dimisalkan suatu direktori memuat beberapa file f1, f2, ..., fm  dan beberapa direktori d1, d2, ..., dn, seperti tertampil pada Gambar 6.5.



Gambar 6.5 Suatu direktori memuat beberapa file dan beberapa direktori


Kapasitas direktori dapat didefinisikan secara rekursif sebagai berikut:

kap(d) = kap(f1) + kap(f2) +...+ kap(fm) + kap(d1) + kap(d2) +...+ kap(dn)

Kelas File dapat digunakan untuk merepresentasikan suatu file atau suatu direktori dan dapat memperoleh properti file dan direktori. Dua metode di dalam kelas File berguna untuk masalah ini:
·         Metode length() mengembalikan kapasitas suatu file.
·         Metode listFile() mengembalikan suatu array yang memuat objek-objek File di bawah suatu direktori.
Kode6.7 menyajikan suatu program yang meminta pengguna untuk memasukkan suatu direktori atau suatu file dan menampilkan kapasitasnya.

Kode6.7 KapasitasDirektori.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.io.File;
import java.util.Scanner;

public class KapasitasDirektori {
  public static void main(String[] args) {
    // Meminta pengguna untuk memasukkan suatu file atau direktori
    System.out.print("Masukkan suatu direktori atau file: ");
    Scanner masukan = new Scanner(System.in);
    String direktori = masukan.nextLine();

    // Menampilkan kapasitas
    System.out.println(getSize(new File(direktori)) + " byte");
  }

  public static long getSize(File file) {
    long kapasitas = 0; // Menyimpan kapasitas total semua file

    if(file.isDirectory()) {
      File[] files = file.listFiles();// Semua file dan subdirektori
      for (int i = 0; i < files.length; i++) {
        kapasitas += getSize(files[i]); // Pemanggilan rekursif
      }
    }
    else { // Kasus basis
      kapasitas += file.length();
    }

    return kapasitas;
  }
}

Keluaran

Masukkan suatu direktori atau file: E:\BUKU SELESAI DITULIS
6323035721 byte

Masukkan suatu direktori atau file: E:\yamaguchi.bmp
589878 byte

Masukkan suatu direktori atau file: E:\filetidakada
0 byte

Jika objek file merepresentasikan suatu direktori (baris 18), maka setiap subitem (file atau direktori) di dalam direktori tersebut dipanggil secara rekursif untuk mendapatkan kapasitasnya (baris 21). Jika objek file merepresentasikan suatu file (baris 24), maka kapasitas file diperoleh (baris 25).

Apa yang terjadi bila direktori salah atau direktori yang tidak ada dientrikan? Program akan mendeteksi dengan memanggil file.length() (baris 25), yang mengembalikan 0. Jadi, pada kasus ini, metode getSize() akan mengembalikan 0.


6.7 Masalah: Menara Hanoi
Masalah menara Hanoi merupakan suatu masalah klasik yang dapat diselesaikan dengan mudah menggunakan rekursi tetapi cukup sulit diselesaikan bila tanpa rekursi.



Gambar 6.6 Tantangan menara Hanoi adalah memindahkan semua disk dari menara A ke menara B tanpa mengabaikan aturan-aturan yang ditetapkan


Masalah ini melibatkan aksi untuk memindahkan sejumlah disk tertentu dengan berbagai ukuran dari satu menara ke menara lain dengan mentaati beberapa aturan sebagai berikut:
·         Terdapat n buah disk berlabel 1, 2, 3,...,n dan tiga menara berlabel A, B, dan C.
·         Tidak ada disk yang berada di atas suatu disk lebih kecil pada sembarang waktu.
·         Semua disk awalnya ditempatkan pada menara A.
·         Hanya satu disk yang boleh digerakkan atau dipindahkan pada suatu waktu, dan disk tersebut harus berada di atas suatu menara.
Tujuan tantangan ini adalah untuk memindahkan semua disk dari menara A ke menara B dengan bantuan menara C. Sebagai contoh, jika Anda memiliki tiga disk, maka beberapa langkah yang dibutuhkan untuk memindahkan semua disk dari menara A ke menara B ditampilkan pada Gambar 6.6.

Pada kasus tiga disk, Anda dapat menemukan solusi secara manual. Untuk jumlah disk yang lebih besar, bahkan bila hanya empat disk, masalah menjadi cukup kompleks. Beruntung bahwa masalah ini bisa diselesaikan secara rekursif.



Gambar 6.7 Masalah menara Hanoi dapat didekomposisi menjadi tiga submasalah


Kasus basis untuk masalah ini adalah n = 1. Jika n == 1, maka Anda hanya perlu memindahkan disk dari A ke B. Jika n > 1, maka Anda perlu memecah masalah awal menjadi tiga submasalah dan menyelesaikannya secara sekuensial.
1.  Memindahkan n – 1 disk pertama dari A ke C dengan bantuan menara B, seperti tertampil pada Gambar 6.7.
2.  Memindahkan n disk dari A ke B, seperti tertampil pada langkah 2 pada Gambar 6.7.
3.  Memindahkan n – 1 disk dari C ke B dengan bantuan menara A, seperti tertampil pada langkah 3 pada Gambar 6.7.
Metode berikut memindahkan n disk dari dariMenara ke keMenara dengan bantuan bantuanMenara:

void pindahDisk(int n, char dariMenara, char keMenara, char bantuanMenara)

Algoritma untuk metode ini dapat dideskripsikan sebagai berikut:

if (n == 1) // Kondisi penghenti
  Memindahkan disk 1 dari dariMenara ke keMenara;
else {
  pindahDisk(n - 1, dariMenara, bantuanMenara, keMenara);
  Memindahkan n disk dari dariMenara ke keMenara;
  pindahDisk (n - 1, bantuanMenara, keMenara, dariMenara);
}

Kode6.8 menyajikan suatu contoh yang meminta pengguna untuk memasukkan sejumlah disk dan yang memanggil metode rekursif pindahDisk untuk menampilkan solusi dalam memindahkan disk-disk yang diberikan.

Kode6.8 MenaraHanoi.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.util.Scanner;

public class MenaraHanoi {
  /** Metode utama */
  public static void main(String[] args) {
    // Menciptakan suatu Scanner
    Scanner masukan = new Scanner(System.in);
    System.out.print("Masukkan sejumlah disk: ");
    int n = masukan.nextInt();

    // Mencari solusi secara rekursif
    System.out.println("Perpindahan yang dilakukan adalah:");
    pindahDisk(n, 'A', 'B', 'C');
  }

  /** Metode untuk mencari solusi untuk memindahkan n disk
      dari dariMenara ke keMenara dengan bantuan bantuanMenara */
  public static void pindahDisk(int n, char dariMenara,
      char keMenara, char bantuanMenara) {
    if (n == 1) // Kondisi penghenti
      System.out.println("Memindahkan disk" + n + " dari " +
        dariMenara + " ke " + keMenara);
    else {
      pindahDisk(n - 1, dariMenara, bantuanMenara, keMenara);
      System.out.println("Memindahkan disk " + n + " dari " +
        dariMenara + " ke " + keMenara);
      pindahDisk(n - 1, bantuanMenara, keMenara, dariMenara);
    }
  }
}

Keluaran

Masukkan sejumlah disk: 4
Perpindahan yang dilakukan adalah:
Memindahkan disk 1 dari A ke C
Memindahkan disk 2 dari A ke B
Memindahkan disk 1 dari C ke B
Memindahkan disk 3 dari A ke C
Memindahkan disk 1 dari B ke A
Memindahkan disk 2 dari B ke C
Memindahkan disk 1 dari A ke C
Memindahkan disk 4 dari A ke B
Memindahkan disk 1 dari C ke B
Memindahkan disk 2 dari C ke A
Memindahkan disk 1 dari B ke A
Memindahkan disk 3 dari C ke B
Memindahkan disk 1 dari A ke C
Memindahkan disk 2 dari A ke B
Memindahkan disk 1 dari C ke B

Masukkan sejumlah disk: 6
Perpindahan yang dilakukan adalah:
Memindahkan disk 1 dari A ke C
Memindahkan disk 2 dari A ke B
Memindahkan disk 1 dari C ke B
Memindahkan disk 3 dari A ke C
Memindahkan disk 1 dari B ke A
Memindahkan disk 2 dari B ke C
Memindahkan disk 1 dari A ke C
Memindahkan disk 4 dari A ke B
Memindahkan disk 1 dari C ke B
Memindahkan disk 2 dari C ke A
Memindahkan disk 1 dari B ke A
Memindahkan disk 3 dari C ke B
Memindahkan disk 1 dari A ke C
Memindahkan disk 2 dari A ke B
Memindahkan disk 1 dari C ke B
Memindahkan disk 5 dari A ke C
Memindahkan disk 1 dari B ke A
Memindahkan disk 2 dari B ke C
Memindahkan disk 1 dari A ke C
Memindahkan disk 3 dari B ke A
Memindahkan disk 1 dari C ke B
Memindahkan disk 2 dari C ke A
Memindahkan disk 1 dari B ke A
Memindahkan disk 4 dari B ke C
Memindahkan disk 1 dari A ke C
Memindahkan disk 2 dari A ke B
Memindahkan disk 1 dari C ke B
Memindahkan disk 3 dari A ke C
Memindahkan disk 1 dari B ke A
Memindahkan disk 2 dari B ke C
Memindahkan disk 1 dari A ke C
Memindahkan disk 6 dari A ke B
Memindahkan disk 1 dari C ke B
Memindahkan disk 2 dari C ke A
Memindahkan disk 1 dari B ke A
Memindahkan disk 3 dari C ke B
Memindahkan disk 1 dari A ke C
Memindahkan disk 2 dari A ke B
Memindahkan disk 1 dari C ke B
Memindahkan disk 4 dari C ke A
Memindahkan disk 1 dari B ke A
Memindahkan disk 2 dari B ke C
Memindahkan disk 1 dari A ke C
Memindahkan disk 3 dari B ke A
Memindahkan disk 1 dari C ke B
Memindahkan disk 2 dari C ke A
Memindahkan disk 1 dari B ke A
Memindahkan disk 5 dari C ke B
Memindahkan disk 1 dari A ke C
Memindahkan disk 2 dari A ke B
Memindahkan disk 1 dari C ke B
Memindahkan disk 3 dari A ke C
Memindahkan disk 1 dari B ke A
Memindahkan disk 2 dari B ke C
Memindahkan disk 1 dari A ke C
Memindahkan disk 4 dari A ke B
Memindahkan disk 1 dari C ke B
Memindahkan disk 2 dari C ke A
Memindahkan disk 1 dari B ke A
Memindahkan disk 3 dari C ke B
Memindahkan disk 1 dari A ke C
Memindahkan disk 2 dari A ke B
Memindahkan disk 1 dari C ke B

Masalah ini secara inheren rekursif. Penggunaan rekursi menjadikan solusi sederhana. Akan menjadi susah untuk diselesaikan bila tanpa menggunakan rekursi.


Gambar 6.8 Pemanggilan pindahDisk(3, ‘A, ‘B’, ‘C’)  menyebabkan pemanggilan pindahDisk secara rekursif


Perhatikan penjejakan program untuk n = 3. Pemanggilan rekursif secara berurutan ditampilkan pada Gambar 6.8. Seperti yang Anda lihat, menulis program lebih mudah daripada menjejak pemanggilan rekursif. Sistem menggunakan tumpukan untuk menjejak beberapa pemanggilan di balik layar. Pada beberapa kasus, rekursi memberikan suatu level abstraksi yang menyembunyikan iterasi dan beberapa detil lain dari pengguna.


6.8 Masalah: Fraktal
Fraktal merupakan suatu bangun geometrik, tetapi tidak seperti segitiga, lingkaran, atau persegipanjang, fraktal dapat dibagi menjadi bagian-bagian, dimana setiap bagian merupakan salinan ukuran-tereduksi dari bangun asli. Ada banyak contoh fraktal yang menarik. Bagian ini akan mengenalkan suatu fraktal sederhana, segitiga Sierpinski, yang dinamai oleh seorang matematikawan berkebangsaan Polandia.



Gambar 6.9 Segitiga Sierpinski merupakan suatu pola segitiga rekursif

Segitiga Sierpinski diciptakan sebagai berikut:
1.  Mulai dengan suatu segitiga sama-sisi, yang dianggap sebagai fraktal Sierpinski orde (level) 0, seperti tertampil pada Gambar 6.9a.
2.  Menghubungkan titik-titik tengah setiap sisi segitiga orde 0 untuk menciptakan segitiga Sierpinski orde 1, seperti tertampil pada Gambar 6.9b.
3.  Menghubungkan titik-titik tengah setiap sisi dari tiga segitiga orde 1 untuk menciptakan segitiga Sierpinski orde 2, seperti tertampil pada Gambar 6.9c.
4.  Mengulangi proses yang sama secara rekursif untuk menciptakan segitiga Sierpinski orde 3, 4, dan seterusnya (Gambar 6.9d).
Masalah ini secara inheren rekursif. Bagaimana Anda mengembangkan solusi rekursif untuk masalah ini? Perhatikan bahwa kelas basis adalah ketika orde 0. Sangat mudah untuk menggambarkan suatu segitiga Sierpinski orde 0. Bagaimana Anda menggambarkan suatu segitiga Sierpinski orde 1? Masalah ini dapat direduksi menjadi masalah menggambarkan tiga segitiga Sierpinski orde 0. Bagaimana Anda menggambarkan suatu segitiga Sierpinski orde 2? Masalah ini dapat direduksi menjadi masalah menggambarkan tiga segitiga Sierpinski orde 1. Jadi masalah menggambarkan segitiga Sierpinski orde n dapat direduksi menjadi masalah menggambarkan tiga buah segitiga Sierpinski orde n – 1.

Kode6.9 menyajikan suatu applet JAVA yang menampilkan segitiga Sierpinski pada sembarang orde, seperti tertampil pada Gambar 6.9. Anda bisa mengentrikan suatu orde di dalam bidang teks untuk menampilkan segitiga Sierpinski pada suatu orde tertentu.

Kode6.9 SegitigaSierpinski.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

public class SegitigaSierpinski extends JApplet {
  private JTextField jtfOrde = new JTextField("0", 5); // Orde
  private PanelSegitigaSierpinski panelSegitiga =
  new PanelSegitigaSierpinski(); // Menampilkan pola

  public SegitigaSierpinski() {
    // Panel untuk memuat label, bidang teks, dan suatu tombol
    JPanel panel = new JPanel();
    panel.add(new JLabel("Masukkan orde fraktal: "));
    panel.add(jtfOrde);
    jtfOrde.setHorizontalAlignment(SwingConstants.RIGHT);

    // Menambahkan suatu panel segitiga Sierpinski kepada applet
    add(panelSegitiga);
    add(panel, BorderLayout.SOUTH);

    // Meregistrasi suatu listener
    jtfOrde.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        panelSegitiga.tetapkanOrde(Integer.parseInt(jtfOrde.getText()));
      }
    });
  }

  static class PanelSegitigaSierpinski extends JPanel {
    private int orde = 0;

    /** Menetapkan suatu orde baru */
    public void tetapkanOrde(int orde) {
      this.orde = orde;
      repaint();
    }

    protected void paintComponent(Graphics g) {
      super.paintComponent(g);

      // Memilih tiga titik proporsional dengan ukuran panel
      Point p1 = new Point(getWidth() / 2, 10);
      Point p2 = new Point(10, getHeight() - 10);
      Point p3 = new Point(getWidth() - 10, getHeight() - 10);

      tampilSegitiga(g, orde, p1, p2, p3);
    }

    private static void tampilSegitiga(Graphics g, int orde,
       Point p1, Point p2, Point p3) {
      if (orde >= 0) {
        // Menggambar suatu segitiga untuk menghubungkan tiga titik
        g.drawLine(p1.x, p1.y, p2.x, p2.y);
        g.drawLine(p1.x, p1.y, p3.x, p3.y);
        g.drawLine(p2.x, p2.y, p3.x, p3.y);

        // Mendapatkan titik-tengah pada setiap sisi segitiga
        Point p12 = titikTengah(p1, p2);
        Point p23 = titikTengah(p2, p3);
        Point p31 = titikTengah(p3, p1);

        // Menampilkan secara rekursif tiga segitiga
        tampilSegitiga(g, orde - 1, p1, p12, p31);
        tampilSegitiga(g, orde - 1, p12, p2, p23);
        tampilSegitiga(g, orde - 1, p31, p23, p3);
      }
    }

    private static Point titikTengah(Point p1, Point p2) {
      return new Point((p1.x + p2.x) / 2, (p1.y + p2.y) / 2);
    }
  }
}

Segitiga awal memiliki tiga titik yang proporsional dengan ukuran panel (baris 42-44). Metode tampilSegitiga(g, orde, p1, p2, p3) (baris 49-67) melakukan beberapa pekerjaan sebagai berikut:
1.  Menampilkan suatu segitiga untuk menghubungkan tiga titik p1, p2, dan p3 pada baris 53-55, seperti tertampil pada Gambar 6.10a.
2.  Mendapatkan titik tengah antara p1 dan p2 (baris 58), titik tengah antara p2 dan p3 (baris 59), dan titik tengah antara p3 dan p1 (baris 60), seperti tertampil pada Gambar 6.10b.
3.  Secara rekursif memanggil metode tampilSegitiga dengan suatu orde tereduksi untuk menampilkan segitiga-segitiga Sierpinski yang lebih kecil (baris 63-66). Perhatikan bahwa setiap segitiga Sierpinski kecil secara struktural sama dengan segitiga Sierpinski besar, seperti tertampil pada Gambar 6.10b.
Suatu segitiga Sierpinski ditampilkan di dalam PanelSegitigaSierpinski. Properti orde di dalam kelas inner PanelSegitigaSierpinski menentukan orde segitiga Sierpinski. Kelas Point merepresentasikan suatu titik di dalam komponen. Metode titikTengah(Point p1, Point p2) mengembalikan titik tengah antara p1 dan p2 (baris 72-74).

Gambar 6.10 Penggambaran segitiga Sierpinski menyebabkan pemanggilan untuk menggambar tiga segitiga Sierpinski secara rekursif


6.9 Masalah: Delapan Ratu
Bagian ini akan memberikan suatu solusi rekursif atas masalah Delapan Ratu yang disajikan pada awal bab ini. Tantangannya adalah untuk menemukan solusi untuk menempatkan suatu ratu pada tiap baris papan catur sehingga tidak terdapat dua ratu yang dapat menyerang satu sama lain. Anda bisa menggunakan suatu array dua-dimensi untuk merepresentasikan papan catur. Namun, karena setiap baris hanya boleh memiliki satu ratu, maka hanya akan digunakan array satu-dimensi untuk menandai posisi ratu di dalam baris. Jadi, Anda bisa mendefinisikan array ratu sebagai berikut:

int[] ratu = new int[8];.

Penugasan j kepada ratu[i] dilakukan untuk menandai suatu ratu yang ditempatkan pada baris i kolom j. Gambar 6.11a menunjukkan isi array ratu untuk papan catur pada Gambar 6.11b.


Gambar 6.11 Array ratu[i] menandai posisi ratu pada baris i


Kode6.10 DelapanRatu.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import java.awt.*;
import javax.swing.*;

public class DelapanRatu extends JApplet {
  public static final int UKURAN = 8; // Ukuran papan catur
  private int[] ratu = new int[UKURAN]; // Posisi ratu

  public DelapanRatu() {
    cari(0); // Mencari solusi dari baris 0
    add(new PapanCatur(), BorderLayout.CENTER); // Menampilkan solusi
  }

  /** Memeriksa apakah suatu ratu dapat ditempatkan pada baris i dan kolom j */
  private boolean apaValid(int baris, int kolom) {
    for (int i = 1; i <= baris; i++)
      if (ratu[baris - i] == kolom // Memeriksa kolom
        || ratu[baris - i] == kolom - i // Memeriksa diagonal atas-kiri
        || ratu[baris - i] == kolom + i) // Memeriksa diagonal atas-kanan
        return false; // Ada konflik
      return true; // Tidak ada konflik
  }

  /** Mencari solusi mulai dari baris tertentu */
  private boolean cari(int baris) {
    if (baris == UKURAN) //kondisi penghenti
      return true; // Silusi ditemukan untuk menempatkan 8 ratu pada 8 baris

    for (int kolom = 0; kolom < UKURAN; kolom++) {
      ratu[baris] = kolom; // Menempatkan ratu pada (baris, kolom)
      if (apaValid(baris, kolom) && cari(baris + 1))
        return true; // Ditemukan, mengembalikan true untuk keluar loop
    }

    // Tidak ada solusi untuk ratu yang ditempatkan pada sembarang kolom pada baris ini
    return false;
  }

  class PapanCatur extends JPanel {
    private Image citraRatu =
       new ImageIcon("Gambar/ratu.gif").getImage();

    PapanCatur() {
      this.setBorder(BorderFactory.createLineBorder(Color.BLACK, 2));
    }

    protected void paintComponent(Graphics g) {
      super.paintComponent(g);

      // Menggambar ratu
      for (int i = 0; i < UKURAN; i++) {
        int j = ratu[i]; // Posisi ratu pada baris i
        g.drawImage(citraRatu, j * getWidth() / UKURAN,
          i * getHeight() / UKURAN, getWidth() / UKURAN,
        getHeight() / UKURAN, this);
      }

      // Menggambar garis horisontal dan fertikal
      for (int i = 1; i < UKURAN; i++) {
        g.drawLine(0, i * getHeight() / UKURAN,
        getWidth(), i * getHeight() / UKURAN);
        g.drawLine(i * getWidth() / UKURAN, 0,
          i * getWidth() / UKURAN, getHeight());
      }
    }
  }
}

Program memanggil cari(0) (baris 9) untuk memulai pencarian untuk solusi pada baris 0, yang secara rekursif memanggil cari(1), cari(2),..., dan cari(7) (baris 30).

Metode cari(baris) mengembalikan true jika semua baris terisi (baris 25-26). Metode ini memeriksa apakah suatu ratu telah ditempatkan pada kolom 0, 1, 2, ..., dan 7 di dalam loop for (baris 28). Kemudian program menempatkan ratu pada kolom (baris 29). Jika penempatan valid, maka pencarian rekursif untuk baris berikutnya dilakukan dengan memanggil cari(baris+1) (baris 30). Jika pencarian berhasil, maka akan metode akan mengembalikan true (baris 31) agar keluar dari loop for. Pada kasus ini, tidak ada kebutuhan untuk kolom berikutnya pada baris tersebut. Jika tidak ada solusi agar suatu ratu ditempatkan pada sembarang kolom di dalam baris ini, maka metode akan mengembalikan false (baris 35).

Dimisalkan bahwa Anda memanggil cari(baris) untuk baris 3, seperti tertampil pada Gambar 6.12a. Metode mencoba untuk mengisi suatu ratu pada kolom 1, 2, dan seterusnya. Untuk setiap percobaan, metode apaValid(baris, kolom) (baris 30) dipanggil untuk memeriksa apakah penempatan suatu ratu pada posisi tertentu akan mengakibatkan konflik atau tidak dengan ratu-ratu yang telah ditempatkan sebelumnya. Hal ini untuk memastikan  bahwa tidak ada ratu yang ditempatkan pada kolom yang sama (baris 16), tidak ada ratu yang ditempatkan pada diagonal atas-kiri (baris 17), tidak ada ratu yang ditempatkan pada diagonal atas-kanan (baris 18), seperti tertampil pada Gambar 6.12a. Jika metode apaValid(baris, kolom) mengembalikan false, maka kolom berikutnya akan diperiksa, seperti tertampil pada Gambar 6.12b. Jika metode apaValid(baris, kolom) mengembalikan true, maka metode cari(baris+1) dipanggil, seperti tertampil pada Gambar 6.12d. Jika metode cari(baris+1) mengembalikan false, maka kolom berikutnya pada baris terdahulu diperiksa, seperti tertampil pada Gambar 6.12c.


Gambar 6.12 Pemanggilan cari(baris) mengisi ratu pada suatu kolom dalam baris tertentu









No comments:

Post a Comment