Warum sind rot-schwarze Bäume so beliebt?

46

Überall, wo ich hinschaue, werden Datenstrukturen mithilfe von rot-schwarzen Bäumen implementiert ( std::setin C ++, SortedDictionaryin C # usw.).

Nachdem ich gerade (a, b), Rot-Schwarz- und AVL-Bäume in meinem Algorithmus-Kurs behandelt habe, habe ich Folgendes herausgefunden (auch nachdem ich mich bei Professoren erkundigt, ein paar Bücher durchgesehen und ein bisschen gegoogelt habe):

  • AVL-Bäume haben eine geringere durchschnittliche Tiefe als rot-schwarze Bäume, und daher ist die Suche nach einem Wert im AVL-Baum durchgehend schneller.
  • Rot-schwarze Bäume führen weniger strukturelle Änderungen durch, um sich auszugleichen als AVL-Bäume, wodurch sie möglicherweise schneller zum Einfügen / Löschen verwendet werden können. Ich sage möglicherweise, weil dies von den Kosten der strukturellen Änderung des Baums abhängen würde, da dies sehr stark von der Laufzeit und der Implementierung abhängen wird (könnte in einer funktionalen Sprache auch völlig anders sein, wenn der Baum unveränderlich ist?)

Es gibt viele Online-Benchmarks, die AVL- und Rot-Schwarz-Bäume vergleichen. Was mich jedoch beeindruckt hat, ist, dass mein Professor im Grunde gesagt hat, dass Sie normalerweise eines von zwei Dingen tun würden:

  • Entweder interessiert Sie die Leistung nicht so sehr. In diesem Fall spielt der Unterschied zwischen AVL und Rot-Schwarz in den meisten Fällen keine Rolle.
  • Oder Sie legen großen Wert auf Leistung. In diesem Fall würden Sie sowohl AVL- als auch Rot-Schwarz-Bäume wegwerfen und sich für B-Bäume entscheiden, die optimiert werden können, um besser zu funktionieren (oder (a, b) -Bäume, I '). Ich werde alle in einen Korb legen.)

Der Grund dafür ist, dass ein B-Baum Daten kompakter im Speicher speichert (ein Knoten enthält viele Werte), und es wird viel weniger Cache-Fehlschläge geben. Sie können die Implementierung auch basierend auf dem Anwendungsfall optimieren und die Reihenfolge des B-Baums von der CPU-Cache-Größe usw. abhängig machen.

Das Problem ist, dass ich kaum eine Quelle finden kann, die die tatsächliche Verwendung verschiedener Implementierungen von Suchbäumen auf moderner Hardware analysiert. Ich habe viele Bücher über Algorithmen durchgesehen und nichts gefunden, das verschiedene Baumvarianten miteinander vergleichen könnte, außer zu zeigen, dass einer eine geringere durchschnittliche Tiefe hat als der andere (was nicht wirklich aussagt, wie sich der Baum verhält) in echten Programmen.)

Abgesehen davon, gibt es einen besonderen Grund, warum überall rot-schwarze Bäume verwendet werden, wenn auf der Grundlage der obigen Aussagen B-Bäume diese übertreffen sollten? (Als einziger Benchmark konnte ich auch http://lh3lh3.users.sourceforge.net/udb.shtml finden , aber es könnte nur eine Frage der spezifischen Implementierung sein). Oder ist es der Grund, warum jeder rot-schwarze Bäume verwendet, weil sie ziemlich einfach zu implementieren oder anders ausgedrückt, schwer zu implementieren sind?

Wie ändert sich dies auch, wenn man in den Bereich der funktionalen Sprachen wechselt? Es scheint, dass sowohl Clojure als auch Scala Hash-Array-zugeordnete Versuche verwenden , wobei Clojure einen Verzweigungsfaktor von 32 verwendet.

Jakub Arnold
quelle
8
Die meisten Artikel, in denen verschiedene Arten von Suchbäumen verglichen werden, sind weniger als ideale Experimente.
Raphael
1
Ich habe das selbst nie verstanden. Meiner Meinung nach sind AVL-Bäume einfacher zu implementieren als rot-schwarze Bäume (weniger Fälle beim Neuausgleich), und ich habe nie einen signifikanten Leistungsunterschied bemerkt.
Jordi Vermeulen
3
Eine relevante Diskussion unserer Freunde bei stackoverflow Warum ist std :: map als rot-schwarzer Baum implementiert? .
Hendrik Jan

Antworten:

10

Zitat aus der Antwort auf die Frage „ Traversale von der Wurzel in AVL-Bäumen und rot-schwarzen Bäumen

Für einige Arten von binären Suchbäumen, einschließlich rot-schwarzer Bäume, aber nicht AVL-Bäume, können die "Korrekturen" des Baums auf dem Weg nach unten ziemlich leicht vorhergesagt und während eines einzelnen Top-Down-Durchlaufs ausgeführt werden, wodurch der zweite Durchlauf unnötig wird. Solche Einfügealgorithmen werden in der Regel mit einer Schleife anstatt mit einer Rekursion implementiert und laufen in der Praxis häufig etwas schneller als ihre Gegenstücke mit zwei Durchläufen.

Damit ein RedBlack-Tree-Insert ohne Rekursion implementiert werden kann, ist die Rekursion auf einigen CPUs sehr teuer, wenn Sie den Funktionsaufruf-Cache überlaufen (z. B. SPARC aufgrund der Verwendung des Register-Fensters ).

(Ich habe gesehen, dass Software auf dem Sparc durch Entfernen eines Funktionsaufrufs über 10-mal so schnell ausgeführt wurde, was dazu führte, dass der häufig aufgerufene Codepfad für das Registerfenster zu tief war. Da Sie nicht wissen, wie tief das Registerfenster eingeschaltet sein wird Das System Ihres Kunden und Sie wissen nicht, wie weit Sie sich in der Anrufliste im "Hot-Code-Pfad" befinden, ohne eine Rekursion zu verwenden.

Es ist auch ein Vorteil, nicht zu riskieren, dass der Stapel ausgeht.

Ian Ringrose
quelle
Ein ausgeglichener Baum mit 2 ^ 32 Knoten würde jedoch nicht mehr als 32 Rekursionsebenen erfordern. Selbst wenn Ihr Stack-Frame 64 Byte groß ist, sind das nicht mehr als 2 KB Stapelspeicher. Kann das wirklich einen Unterschied machen? Ich würde es bezweifeln.
Björn Lindqvist
@ BjörnLindqvist, Beim SPARC-Prozessor in den 1990er-Jahren habe ich durch Ändern eines allgemeinen Codepfads von einer Stapeltiefe von 7 auf 6 oft mehr als das Zehnfache erreicht! Lesen Sie, wie es Dateien registriert hat ....
Ian Ringrose
9

Ich habe in letzter Zeit auch dieses Thema recherchiert. Hier sind meine Ergebnisse, aber bedenke, dass ich kein Experte für Datenstrukturen bin!

Es gibt einige Fälle, in denen Sie B-Bäume überhaupt nicht verwenden können.

Ein prominenter Fall stammt std::mapaus C ++ STL. Der Standard verlangt, dass insertvorhandene Iteratoren nicht ungültig werden

Keine Iteratoren oder Referenzen sind ungültig.

http://en.cppreference.com/w/cpp/container/map/insert

Dies schließt B-Tree als Implementierung aus, da sich das Einfügen um vorhandene Elemente bewegen würde.

Ein weiterer ähnlicher Anwendungsfall sind aufdringliche Datenstrukturen. Das heißt, anstatt Ihre Daten im Knoten des Baums zu speichern, speichern Sie Zeiger auf Kinder / Eltern in Ihrer Struktur:

// non intrusive
struct Node<T> {
    T value;
    Node<T> *left;
    Node<T> *right;
};
using WalrusList = Node<Walrus>;

// intrusive
struct Walrus {
    // Tree part
    Walrus *left;
    Walrus *right;

    // Object part
    int age;
    Food[4] stomach;
};

Sie können einen B-Baum einfach nicht aufdringlich machen, da es sich nicht um eine reine Zeigerdatenstruktur handelt.

Aufdringliche rot-schwarze Bäume werden beispielsweise in jemalloc verwendet , um freie Speicherblöcke zu verwalten. Dies ist auch eine beliebte Datenstruktur im Linux-Kernel.

Ich glaube auch, dass die "Single Pass Tail Recursive" -Implementierung nicht der Grund für die Beliebtheit von Rot-Schwarz-Bäumen als veränderbare Datenstruktur ist.

Logn

O(1)

O(1)

Die in opendatastructures beschriebene Variante verwendet übergeordnete Zeiger, einen rekursiven Down-Pass zum Einfügen und einen iterativen Loop-Up-Pass für Korrekturen. Die rekursiven Aufrufe befinden sich in einer Endposition und Compiler optimieren dies zu einer Schleife (ich habe dies in Rust überprüft).

O(1)

matklad
quelle
3

Nun, dies ist keine maßgebliche Antwort, aber wenn ich einen ausgeglichenen binären Suchbaum codieren muss, ist es ein rot-schwarzer Baum. Dafür gibt es einige Gründe:

1) Die durchschnittlichen Einfügungskosten sind für rot-schwarze Bäume konstant (wenn Sie nicht suchen müssen), während sie für AVL-Bäume logarithmisch sind. Darüber hinaus handelt es sich höchstens um eine komplizierte Umstrukturierung. Im schlimmsten Fall ist es immer noch O (log N), aber das sind nur einfache Umfärbungen.

2) Sie benötigen nur 1 Bit an zusätzlichen Informationen pro Knoten, und Sie können häufig einen Weg finden, um diese kostenlos zu erhalten.

3) Ich muss das nicht sehr oft machen, also muss ich jedes Mal, wenn ich es mache, herausfinden, wie ich es noch einmal machen soll. Die einfachen Regeln und die Korrespondenz mit 2-4 Bäumen lassen es jedes Mal einfach erscheinen , auch wenn sich der Code jedes Mal als kompliziert herausstellt . Ich hoffe immer noch, dass der Code eines Tages einfach wird.

