Red-Black Tree (RBT) adalah self-balancing BST yang ditemukan Rudolf Bayer (1972). Berbeda dari AVL yang sangat ketat (|BF| ≤ 1), RBT menggunakan pewarnaan node merah/hitam untuk menjaga keseimbangan secara longgar — height maksimum 2× height minimum. Pendekatan ini lebih hemat rotasi: insert rata-rata hanya butuh 1 rotasi (vs AVL yang bisa banyak). Inilah alasan std::map, std::set, std::multimap, dan std::multiset di C++ STL memakai RBT, bukan AVL. Pada Chapter ini kita akan memahami empat properti RBT, mengimplementasikan insert dengan fixup (recoloring + rotasi), dan membandingkan kinerja RBT vs AVL. Karena implementasi RBT lengkap cukup panjang, fokus kita adalah memahami mekanisme fixup dan memverifikasi dengan eksperimen bahwa std::map memang self-balancing pada data terurut.
Empat Properti Red-Black Tree #
Sebuah BST adalah Red-Black Tree bila memenuhi:
- Setiap node berwarna merah atau hitam.
- Root selalu hitam.
- Setiap leaf (NIL) dianggap hitam. (NIL adalah sentinel untuk anak kosong.)
- Bila sebuah node merah, maka kedua anaknya hitam. (Tidak boleh ada dua merah berurutan.)
- Untuk setiap node, semua path dari node ke leaf mengandung jumlah node hitam yang sama — disebut black-height.
Konsekuensi: panjang path terpanjang (alternating merah-hitam) maksimal 2× panjang path terpendek (semua hitam). Ini menjamin height = O(log n).
Pada tree di atas, setiap path dari root ke NIL leaf mengandung 2 node hitam (black-height = 2 dari root, atau 1 dari root hitam saja bila root tidak dihitung). Tidak ada dua merah berurutan.
Mengapa RBT Lebih Efisien untuk Insert? #
Pada AVL, setiap insert yang melanggar balance langsung memicu rotasi. Pada RBT, recoloring (mengubah warna node) sering cukup untuk memulihkan properti tanpa rotasi. Recoloring O(1) dan tidak mengubah struktur — lebih murah daripada rotasi.
Kasus fixup RBT insert (dengan paman = uncle):
| Kasus | Kondisi | Aksi |
|---|---|---|
| 1 | Uncle merah | Recolor parent, uncle, grandparent; lanjut naik ke grandparent |
| 2 | Uncle hitam, node adalah right child dari parent (LL mirror) | Rotasi kiri pada parent → jadi kasus 3 |
| 3 | Uncle hitam, node adalah left child (LL) | Recolor parent, grandparent; rotasi kanan pada grandparent |
Rata-rata insert RBT: 1 rotasi (vs AVL: hingga O(log n) rotasi).
Insert dengan Fixup: Sketsa #
Implementasi RBT lengkap memerlukan banyak kode (~200 baris). Berikut sketsa insert dengan fixup untuk kasus LL (left-left, parent dan uncle kiri):
#include <iostream>
#include <map>
#include <vector>
#include <chrono>
#include <algorithm>
// Sketsa struct node RBT (untuk referensi; implementasi penuh dilakukan di kelas)
template <typename T>
struct RBTNode {
T data;
RBTNode* left;
RBTNode* right;
RBTNode* parent;
bool red; // true = merah, false = hitam
explicit RBTNode(const T& v)
: data(v), left(nullptr), right(nullptr), parent(nullptr), red(true) {}
};
// Demo: bandingkan std::map (RBT) vs insert terurut ke BST biasa
template <typename T>
struct BSTNode {
T data;
BSTNode* left;
BSTNode* right;
explicit BSTNode(const T& v) : data(v), left(nullptr), right(nullptr) {}
};
template <typename T>
BSTNode<T>* bst_insert(BSTNode<T>* node, const T& v) {
if (node == nullptr) return new BSTNode<T>(v);
if (v < node->data) node->left = bst_insert(node->left, v);
else if (v > node->data) node->right = bst_insert(node->right, v);
return node;
}
template <typename T>
bool bst_search(BSTNode<T>* node, const T& v) {
while (node) {
if (v == node->data) return true;
node = (v < node->data) ? node->left : node->right;
}
return false;
}
int main() {
const int N = 100'000;
// BST biasa dengan input terurut → skewed (depth = N)
BSTNode<int>* bst = nullptr;
auto t1 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < N; ++i) bst = bst_insert_iter(bst, i);
auto t2 = std::chrono::high_resolution_clock::now();
bool found_bst = bst_search(bst, N - 1);
auto t3 = std::chrono::high_resolution_clock::now();
// std::map (RBT) dengan input terurut → tetap balanced (depth ≈ log2 N)
std::map<int, int> m;
auto t4 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < N; ++i) m[i] = i;
auto t5 = std::chrono::high_resolution_clock::now();
auto it = m.find(N - 1);
auto t6 = std::chrono::high_resolution_clock::now();
bool found_map = (it != m.end());
auto ms_bst_build = std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1).count();
auto us_bst_search = std::chrono::duration_cast<std::chrono::microseconds>(t3 - t2).count();
auto ms_map_build = std::chrono::duration_cast<std::chrono::milliseconds>(t5 - t4).count();
auto us_map_search = std::chrono::duration_cast<std::chrono::microseconds>(t6 - t5).count();
std::cout << "Input terurut 0.." << N - 1 << ":\n";
std::cout << " BST biasa: build=" << ms_bst_build << "ms, search=" << us_bst_search << "us, found=" << found_bst << "\n";
std::cout << " std::map : build=" << ms_map_build << "ms, search=" << us_map_search << "us, found=" << found_map << "\n";
return 0;
}Output yang Dihasilkan:
Input terurut 0..99999:
BST biasa: build=9814ms, search=355us, found=1
std::map : build=10ms, search=0us, found=1- BST biasa pada input terurut: build 9814ms (skewed tree, setiap insert traverse
inode). SearchN-1= 355us karena harus traverse 100000 node linear. std::map(RBT) pada input terurut: build 10ms (~1000× lebih cepat). SearchN-1< 1us (terlalu cepat untuk diukur presisi) karena height tree ~17 (log₂ 100000 ≈ 17).- Selisih search: 355us vs <1us — setidaknya 350× lebih cepat. Pada N lebih besar, selisih ini tumbuh linear terhadap N.
- Build
std::map~1000× lebih cepat: meskipun ada overhead rebalancing, BST biasa pada input terurut membentuk linked list — insert ke-i traverseinode, total O(n²) vs O(n log n).
Perbandingan RBT vs AVL #
| Aspek | Red-Black Tree | AVL Tree |
|---|---|---|
| Aturan balancing | Warna + black-height | |
| Height praktis | ≤ 2 log n | ≤ 1.44 log n |
| Search lebih cepat | Sedikit lebih lambat (height lebih besar) | Lebih cepat |
| Insert/delete lebih cepat | Lebih cepat (rata-rata 1 rotasi) | Lebih lambat (bisa O(log n) rotasi) |
| Cocok untuk | Write-heavy, general purpose | Read-heavy, database index |
| Dipakai di | std::map, std::set (C++ STL) | Kernel Linux (timers), database (read-heavy) |
Kapan Pakai AVL vs RBT vs Hash Table #
| Skenario | Pilihan | Alasan |
|---|---|---|
| Banyak search, sedikit insert | AVL | Height lebih kecil, search O(log n) terbaik |
| Banyak insert/delete, sedikit range query | Hash table | O(1) avg, tidak ada overhead balancing |
Range query [lo, hi] | RBT atau AVL | Traversal in-order pada sorted keys |
| Sorted iteration | std::map (RBT) | Iterasi in-order terurut |
| General purpose, unknown workload | std::map | STL default, well-tested |
Ringkasan #
- Red-Black Tree: BST dengan 5 properti (warna, root hitam, NIL hitam, no dua-merah, black-height konsisten).
- Pendekatan balancing longgar: height ≤ 2 log n (vs AVL 1.44 log n), namun rotasi lebih sedikit.
- Insert fixup memakai recoloring + rotasi. Recoloring O(1) lebih murah daripada rotasi — inilah keunggulan RBT untuk write-heavy.
std::map,std::set,std::multimap,std::multisetSTL memakai RBT — operasi O(log n) terlepas urutan input.- RBT vs AVL: RBT untuk write-heavy dan general purpose (STL default), AVL untuk read-heavy (database, kernel timers). Hash table untuk lookup-only tanpa range query.