In-place sorting adalah properti algoritma sorting yang hanya menggunakan sejumlah kecil memori tambahan — biasanya O(1) — selain memori untuk input itu sendiri. Properti ini penting pada lingkungan dengan memori terbatas (embedded systems, real-time systems) atau saat menangani dataset yang besar di mana alokasi buffer O(n) tidak feasible. Beberapa algoritma (bubble, insertion, heap sort) benar-benar O(1) memori tambahan, sementara algoritma lain (merge sort, counting sort) membutuhkan O(n) atau lebih. Pada Chapter ini kita akan membedah definisi in-place, mengidentifikasi algoritma mana yang in-place, dan menunjukkan trade-off antara in-place dan stabilitas. Pada akhir chapter kita akan paham kapan in-place menjadi prioritas utama.
Definisi In-Place #
Suatu algoritma sorting disebut in-place bila memori tambahan yang digunakan selain input adalah O(1) — sejumlah konstanta variabel saja, tidak bergantung pada ukuran input n.
Catatan teknis:
- Variabel loop, indeks, flag — dihitung
O(1), tidak masalah berapa punn. - Call stack rekursi — dihitung sebagai memori tambahan. Quick sort memerlukan
O(log n)call stack pada best case, sehingga secara teknis bukanO(1), tetapi sering diklasifikasikan “in-place” karenaO(log n)sangat kecil. - Buffer sementara — merge sort memerlukan
O(n)buffer, sehingga tidak in-place.
Algoritma Mana yang In-Place #
In-Place (O(1) auxiliary) | Tidak In-Place |
|---|---|
| Bubble sort | Merge sort (O(n) buffer) |
| Selection sort | Counting sort (O(n + k)) |
| Insertion sort | Radix sort (O(n + b)) |
Quick sort (O(log n) call stack) | Bucket sort (O(n + k)) |
| Heap sort | Tim sort (O(n) buffer) |
| Shell sort |
Demonstrasi: In-Place vs Non In-Place #
Mari kita bandingkan insertion sort (in-place) dengan merge sort (membutuhkan buffer).
#include <iostream>
#include <vector>
#include <algorithm>
void insertion_sort_inplace(std::vector<int>& v) {
for (std::size_t i = 1; i < v.size(); ++i) {
int key = v[i];
std::size_t j = i;
while (j > 0 && v[j - 1] > key) {
v[j] = v[j - 1];
--j;
}
v[j] = key;
}
}
void merge_with_buffer(std::vector<int>& v, int lo, int mid, int hi, std::vector<int>& buf) {
int i = lo, j = mid + 1, k = lo;
while (i <= mid && j <= hi) {
if (v[i] <= v[j]) buf[k++] = v[i++];
else buf[k++] = v[j++];
}
while (i <= mid) buf[k++] = v[i++];
while (j <= hi) buf[k++] = v[j++];
for (int t = lo; t <= hi; ++t) v[t] = buf[t];
}
void merge_sort(std::vector<int>& v, int lo, int hi, std::vector<int>& buf) {
if (lo >= hi) return;
int mid = lo + (hi - lo) / 2;
merge_sort(v, lo, mid, buf);
merge_sort(v, mid + 1, hi, buf);
merge_with_buffer(v, lo, mid, hi, buf);
}
void merge_sort_non_inplace(std::vector<int>& v) {
if (v.size() < 2) return;
std::vector<int> buf(v.size());
merge_sort(v, 0, static_cast<int>(v.size()) - 1, buf);
}
void print(const std::vector<int>& v) {
for (int x : v) std::cout << ' ' << x;
std::cout << '\n';
}
int main() {
std::vector<int> a = {38, 27, 43, 3, 9, 82, 10};
std::cout << "Sebelum : "; print(a);
auto b = a;
insertion_sort_inplace(b);
std::cout << "Insertion : "; print(b);
auto c = a;
merge_sort_non_inplace(c);
std::cout << "Merge (buf) : "; print(c);
auto d = a;
std::sort(d.begin(), d.end());
std::cout << "std::sort : "; print(d);
return 0;
}Output yang Dihasilkan:
Sebelum : 38 27 43 3 9 82 10
Insertion : 3 9 10 27 38 43 82
Merge (buf) : 3 9 10 27 38 43 82
std::sort : 3 9 10 27 38 43 82Bedah Kode Baris-per-Baris #
insertion_sort_inplace: hanya menggunakan variabelkey,i,j— totalO(1)memori tambahan. Elemen digeser di tempat tanpa buffer eksternal.merge_sort_non_inplace: mengalokasikanstd::vector<int> buf(v.size())di awal —O(n)memori tambahan. Buffer ini diperlukan karena operasi merge perlu tempat untuk menampung hasil sementara sebelum disalin kembali kev.std::sort: mengimplementasikan introsort — in-placeO(1)memori tambahan (selain call stackO(log n)).- Output ketiganya identik — perbedaan hanya pada penggunaan memori, bukan hasil.
Mengapa Merge Sort Tidak In-Place #
Operasi merge pada merge sort membutuhkan dua stream input (kiri dan kanan) dan satu stream output. Tanpa buffer tambahan, hasil merge akan menimpa subarray kiri yang belum selesai diproses — korupsi data.
Beberapa varian in-place merge sort ada (misalnya block merge sort atau Katajainen), tetapi:
- Kompleksitas konstanta jauh lebih besar (~2-3× lebih lambat).
- Implementasi sangat kompleks — mudah bug.
- Jarang dipakai di praktik.
Mengapa Counting/Radix/Bucket Sort Tidak In-Place #
- Counting sort: array
countberukurankdan arrayoutputberukurann— totalO(n + k)memori tambahan. Operasi rekonstruksi membutuhkan output array terpisah karena elemen input ditimpa selama proses. - Radix sort: setiap pass menggunakan counting sort →
O(n + b)memori tambahan per pass. - Bucket sort: array
bucketsberisi totalnelemen tersebar —O(n + k)memori tambahan.
Trade-off: In-Place vs Stabilitas #
Tabel berikut menunjukkan trade-off antara in-place, stabilitas, dan kompleksitas:
| Algoritma | In-Place | Stabil | Worst Time |
|---|---|---|---|
| Bubble sort | Ya | Ya | O(n²) |
| Insertion sort | Ya | Ya | O(n²) |
| Selection sort | Ya | Tidak | O(n²) |
| Merge sort | Tidak | Ya | O(n log n) |
| Quick sort | Ya (O(log n)) | Tidak | O(n²) (avg O(n log n)) |
| Heap sort | Ya | Tidak | O(n log n) |
Memori di Lingkungan Terbatas #
Pada lingkungan dengan memori terbatas (embedded, IoT, real-time):
- Heap sort menjadi pilihan utama —
O(1)memori,O(n log n)worst case. - Quick sort dengan pivot random —
O(log n)call stack, namunO(n²)worst case. - Insertion sort untuk
nkecil —O(1)memori, tetapiO(n²)time.
Mitigasi Call Stack pada Quick Sort #
Quick sort standar menggunakan rekursi sehingga call stack O(log n) best case, O(n) worst case. Untuk memori terbatas:
- Tail call optimization — rekursi cabang yang lebih kecil lebih dulu, cabang besar di-tail-call. Stack depth dijamin
O(log n). - Iteratif dengan stack eksplisit — ganti rekursi dengan loop dan stack buatan sendiri. Stack dapat di-batasi ukurannya.
- Introsort — switch ke heap sort bila kedalaman melebihi
2·log₂ n.
Pengukuran Memori: Praktik #
Pada n = 1.000.000 elemen int (4 byte):
- Input:
4 MB(tidak terhindarkan). - Heap sort:
+ beberapa byte(variabel lokal). - Quick sort:
+ ~120 byte(call stack2·log₂ n ≈ 40frame). - Merge sort:
+ 4 MB(buffer duplikat). - Counting sort:
+ 4 MB + k byte(output + count).
Bila dataset berukuran n = 100 juta:
- Heap sort tetap berjalan dengan memori tambahan ~1 KB.
- Merge sort memerlukan 400 MB tambahan — mungkin tidak feasible.
Aplikasi In-Place Sorting #
- Embedded systems — memori terbatas, tidak boleh alokasi dinamis berlebihan.
- Real-time systems — alokasi dinamis dapat menyebabkan fragmentation dan pelanggaran deadline.
- Database buffer sort — mengurutkan page di tempat untuk efisiensi I/O.
- Kernel mode — alokasi dinamis terbatas, prefer in-place.
Variasi In-Place dengan Kompromi #
- In-place merge sort (block merge sort) —
O(1)memori, stabil,O(n log n)worst case. Konstanta ~2-3× lebih lambat dari merge sort standar. Implementasi sangat kompleks. - Quick sort dengan insertion cutoff — quick sort hingga sub-array ≤ 16, lalu insertion sort. Mengurangi call stack dan memanfaatkan adaptifitas insertion sort.
- Smooth sort — varian heap sort adaptif,
O(n)pada data hampir terurut,O(n log n)worst case,O(1)memori. Tidak stabil, konstanta besar.
Ringkasan #
- In-place =
O(1)memori tambahan (atauO(log n)untuk rekursi). - In-place: bubble, selection, insertion, quick, heap, shell sort.
- Tidak in-place: merge, counting, radix, bucket, Tim sort.
- Trade-off klasik: tidak ada algoritma comparison-based yang in-place + stabil +
O(n log n)worst case sekaligus (kecuali block merge sort dengan konstanta besar). - Heap sort menjadi pilihan utama untuk memori sangat terbatas.
std::sort(introsort) in-place danO(n log n)worst case — pilihan default untuk kode produksi.
Pada chapter berikutnya kita akan merangkum seluruh algoritma sorting dalam tabel perbandingan komprehensif, dengan rekomendasi kapan menggunakan masing-masing.