Bubble sort adalah algoritma sorting paling sederhana yang sering dipakai sebagai pengantar konsep sorting. Algoritma ini bekerja dengan membandingkan setiap pasangan elemen berdekatan dan menukarnya bila urutan salah. Walaupun kompleksitasnya O(n^2)—jauh lebih lambat dibanding algoritma modern seperti merge sort—bubble sort punya keunggulan: implementasi sangat sederhana dan stabil. Pada chapter ini kita akan mengimplementasikan versi naive, versi optimized dengan early exit, lalu menghitung pass dan pertukaran untuk berbagai skenario data.
Cara Kerja Bubble Sort #
Pada setiap pass, algoritma menelusuri array dari kiri ke kanan, membandingkan elemen a[j] dan a[j+1]. Bila a[j] > a[j+1], keduanya ditukar. Setelah satu pass, elemen terbesar akan “menggelembung” ke posisi paling kanan. Algoritma mengulang pass ini hingga n-1 kali. Kompleksitas waktu O(n^2) muncul dari loop bersarang n x n.
Optimisasi penting: bila satu pass tidak menghasilkan pertukaran apapun, array sudah terurut dan algoritma boleh berhenti lebih awal. Pada kasus data yang sudah hampir terurut, optimisasi ini menurunkan kompleksitas praktis menjadi O(n).
Versi Naive dan Optimized #
Kita akan menulis dua versi bubble sort untuk membandingkan keduanya.
#include <stdio.h>
/* Bubble sort naive: selalu n-1 pass */
void bubble_naive(int *a, int n) {
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (a[j] > a[j + 1]) {
int t = a[j]; a[j] = a[j + 1]; a[j + 1] = t;
}
}
}
}
/* Bubble sort optimized dengan early exit */
void bubble_optimized(int *a, int n) {
for (int i = 0; i < n - 1; i++) {
int ditukar = 0;
for (int j = 0; j < n - i - 1; j++) {
if (a[j] > a[j + 1]) {
int t = a[j]; a[j] = a[j + 1]; a[j + 1] = t;
ditukar = 1;
}
}
if (!ditukar) break; /* tidak ada pertukaran -> sudah terurut */
}
}
static void cetak(const int *a, int n, const char *label) {
printf("%s: ", label);
for (int i = 0; i < n; i++) printf("%d ", a[i]);
printf("\n");
}
int main(void) {
int a[] = {5, 1, 4, 2, 8, 3, 7, 6};
int n = sizeof(a) / sizeof(a[0]);
cetak(a, n, "Sebelum");
bubble_naive(a, n);
cetak(a, n, "Naive ");
int b[] = {5, 1, 4, 2, 8, 3, 7, 6};
bubble_optimized(b, n);
cetak(b, n, "Optimiz");
/* Kasus sudah terurut */
int c[] = {1, 2, 3, 4, 5};
int m = 5;
bubble_optimized(c, m);
cetak(c, m, "Terurut");
return 0;
}Output yang Dihasilkan:
Sebelum: 5 1 4 2 8 3 7 6
Naive : 1 2 3 4 5 6 7 8
Optimiz: 1 2 3 4 5 6 7 8
Terurut: 1 2 3 4 5 Kedua versi menghasilkan array terurut. Perbedaan utama adalah behavior pada data yang sudah terurut: versi optimized berhenti setelah 1 pass, sedangkan versi naive tetap menjalankan n-1 pass sia-sia.
Mari kita telaah baris-baris kode di atas:
- Baris 4-11 (
bubble_naive): dua loop bersarang. Loop luaridari0sampain-2(totaln-1pass). Loop dalamjdari0sampain-i-2(membandingkan elemen berdekatan). Setiap pasangan yang urutannya salah ditukar dengan trik variabel sementarat. - Baris 14-26 (
bubble_optimized): struktur sama dengan naive, ditambah flagditukar. Flag direset ke0di awal setiap pass lalu di-set1bila ada pertukaran. Bila setelah pass selesaiditukarmasih0, array sudah terurut—breakkeluar dari loop luar. - Baris 24 (
if (!ditukar) break;): inti optimisasi. Trik ini menurunkan best case dariO(n^2)menjadiO(n). - Baris 28-32 (
cetak): helper untuk mencetak array dengan label, mempermudah verifikasi. - Baris 38-39 (
int b[] = {...}; bubble_optimized(b, n);): salinan data untuk versi optimized karenabubble_naivesudah memodifikasiadi tempat.
Menghitung Jumlah Pass dan Pertukaran #
Untuk memahami kapan optimisasi membantu, kita akan menghitung jumlah pass dan pertukaran pada tiga skenario: data acak, data terurut, dan data terbalik.
#include <stdio.h>
/* Bubble sort dengan penghitung pass dan pertukaran */
void bubble_count(int *a, int n, int *jumlah_pass, int *jumlah_tukar) {
*jumlah_pass = 0;
*jumlah_tukar = 0;
for (int i = 0; i < n - 1; i++) {
int ditukar = 0;
(*jumlah_pass)++;
for (int j = 0; j < n - i - 1; j++) {
if (a[j] > a[j + 1]) {
int t = a[j]; a[j] = a[j + 1]; a[j + 1] = t;
ditukar = 1;
(*jumlah_tukar)++;
}
}
if (!ditukar) break;
}
}
int main(void) {
int a[] = {5, 1, 4, 2, 8, 3, 7, 6};
int b[] = {1, 2, 3, 4, 5};
int c[] = {5, 4, 3, 2, 1};
int n = 5;
int pass, tukar;
bubble_count(a, 8, &pass, &tukar);
printf("Acak (8 elemen): pass=%d, tukar=%d\n", pass, tukar);
bubble_count(b, n, &pass, &tukar);
printf("Terurut (5 elemen): pass=%d, tukar=%d\n", pass, tukar);
bubble_count(c, n, &pass, &tukar);
printf("Terbalik(5 elemen): pass=%d, tukar=%d\n", pass, tukar);
return 0;
}Output yang Dihasilkan:
Acak (8 elemen): pass=4, tukar=10
Terurut (5 elemen): pass=1, tukar=0
Terbalik(5 elemen): pass=4, tukar=10Hasilnya menggambarkan tiga kelas kompleksitas praktis:
- Data terurut: 1 pass, 0 pertukaran—algoritma langsung berhenti. Best case
O(n). - Data acak: jumlah pass lebih sedikit dari
n-1karena pertukaran terjadi. Average caseO(n^2). - Data terbalik: butuh
n-1pass penuh dengan pertukaran maksimum. Worst caseO(n^2)dengan konstanta paling besar.
Kapan Bubble Sort Cocok Dipakai #
Walaupun bubble sort jarang dipakai di produksi, ada situasi di mana algoritma ini masuk akal:
- Data hampir terurut: optimisasi early exit membuat bubble sort berjalan cepat pada data yang hanya butuh sedikit pertukaran.
- Ukuran data sangat kecil (n < 50): overhead rendah dan kode sederhana.
- Stability diperlukan: bubble sort natural stabil tanpa modifikasi.
- Memori sangat terbatas: bubble sort in-place dengan space complexity
O(1).