Pourquoi les arbres rouge-noir sont-ils si populaires?

46

Il semble que partout où je regarde, les structures de données sont mises en œuvre en utilisant des arbres rouges-noirs ( std::seten C ++, SortedDictionaryen C #, etc.)

Après avoir couvert (a, b), les arbres rouge-noir et AVL dans mon cours d’algorithme, voici ce que j’ai dégagé (aussi en interrogeant des professeurs, en parcourant quelques livres et en recherchant un peu sur Google):

  • La profondeur moyenne des arbres AVL est inférieure à celle des arbres rouge-noir. Par conséquent, la recherche d’une valeur dans l’arbre AVL est toujours plus rapide.
  • Les arbres rouge-noir apportent moins de changements structurels pour s'équilibrer que les arbres AVL, ce qui pourrait les rendre potentiellement plus rapides pour l'insertion / suppression. Je dis potentiellement, car cela dépendrait du coût de la modification structurelle de l’arbre, car cela dépendra beaucoup du temps d’exécution et de l’implémentation (pourrait aussi être complètement différent dans un langage fonctionnel lorsque l’arbre est immuable?)

Il existe de nombreux points de repère en ligne qui comparent les arbres AVL et les arbres rouge-noir, mais ce qui m'a frappé est que, selon mon professeur, vous feriez généralement l'une des deux choses suivantes:

  • Soit vous ne vous souciez pas vraiment de la performance. Dans ce cas, la différence de 10 à 20% entre AVL et Rouge-Noir dans la plupart des cas n’aura aucune importance.
  • Ou bien vous vous souciez vraiment de la performance, dans laquelle vous abandonneriez à la fois les arbres AVL et rouge-noir, et utilisiez les arbres B, qui peuvent être modifiés pour fonctionner beaucoup mieux (ou (a, b)-arbres, je ' Je vais tous les mettre dans le même panier.)

La raison en est qu'un B-tree stocke les données de manière plus compacte dans la mémoire (un nœud contient plusieurs valeurs), le nombre d'erreurs dans le cache sera beaucoup moins important. Vous pouvez également modifier l'implémentation en fonction du cas d'utilisation et rendre l'ordre de l'arborescence B dépend de la taille du cache de la CPU, etc.

Le problème est que je ne trouve pratiquement aucune source capable d'analyser l'utilisation réelle de différentes implémentations d'arbres de recherche sur du matériel réel et moderne. J'ai parcouru de nombreux ouvrages sur les algorithmes et rien trouvé qui puisse comparer différentes variantes d'arborescence, à part montrer que l'une a une profondeur moyenne inférieure à l'autre (ce qui ne dit pas vraiment comment l'arbre se comportera dans de vrais programmes.)

Cela dit, y a-t-il une raison particulière pour laquelle les arbres rouge-noir sont utilisés partout, alors que sur la base de ce qui est dit ci-dessus, les arbres B devraient les surpasser? (en tant que seul point de référence que j'ai pu trouver, montre également les spectacles http://lh3lh3.users.sourceforge.net/udb.shtml , mais il pourrait s'agir simplement d'une mise en œuvre spécifique). Ou est-ce la raison pour laquelle tout le monde utilise des arbres rouge-noir parce qu'ils sont plutôt faciles à mettre en œuvre, ou pour le dire autrement, difficiles à mettre en œuvre de manière médiocre?

Aussi, comment cela change-t-il lorsque l'on passe au royaume des langages fonctionnels? Il semble que Clojure et Scala utilisent des essais mappés sur des tableaux de hachage , où Clojure utilise un facteur de branchement de 32.

Jakub Arnold
la source
8
Pour ajouter à votre douleur, la plupart des articles qui comparent différents types d'arbres de recherche effectuent… des expériences moins qu'idéales.
Raphaël
1
Je n'ai jamais compris cela moi-même. À mon avis, les arbres AVL sont plus faciles à implémenter que les arbres rouge-noir (moins de cas lors du rééquilibrage) et je n'ai jamais remarqué de différence de performances significative.
Jordi Vermeulen
3
Une discussion pertinente de nos amis chez stackoverflow Pourquoi std :: map est-il implémenté comme un arbre rouge-noir? .
Hendrik Jan

Réponses:

10

Pour citer la réponse à la question « Traversées de la racine dans des arbres AVL et des arbres noirs et rouges »

Pour certains types d'arbres de recherche binaires, y compris les arbres rouge-noir mais pas les arbres AVL, les "corrections" apportées à l'arbre peuvent être assez facilement prédites en descendant et exécutées au cours d'une seule passe descendante, rendant inutile la deuxième passe. De tels algorithmes d'insertion sont généralement implémentés avec une boucle plutôt que la récursion, et s'exécutent souvent un peu plus rapidement en pratique que leurs homologues à deux passes.

Ainsi, une insertion d'arborescence RedBlack peut être implémentée sans récursion. Sur certains processeurs, la récursivité est très coûteuse si vous surchargez le cache des appels de fonctions (par exemple, SPARC en raison de l'utilisation de la fenêtre Enregistrer ).

(J'ai vu un logiciel fonctionner 10 fois plus vite sur le Sparc en supprimant un appel de fonction, ce qui rendait un chemin de code souvent appelé trop profond pour la fenêtre de registre. Comme vous ne savez pas à quelle profondeur la fenêtre de registre sera active le système de votre client, et vous ne savez pas à quelle distance de la pile d’appels vous vous trouvez dans le "chemin du code chaud", le fait de ne pas utiliser la récursivité est plus prévisible.)

De plus, ne pas risquer de tomber à court de pile est un avantage.

Ian Ringrose
la source
Mais un arbre équilibré avec 2 ^ 32 nœuds ne nécessiterait pas plus de 32 niveaux de récursivité. Même si votre cadre de pile est de 64 octets, cela ne représente pas plus de 2 Ko d'espace de pile. Cela peut-il vraiment faire une différence? J'en douterais.
Björn Lindqvist le
@ BjörnLindqvist, Sur le processeur SPARC dans les années 1990, j'ai souvent obtenu une vitesse plus de 10 fois supérieure en modifiant un chemin de code commun d'une profondeur de pile de 7 à 6! Lisez la procédure d'enregistrement des fichiers ....
Ian Ringrose
9

J'ai également fait des recherches sur ce sujet récemment, alors voici mes conclusions, mais n'oubliez pas que je ne suis pas un expert en structures de données!

Dans certains cas, vous ne pouvez pas utiliser d'arbres B du tout.

Un cas important est celui std::mapde C ++ STL. La norme exige que insertne pas invalider les itérateurs existants

Aucun itérateur ou référence n'est invalidé.

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

Cela exclut B-tree en tant qu'implémentation car l'insertion déplacerait les éléments existants.

Un autre cas d'utilisation similaire est celui des infrastructures de données intrusives. C'est-à-dire qu'au lieu de stocker vos données dans le nœud de l'arborescence, vous stockez les pointeurs vers les enfants / parents dans votre structure:

// 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;
};

Vous ne pouvez tout simplement pas créer une intrusion dans le B-tree, car ce n'est pas une structure de données avec un pointeur uniquement.

Des arbres rouge-noirs intrusifs sont utilisés, par exemple, dans jemalloc pour gérer des blocs de mémoire libres. C'est également une structure de données populaire dans le noyau Linux.

Je pense également que l'implémentation "récursive en un seul passage" n'est pas la raison de la popularité de l'arbre noir rouge en tant que structure de données modifiable .

Tout d’abord, la profondeur de la pile n’a aucune importance ici, car (étant donné height), vous manqueriez de mémoire principale avant de manquer d’espace de pile. Jemalloc est satisfait de la préallocation de la profondeur de cas la plus défavorable sur la pile.logn

Il existe un certain nombre de types d'implémentation d'arborescence rouge-noir. Robert Sedgewick a laissé des arbres noirs et rouges penchés ( attention, il existe d’autres variantes nommées également "penchées à gauche", mais qui utilisent un algorithme différent). Cette variante permet certes d’effectuer des rotations en descendant dans l’arbre, mais elle manque de la propriété importante de nombre de corrections amorties, ce qui la ralentit ( telle que mesurée par l’auteur de jemalloc ). Ou, comme opendatastrutures metO(1)

La variante des arbres rouge-noir d'Andersson, la variante des arbres rouge-noir de Sedgewick et les arbres AVL sont tous plus simples à mettre en œuvre que la structure RedBlackTree définie ici. Malheureusement, aucun d'entre eux ne peut garantir que le temps amorti consacré au rééquilibrage est égal à par mise à jour.O(1)

La variante décrite dans opendatastructures utilise des pointeurs parents, une passe descendante récursive pour l’insertion et une passe en boucle itérative pour les corrections. Les appels récursifs sont dans une position de queue et les compilateurs l'optimisent en boucle (j'ai vérifié cela dans Rust).

