Assignment Chef icon Assignment Chef
All German tutorials

Programming lesson

AVL-Bäume in C++: Selbstbalancierende Suchbäume für effiziente Datenverwaltung

Lerne, wie du einen AVL-Baum in C++ implementierst – inklusive Einfügen, Löschen und Traversierung. Perfekt für Studierende der Informatik (CS251).

AVL Baum C++ AVL Tree Implementierung C++ AVL Baum Tutorial selbstbalancierender Suchbaum CS251 AVL Tree AVL Tree insert remove Binary Search Tree C++ Baumrotationen C++ Inorder Postorder Traversierung Datenstrukturen und Algorithmen C++ Programmierung lernen AVL Baum Höhe Balance effiziente Datenverwaltung C++ Studium Informatik AVL AVL Tree Beispielcode C++ OOP AVL Klasse

Einführung: Warum AVL-Bäume?

Stell dir vor, du entwickelst eine App wie TikTok, die täglich Millionen neuer Videos verarbeitet. Die Trending-Seite muss in Echtzeit aktualisiert werden – dafür brauchst du eine Datenstruktur, die Einfügungen, Löschungen und Suchanfragen in O(log n) erledigt. Hier kommen AVL-Bäume ins Spiel: selbstbalancierende binäre Suchbäume, die nach jeder Änderung ihre Höhe ausgleichen.

In diesem Tutorial lernst du, wie du einen AVL-Baum in C++ implementierst – genau wie in der CS251-Aufgabe gefordert. Wir behandeln Konstruktor, Destruktor, insert, remove und die Traversierungen printInorder und printPostorder. Keine Sorge, wir lösen die Aufgabe nicht für dich, aber du bekommst das Rüstzeug, um sie selbst zu meistern.

Grundlagen: Binäre Suchbäume und ihre Schwächen

Ein binärer Suchbaum (BST) speichert Schlüssel so, dass für jeden Knoten gilt: Alle Schlüssel im linken Teilbaum sind kleiner, alle im rechten größer. Das ermöglicht schnelles Suchen – im Idealfall O(log n). Doch ohne Balancierung kann ein BST entarten: Fügst du sortierte Zahlen ein, wird er zu einer linearen Liste – Suchzeit wird zu O(n). Genau das verhindern AVL-Bäume.

AVL-Bäume: Definition und Balance-Faktor

Ein AVL-Baum ist ein BST, bei dem für jeden Knoten die Höhen der beiden Kindbäume um höchstens 1 differieren. Der Balance-Faktor ist definiert als:

Balance-Faktor = Höhe(linker Teilbaum) - Höhe(rechter Teilbaum)

Erlaubte Werte sind -1, 0 und +1. Wird dieser Bereich verletzt, muss der Baum durch Rotationen neu balanciert werden.

Implementierung in C++: Die Klasse AVLTree

Wir erstellen zwei Dateien: AVLTree.h (Header) und AVLTree.cpp (Implementierung). Die Klasse speichert positive Integer als Schlüssel und Element. Jeder Knoten enthält Schlüssel, Höhe und Zeiger auf linkes/rechtes Kind.

Knotenstruktur

struct Node {
    int key;
    int height;
    Node* left;
    Node* right;
    Node(int k) : key(k), height(1), left(nullptr), right(nullptr) {}
};

Konstruktor und Destruktor

Der Konstruktor initialisiert die Wurzel auf nullptr. Der Destruktor löscht rekursiv alle Knoten, um Speicherlecks zu vermeiden – ein Muss in C++.

AVLTree::AVLTree() : root(nullptr) {}

AVLTree::~AVLTree() {
    destroyTree(root);
}

void AVLTree::destroyTree(Node* node) {
    if (node) {
        destroyTree(node->left);
        destroyTree(node->right);
        delete node;
    }
}

Einfügen (insert) und Balancierung

Das Einfügen erfolgt wie in einem BST, danach wird die Höhe aktualisiert und der Balance-Faktor geprüft. Bei Unbalance kommen vier Fälle in Frage: Links-Links, Links-Rechts, Rechts-Rechts und Rechts-Links. Diese werden durch Rotationen behoben.

Rotationen

Eine Rechtsrotation (für LL-Fall) und Linksrotation (für RR-Fall) sind die Basis. Für LR und RL werden zwei Rotationen kombiniert.

Node* rotateRight(Node* y) {
    Node* x = y->left;
    Node* T2 = x->right;
    x->right = y;
    y->left = T2;
    y->height = max(height(y->left), height(y->right)) + 1;
    x->height = max(height(x->left), height(x->right)) + 1;
    return x;
}

Node* rotateLeft(Node* x) {
    Node* y = x->right;
    Node* T2 = y->left;
    y->left = x;
    x->right = T2;
    x->height = max(height(x->left), height(x->right)) + 1;
    y->height = max(height(y->left), height(y->right)) + 1;
    return y;
}

Insert-Methode

