Algoritma-algoritma Pengurutan Internal
28 April 1999
Proses pengurutan seringkali menjadi bagian yang krusial dalam setiap pengolahan data sehingga mengapa ini merupakan topik yang penting. Contohnya dalam berbagai aplikasi dengan data ribuan bahkan jutaan, pengurutan bisa menghabiskan CPU-time paling banyak dari keseluruhan pengolahan data.
Masalah pengurutan itu sendiri selama ini memunculkan bervariasi algoritma. Yang akan dibahas disini adalah algoritma-algoritma yang memiliki keunikan tersendiri. Di luar algoritma-algoritma yang dibahas ada puluhan algoritma lain yang pada dasarnya variant-variant dari algoritma-algoritma yang akan dibahas berikut ini.
Mengikuti pembahasan buku teks yang digunakan, algoritma-algoritma tersebut dikategorikan ke dalam sejumlah kelompok:
Sebagai asumsi dasar dalam pembahasan selanjutnya tanpa menghilangkan sifat umumnya, pengurutan-pengurutan dilakukan menaik. Pengurutan menurun pada dasarnya sama kecuali pemeriksaan lojik adalah kebalikannya. Demikian halnya data yang diurutkan hanyalah array dari bilangan. Secara umum data dapat merupakan array dari suatu data record dari sejumlah field dan salah satunya digunakan kebagai key untuk pengurutan. Ragam dari field untuk pengurutan itu pun dapat pula berupa alfanumeris yang berimplikasi ekspresi-ekspresi lojik perlu disubstitusi oleh metoda-metoda lojik yang sesuai.
Dalam pengurutan ini kedua data set (terurut maupun yang belum) berada dalam array yang sama. Misalnya array tersebut adalah X, maka pada setiap saat terdapat i buah data terurut pada X[0], X[1], ..., X[i-1], dan data tak terurut pada X[i], X[i+1], ..., X[n-1]. Algoritma melakukan pencarian X[j] terkecil dari data set yang belum terurut tersebut, misalnya didapat X[m] lalu melakukan penukaran X[i] dengan X[m] sehingga kemudian sudah terurut i+1 buat data dalam X.
Algoritma selengkapnya adalah:
for (i=0; i < n-2; i++) {
m = i;
for (j = m+1; j < n-1; j++) {
if (X[m] > X[j]) m = j;
}
if (m != i) { t = X[m]; X[m] = X[i]; X[i] = t; }
}
m = j;
" bergantung pada kondisi data. Average case
adalah setengah dari jumlah iterasi dalam yang dilakukan pada setiap
iterasi luar, yang menghasilkan jumlah total 1/4 n2) operasi
tersebut. Kompleksitas dari algoritma ini O(n2).
Kompleksitas waktu dari algoritma ini adalah O(n log n) yang diperoleh dari n-1 kali iterasi dikalikan dengan proses reheapify yang merupakan proses logaritmis. Inisialisasi dengan heapify hanya memerlukan O(n) (worse case adalah O(n log n), sehingga kompleksitas waktu total masih merupakan O(n log n). Algoritma ini termasuk stabil; perbedaan antara worse case dengan best case tidak besar.
Contoh:
Dalam implementasinya pemeriksaan x[i] diefisienkan dengan menyimpan isi x[i] ke dalam suatu temporary variable, kemudian setiap x[j] di kirinya yang lebih besar dari t digeser ke kanan, hingga didapatkan kondisi j < 0 atau x[j] <= t. Jadi algoritma selengkapnya:
for (j =i-1; j >= 0 && !stop; j--) {
else {
break;
Suatu variant mengurangi pemeriksaan j >= 0 dengan menambahkan sentinel di awal array. Sentinel tersebut berharga bilangan yang tak hingga negatif, sehingga iterasi selalu berhenti. Masalahnya, kita harus melakukan inisialisasi dengan menggeserkan isi array satu posisi terlebih dahulu untuk dapat ditempati sentinel tersebut, dan di akhir algoritma kita harus menggeserkan kembali ke posisi semula.
loop-for dalam dilakukan dalam jumlah iterasi yang bervariasi: best case adalah 1 kali (langsung keluar), dan worse case adalah i-1 kali yaitu jika disisipkan di x[0]. Sehingga kompleksitas best case adalah O(n), worse-case adalah O(n2).
Pertanyaan: Apa yang menentukan kondisi worse/best case dalam hal ini? Beri contoh data yang best case dan contoh untuk yang worse case? Bagaimana dengan data acak?
Seperti yang kita ketahui kompleksitas operasi penyisipan pada BST adalah O(log n) dan operasi traversal adalah O(n). Untuk n data operasinya adalah O(n log n + n) = O(n log n).
Walaupun kompleksitasnya ini adalah O(n log n), algoritma ini kurang populer karena adanya kebutuhan ruang untuk BST.
Pada iterasi ke-i, pemeriksaan tsb. dilakukan pula dalam loop-for sbb.
Loop-for tersebut akan menggeser bilangan terkecil ke posisi i.
Loop-for dilakukan hanya sampai ke i karena pada iterasi ke-i data dalam
x[0], x[1], ..., x[i-1] merupakan yang paling kecil dan sudah terurut
hasil pengeseran yang dilakukan setiap loop sebelumnya.
Oleh sebab itu iterasi hanya dilakukan untuk harga i=0, 1, ..., n-2 atau
sampai tidak terjadi penukaran dalam suatu iterasi.
x[j] = x[j-1];
x[j-1] = tmp;
for (i=0; i < n-2 && ch; i++) {
for (j=n-1; j > i; j--) {
x[j] = x[j-1];
x[j-1] = tmp;
ch = true;
Contoh untuk mengurutkan data 34,43,65,90,48,82,93,86,26,76,49,23,56,37.
Pada iterasi i=0:
j=13: tukar 56-37 menjadi 34,43,65,90,49,82,93,86,26,76,49,23,37,56
j=12: tidak ada penukaran
j=11: tukar 49-23 menjadi 34,43,65,90,48,82,93,86,26,76,23,49,37,56
j=10: tukar 76-23 menjadi 34,43,65,90,48,82,93,86,26,23,76,49,37,56
j= 9: tukar 26-23 menjadi 34,43,65,90,48,82,93,86,23,26,76,49,37,56
j= 8: tukar 86-23 menjadi 34,43,65,90,48,82,93,23,86,26,76,49,37,56
j= 7: tukar 93-23 menjadi 34,43,65,90,48,82,23,93,86,26,76,49,37,56
j= 6: tukar 82-23 menjadi 34,43,65,90,48,23,82,93,86,26,76,49,37,56
j= 5: tukar 49-23 menjadi 34,43,65,90,23,48,82,93,86,26,76,49,37,56
j= 4: tukar 90-23 menjadi 34,43,65,23,90,48,82,93,86,26,76,49,37,56
j= 3: tukar 65-23 menjadi 34,43,23,65,90,48,82,93,86,26,76,49,37,56
j= 2: tukar 43-23 menjadi 34,23,43,65,90,48,82,93,86,26,76,49,37,56
j= 1: tukar 34-23 menjadi 23,34,43,65,90,48,82,93,86,26,76,49,37,56
Pada iterasi i=1:
j=13: tidak ada penukaran
j=12: tukar 49-37 menjadi 23,34,43,65,90,48,82,93,86,26,76,37,49,56
j=11: tukar 76-37 menjadi 23,34,43,65,90,48,82,93,86,26,37,76,49,56
j=10: tidak ada penukaran
j= 9: tukar 86-26 menjadi 23,34,43,65,90,48,82,93,26,86,37,76,49,56
j= 8: tukar 93-26 menjadi 23,34,43,65,90,48,82,26,93,86,37,76,49,56
j= 7: tukar 82-26 menjadi 23,34,43,65,90,48,26,82,93,86,37,76,49,56
...
j= 2: tukar 34-26 menjadi 23,26,34,43,65,90,48,82,93,86,37,76,49,56
Dari kedua iterasi dengan increment linear demikian dapat mudah dilihat bahwa algoritma ini memiliki kompleksitas O(n2) dan proses akan menjadi amat cepat kalau data sudah sebagian besar terurut. Masalah yang dihadapi algoritma ini adalah banyaknya penukaran yang dilakukan selama proses dalam kondisi normal. Efisiensi dapat ditingkatkan dengan mengurangi proses penukaran dengan penggeseran dan penyisipan seperti halnya insertion sort. Untuk lingkungan pemrograman paralel dengan array processor pengurutan ini menjadi amat menarik untuk dikaji.
Jika Bobble Sort dalam setiap iterasi melakukan loop-for penukaran ke satu arah, Shaker Sort (suatu variant dari Bubble Sort) melakukan loop-for penukaran dengan arah bolak-balik dengan batas loop-for kiri dan kanan semakin menyempit.
Dengan suatu bilangan integer positif D, maka setiap X[i], X[i+D], X[i+2D, ..., dimasukkan dalam satu subset yang sama dari D subset yang ada. Pengurutan a'la Insertion Sort dilakukan pada masing-masing subset. Jika D=1 maka yang terjadi adalah Insertion Sort murni.
Sejumlah harga D: D1, D2, ..., Dh, di mana D1 > D2 > ... > Dh = 1, masing-masing diaplikasikan dengan cara di atas berturut-turut mulai dari yang terbesar hingga yang terkecil maka akhirnya terjadi hal-hal sebagai berikut:
Kompleksitas dari algoritma ini secara worse case adalah dengan waktu O(n2) tetapi dengan pemilihan harga-harga D yang tepat dapat menghasilkan kompleksitas waktu yang kurang dari itu. Namun sejumlah literatur berdasarkan pengukuran eksperimental mendapatkan average case dengan waktu O(n1.25).
Jika data set berada dalam array X berindeks dari l sampai dengan r maka pembagian ini mempertukarkan sejumlah isi array sehingga sub data set kiri berada dalam array yang sama berindeks l sampai dengan t-1 dan sub data set kanan berada dalam array berindeks t+1 sampai dengan r. X[t] sendiri ditempati oleh pivot.
Setelah dilakukan pembagian tersebut maka algoritma Quick Sort diaplikasikan untuk masing-masing sub data set tsb dan seterusnya secara rekursif hingga terbentuk sub data set yang tidak dapat dibagi lagi yaitu jika hanya berisi satu sel (l = r).
Bagian yang paling tricky adalah pada pembagian data set, kita sebut fungsi tersebut adalah Partition(l,r) yang menghasilkan t yaitu posisi pivotnya. Maka, algoritma Quick Sort adala sebagai berikut.
QuickSort(l,t-1);
QuickSort(t+1,r);
Proses Partisi diserahkan kepada anda untuk mempelajarinya sendiri. Dalam beberapa literatur terdapat variant-varuant yang pada dasarnya terjadi karena perbedaan cara menentukan harga pivot: bisa data pertama (X[l]), data terakhir (X[r]) atau data ditengah (X[(l+r)/2]) data set).
Kompleksitas algoritma Quick Sort adalah bergantung pada hasil partisi tersebut. Kondisi best case adalah jika dalam setiap partisi tidak dilakukan operasi pertukaran tempat dan pivot tepat berada ditengah subset (O(n)). Kondisi average case adalah jika partisi menghasilkan sub data set yang balance (o(n log n)). Kondisi worse case adalah jika partisi menghasilkan sub data set kosong dan yang lain besar (o(n2). Walaupun demikian, algoritma ini cenderung untuk average case.
Algoritma ini memiliki kompleksitas O(n log n).
MergeSort((l+r)/2,r);
Merging();
Contoh radix disini adalah digit-digit dari data. Misalnya data berisi bilangan antara 000 sampai 999, maka ada 3 digit yang bisa digunakan sebagai radix (000, 001, 002, ... 999). Digit terkanan adalah radix paling tidak signifikan, dan digit terkiri adalah yang paling signifikan.
Contoh untuk mengurutkan data: 121, 076, 823, 367, 232, 824, 434, 742, 936, 274, 736, 576, 875, 268, 764, 548, 648, 358, 637, 534.
Pertama kali dibagi sesuai digit terkanan: | Kemudian dibagi sesuai digit kedua: | Terakhir sesuai digit terkiri: |
Subset#0: (kosong) Subset#1: 121 Subset#2: 232 742 Subset#3: 823 Subset#4: 824 434 274 764 534 Subset#5: 875 Subset#6: 076 936 736 576 Subset#7: 367 637 Subset#8: 268 548 648 358 Subset#9: (kosong) Hasil penggabungannya adalah: 121 232 742 823 824 434 274 764 534 875 076 936 736 576 367 637 268 548 648 358 |
Subset#0: (kosong) Subset#1: (kosong) Subset#2: 121 823 824 Subset#3: 232 434 534 936 736 637 Subset#4: 742 548 648 Subset#5: 358 Subset#6: 764 367 268 Subset#7: 274 875 076 576 Subset#8: (kosong) Subset#9: (kosong) Hasil penggabungannya adalah: 121 823 824 232 434 534 936 736 637 742 548 648 358 764 367 268 274 875 076 576 |
Subset#0: 076 Subset#1: 121 Subset#2: 232 268 274 Subset#3: 358 367 Subset#4: 434 Subset#5: 534 548 576 Subset#6: 637 648 Subset#7: 736 742 764 Subset#8: 823 824 875 Subset#9: 936 Hasil penggabungannya adalah: 076 121 232 268 274 358 367 434 534 548 576 637 648 736 742 764 823 824 875 936 |
Dalam contoh tersebut pengetian radix adalah digit. Dalam penggunaannya bisa saja radix adalah dalam ukuran-ukuran 3 bit (oktal) dari data, atau bisa juga karakter ASCII (1 byte) dari data. Yang perlu diperhatikan adalah jika radix berukuran r bit maka akan diperlukan 2r buah subset yang masing-masing harus disediakan dalam ukuran maksimumnya (yaitu sebanyak datanya). Jadi diperlukan space tambahan untuk 2r*n data. Jika r terlalu besar jelas akan memakan banyak ruang, sebaliknya jika r terlalu kecil maka akan diperlukan lebih banyak iterasi.
Jika jumlah radix adalah d dan jumlah data adalah n maka operasi pengurutan ini memerlukan penempatan data dalam subset sebanyak dn kali yang berarti memiliki kompleksitas waktu O(n). Namun dicapainya waktu linear tersebut harus diiringi pertimbangan kebutuhan ruang yang besar.
minValue = INFINITY; maxValue = -INFINITY;
for (i=0; i < n; i++) {
if (X[i] > maxValue) maxValue = X[i];
if (X[i] < minValue) minValue = X[i];
}
intervalLength = (maxValue-minValue)/(n-1));
for (i=0; i < n; i++) mapKey[i] = (int)((X[i]-minValue)/intervalLength);
for (i=0; i < n; i++) hitCount[mapKey[i]]++;
proxMap[0] = 0;
for (i=1; i < n; i++) proxMap[i] = proxMap[i-1]+hitCount[i-1];
for (i=0; i < n; i++) Y[i] = INFINITY;
for (i=0; i < n; i++) {
j = proxMap[mapKey[i]];
for(k = j+hitCount[mapKey[i]]-2; k >= j; k--) {
if (X[i] < Y[k]) Y[k+1] = Y[k];
else break;
}
Y[k+1] = X[i];
}
Jika pengurutan berkaitan dengan array dari data record yang berukuran besar, maka array Y dapat digantikan oleh array Location yang hanya menyimpan posisi seharusnya data itu berada dalam array hasil pengurutan.
Algoritma ini jika diimplementasikan untuk data key alfanumeris memerlukan fungsi pemetaan yang dapat memetakan key alfanumeris ke dalam bilangan dalam mapKey (atau sering disebut juga sebahai ordinalisasi yang dilakukan pada Hashing).
Algoritma ini melakukan enam (6) loop-for dan setiap loop-for beriterasi sebanyak n. Best case algoritma ini algoritma ini adalah O(n). Dalam kondisi ekstrim bisa jadi terbentuk interval yang amat gemuk sehingga pada akhirnya akan mendekati operasi Insertion Sort murni bahkan lebih buruk lagi akibat overhead untuk melakukan penghitungan-penghitungan di atas. Secara normal diharapkan interval tidak terbentuk terlalu panjang sehingga operasi Insertion Sort itu pun masih cukup efisien.