4) Die Art und Weise, wie der rot-schwarze Baum den entsprechenden 2-4-Baumknoten aufteilt und den mittleren Schlüssel durch einfaches Umfärben in den übergeordneten 2-4-Knoten einfügt, ist sehr elegant. Ich liebe es einfach, es zu tun.

Matt Timmermans
quelle
0

Rot-Schwarz- oder AVL-Bäume haben einen Vorteil gegenüber B-Bäumen und dergleichen, wenn der Schlüssel lang ist oder aus einem anderen Grund das Verschieben eines Schlüssels teuer ist.

Ich habe std::setaus verschiedenen Gründen meine eigene Alternative zu einem Großprojekt geschaffen. Ich habe aus Leistungsgründen AVL gegenüber Rot-Schwarz gewählt (aber diese kleine Leistungsverbesserung war nicht die Rechtfertigung, meine eigene anstelle von std :: set zu rollen). Der "Schlüssel", der kompliziert und schwer zu bewegen war, war ein bedeutender Faktor. Sind (a, b) Bäume immer noch sinnvoll, wenn Sie eine andere Indirektionsebene vor den Schlüsseln benötigen? AVL- und rot-schwarze Bäume können ohne Verschieben von Schlüsseln umstrukturiert werden, sodass sie den Vorteil haben, wenn das Verschieben von Schlüsseln teuer ist.

JSF
quelle
Ironischerweise sind rot-schwarze Bäume "nur" ein Sonderfall von (a, b) -Bäumen, weshalb die Angelegenheit auf eine Optimierung der Parameter hinauszugehen scheint? (cc @ Gilles)
Raphael