Node* insert(Node* node, int key) {
    if (!node) return new Node(key);
    if (key < node->key)
        node->left = insert(node->left, key);
    else if (key > node->key)
        node->right = insert(node->right, key);
    else
        return node; // keine Duplikate

    node->height = 1 + max(height(node->left), height(node->right));
    int balance = getBalance(node);

    // Links-Links
    if (balance > 1 && key < node->left->key)
        return rotateRight(node);
    // Rechts-Rechts
    if (balance < -1 && key > node->right->key)
        return rotateLeft(node);
    // Links-Rechts
    if (balance > 1 && key > node->left->key) {
        node->left = rotateLeft(node->left);
        return rotateRight(node);
    }
    // Rechts-Links
    if (balance < -1 && key < node->right->key) {
        node->right = rotateRight(node->right);
        return rotateLeft(node);
    }
    return node;
}

Löschen (remove) mit Balancierung

Das Löschen in einem AVL-Baum ist komplexer. Zuerst wird der Knoten wie in einem BST gelöscht, dann wird der Baum auf dem Weg nach oben balanciert. Drei Fälle: Knoten ist Blatt, hat ein Kind oder zwei Kinder. Bei zwei Kindern ersetzen wir durch den Inorder-Nachfolger (kleinster Knoten im rechten Teilbaum).

Node* minValueNode(Node* node) {
    Node* current = node;
    while (current->left)
        current = current->left;
    return current;
}

Node* remove(Node* root, int key) {
    if (!root) return root;
    if (key < root->key)
        root->left = remove(root->left, key);
    else if (key > root->key)
        root->right = remove(root->right, key);
    else {
        if (!root->left || !root->right) {
            Node* temp = root->left ? root->left : root->right;
            if (!temp) {
                temp = root;
                root = nullptr;
            } else
                *root = *temp;
            delete temp;
        } else {
            Node* temp = minValueNode(root->right);
            root->key = temp->key;
            root->right = remove(root->right, temp->key);
        }
    }
    if (!root) return root;
    root->height = 1 + max(height(root->left), height(root->right));
    int balance = getBalance(root);
    // Balancierung analog zu insert
    if (balance > 1 && getBalance(root->left) >= 0)
        return rotateRight(root);
    if (balance > 1 && getBalance(root->left) < 0) {
        root->left = rotateLeft(root->left);
        return rotateRight(root);
    }
    if (balance < -1 && getBalance(root->right) <= 0)
        return rotateLeft(root);
    if (balance < -1 && getBalance(root->right) > 0) {
        root->right = rotateRight(root->right);
        return rotateLeft(root);
    }
    return root;
}

Traversierungen: Inorder und Postorder

Die Inorder-Traversierung gibt die Schlüssel in aufsteigender Reihenfolge aus – nützlich, um sortierte Daten zu erhalten. Die Postorder-Traversierung besucht zuerst die Kinder, dann die Wurzel – praktisch zum Löschen des Baums. Gib für jeden Knoten (key, height) aus.

void printInorder(Node* node) {
    if (node) {
        printInorder(node->left);
        cout << "(" << node->key << ", " << node->height << ") ";
        printInorder(node->right);
    }
}

void printPostorder(Node* node) {
    if (node) {
        printPostorder(node->left);
        printPostorder(node->right);
        cout << "(" << node->key << ", " << node->height << ") ";
    }
}

Beispiel aus der Praxis: Song-Playlist

Stell dir vor, du verwaltest eine Playlist in einer Musik-App wie Spotify. Jeder Song hat eine ID (positiver Integer). Neue Songs werden eingefügt, alte gelöscht. Mit einem AVL-Baum bleiben Einfügen und Löschen auch bei Millionen Songs schnell – anders als bei einer verketteten Liste, die bei jeder Sortierung neu geordnet werden müsste.

Häufige Fehler und Tipps

  • Höhe nicht aktualisiert: Vergiss nicht, nach jeder Rotation die Höhe neu zu berechnen.
  • Balance-Faktor falsch berechnet: Nutze eine Hilfsfunktion height(Node*), die bei nullptr 0 zurückgibt.
  • Speicherlecks: Implementiere den Destruktor rekursiv, um alle Knoten zu löschen.
  • Duplikate: Die Aufgabenstellung sagt, dass bei bereits vorhandenem Schlüssel nichts passiert – also einfach return.
  • Teste mit verschiedenen Sequenzen: Probiere aufsteigende, absteigende und zufällige Reihenfolgen, um die Balancierung zu prüfen.

Fazit

AVL-Bäume sind eine elegante Lösung, um binäre Suchbäume balanciert zu halten. Mit den gezeigten Methoden – insert, remove, printInorder und printPostorder – bist du bestens gerüstet, um die CS251-Aufgabe zu lösen. Denk daran: Übung macht den Meister! Implementiere die Methoden Schritt für Schritt und teste mit kleinen Beispielen. Viel Erfolg!