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:

  1. Setiap node berwarna merah atau hitam.
  2. Root selalu hitam.
  3. Setiap leaf (NIL) dianggap hitam. (NIL adalah sentinel untuk anak kosong.)
  4. Bila sebuah node merah, maka kedua anaknya hitam. (Tidak boleh ada dua merah berurutan.)
  5. 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):

KasusKondisiAksi
1Uncle merahRecolor parent, uncle, grandparent; lanjut naik ke grandparent
2Uncle hitam, node adalah right child dari parent (LL mirror)Rotasi kiri pada parent → jadi kasus 3
3Uncle 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):

cpp
#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:

text
Input terurut 0..99999:
  BST biasa: build=9814ms, search=355us, found=1
  std::map : build=10ms, search=0us, found=1
  1. BST biasa pada input terurut: build 9814ms (skewed tree, setiap insert traverse i node). Search N-1 = 355us karena harus traverse 100000 node linear.
  2. std::map (RBT) pada input terurut: build 10ms (~1000× lebih cepat). Search N-1 < 1us (terlalu cepat untuk diukur presisi) karena height tree ~17 (log₂ 100000 ≈ 17).
  3. Selisih search: 355us vs <1us — setidaknya 350× lebih cepat. Pada N lebih besar, selisih ini tumbuh linear terhadap N.
  4. Build std::map ~1000× lebih cepat: meskipun ada overhead rebalancing, BST biasa pada input terurut membentuk linked list — insert ke-i traverse i node, total O(n²) vs O(n log n).

Perbandingan RBT vs AVL #

AspekRed-Black TreeAVL Tree
Aturan balancingWarna + black-height
Height praktis≤ 2 log n≤ 1.44 log n
Search lebih cepatSedikit lebih lambat (height lebih besar)Lebih cepat
Insert/delete lebih cepatLebih cepat (rata-rata 1 rotasi)Lebih lambat (bisa O(log n) rotasi)
Cocok untukWrite-heavy, general purposeRead-heavy, database index
Dipakai distd::map, std::set (C++ STL)Kernel Linux (timers), database (read-heavy)

Kapan Pakai AVL vs RBT vs Hash Table #

SkenarioPilihanAlasan
Banyak search, sedikit insertAVLHeight lebih kecil, search O(log n) terbaik
Banyak insert/delete, sedikit range queryHash tableO(1) avg, tidak ada overhead balancing
Range query [lo, hi]RBT atau AVLTraversal in-order pada sorted keys
Sorted iterationstd::map (RBT)Iterasi in-order terurut
General purpose, unknown workloadstd::mapSTL 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::multiset STL 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.