En d'autres termes, vous pouvez obtenir une implémentation en boucle de mémoire constante d'un arbre de recherche mutable sans magie rouge-noir si vous utilisez des pointeurs parents. Cela fonctionne aussi pour les arbres B. Vous avez besoin de magie pour la variante immuable récursive en un seul passage, et elle corrigera de toute façon la correction .O(1)

matklad
la source
3

Eh bien, ce n'est pas une réponse qui fait autorité, mais chaque fois que je dois coder un arbre de recherche binaire équilibré, c'est un arbre rouge-noir. Il y a plusieurs raisons à cela:

1) Le coût moyen d'insertion est constant pour les arbres rouge-noir (si vous n'avez pas à chercher), alors que c'est logarithmique pour les arbres AVL. En outre, cela implique tout au plus une restructuration compliquée. C’est toujours O (log N) dans le pire des cas, mais ce n’est que de simples changements de couleur.

2) Ils n’exigent que 1 bit d’information supplémentaire par nœud, et vous pouvez souvent trouver le moyen de l’obtenir gratuitement.

3) Je n'ai pas à le faire très souvent, alors chaque fois que je le fais, je dois trouver le moyen de le faire à nouveau. Les règles simples et la correspondance avec 2-4 arbres le rendent facile à chaque fois , même si le code s'avère compliqué à chaque fois . J'espère toujours qu'un jour le code deviendra simple.

4) La manière dont l'arborescence rouge-noire divise le nœud d'arbre 2-4 correspondant et insère la clé centrale dans le nœud parent 2-4 par simple recoloration est extrêmement élégante. J'aime juste le faire.

Matt Timmermans
la source
0

Les arbres rouge-noir ou AVL ont un avantage sur les arbres B et autres lorsque la clé est longue ou pour une autre raison que le déplacement d'une clé coûte cher.

J'ai créé ma propre alternative à std::setun projet majeur, pour un certain nombre de raisons de performance. J'ai choisi AVL au lieu de rouge-noir pour des raisons de performances (mais cette petite amélioration des performances n'était pas une justification pour lancer la mienne au lieu de std :: set). La "clé" étant compliquée et difficile à déplacer était un facteur important. Les arbres (a, b) sont-ils toujours utiles si vous avez besoin d’un autre niveau d’indirection devant les touches? Les arbres AVL et rouge-noir peuvent être restructurés sans déplacer les clés, ils ont donc cet avantage lorsque le déplacement des clés est coûteux.

JSF
la source
Ironiquement, les arbres rouge-noir ne sont "que" des cas particuliers d'arbres (a, b), de sorte que la question semble se résumer à une modification des paramètres? (cc @Gilles)
Raphaël