Pourquoi std :: map est-il implémenté comme un arbre rouge-noir?

193

Pourquoi est std::mapimplémenté comme un arbre rouge-noir ?

Il existe plusieurs arbres de recherche binaires équilibrés (BST). Quels ont été les compromis de conception dans le choix d'un arbre rouge-noir?

Denis Gorodetskiy
la source
26
Bien que toutes les implémentations que j'ai vues utilisent un arbre RB, notez que cela dépend toujours de l'implémentation.
Thomas
3
@Thomas. Cela dépend de l'implémentation, alors pourquoi est-ce que toutes les implémentations utilisent des arbres RB?
Denis Gorodetskiy
1
J'aimerais vraiment savoir si un implémenteur STL a pensé à utiliser une liste de saut.
Matthieu M.
2
La carte et l'ensemble C ++ sont en fait une carte et un ensemble ordonnés. Ils ne sont pas implémentés à l'aide de fonctions de hachage. Chaque requête prendrait O(logn)et non O(1), mais les valeurs seront toujours triées. À partir de C ++ 11 (je pense), il y a unordered_mapet unordered_set, qui sont implémentés à l'aide de fonctions de hachage et bien qu'ils ne soient pas triés, la plupart des requêtes et des opérations sont possibles dans O(1)(en moyenne)
SomethingSomething
@Thomas c'est vrai, mais pas si intéressant dans la pratique. La norme offre des garanties de complexité avec un algorithme spécifique ou un ensemble d'algorithmes à l'esprit.
Justin Meiners

Réponses:

125

Les deux algorithmes d'arbre à équilibrage automatique les plus courants sont probablement les arbres rouge-noir et les arbres AVL . Pour équilibrer l'arbre après une insertion / mise à jour, les deux algorithmes utilisent la notion de rotations où les nœuds de l'arbre sont tournés pour effectuer le rééquilibrage.

Alors que dans les deux algorithmes, les opérations d'insertion / suppression sont O (log n), dans le cas de la rotation de rééquilibrage de l'arbre rouge-noir est une opération O (1) tandis qu'avec AVL, il s'agit d'une opération O (log n) , ce qui rend la Arbre rouge-noir plus efficace dans cet aspect de la phase de rééquilibrage et l'une des raisons possibles pour lesquelles il est plus couramment utilisé.

Les arbres rouge-noir sont utilisés dans la plupart des bibliothèques de collections, y compris les offres de Java et de Microsoft .NET Framework.

Chris Taylor
la source
54
vous donnez l'impression que les arbres rouge-noir peuvent faire des modifications d'arbre en temps O (1), ce qui n'est pas vrai. les modifications d'arbre sont O (log n) pour les arbres rouge-noir et AVL. cela fait que la partie d'équilibrage de la modification de l'arborescence soit O (1) ou O (log n) car l'opération principale est déjà O (log n). même après tout le travail légèrement supplémentaire effectué par les arbres AVL, il en résulte un arbre plus équilibré, ce qui conduit à des recherches légèrement plus rapides. il s'agit donc d'un compromis parfaitement valable et ne rend pas les arbres AVL inférieurs aux arbres rouge-noir.
nécromancien
35
Vous devez regarder au-delà de la complexité jusqu'au temps d'exécution réel pour voir une différence - les arbres AVL ont généralement un temps d'exécution total inférieur lorsqu'il y a beaucoup plus de recherches que d'insertions / suppressions. Les arborescences RB ont un temps d'exécution total inférieur lorsqu'il y a beaucoup plus d'insertions / suppressions. La proportion exacte à laquelle la rupture se produit dépend bien sûr de nombreux détails de mise en œuvre, du matériel et de l'utilisation exacte, mais comme les auteurs de bibliothèque doivent prendre en charge un large éventail de modèles d'utilisation, ils doivent faire une supposition éclairée. AVL est également légèrement plus difficile à implémenter, vous pouvez donc souhaiter un avantage éprouvé pour l'utiliser.
Steve Jessop
6
L'arbre RB n'est pas une "implémentation par défaut". Chaque implémenteur choisit une implémentation. À notre connaissance, ils ont tous choisi des arbres RB, donc c'est probablement pour des performances ou pour une facilité de mise en œuvre / maintenance. Comme je l'ai dit, le point d'arrêt pour les performances peut ne pas impliquer qu'ils pensent qu'il y a plus d'insertions / suppressions que de recherches, juste que le rapport entre les deux est supérieur au niveau où ils pensent que RB bat probablement AVL.
Steve Jessop
9
@Denis: malheureusement, la seule façon d'obtenir des chiffres est de dresser une liste des std::mapimplémentations, de retrouver les développeurs et de leur demander quels critères ils ont utilisés pour prendre la décision, cela reste donc de la spéculation.
Steve Jessop
4
Il manque à tout cela le coût, par nœud, pour stocker les informations auxiliaires nécessaires pour prendre des décisions d'équilibre. Les arbres rouge-noir nécessitent 1 bit pour représenter la couleur. Les arbres AVL nécessitent au moins 2 bits (pour représenter -1, 0 ou 1).
SJHowe
46

Cela dépend vraiment de l'utilisation. L'arbre AVL a généralement plus de rotations de rééquilibrage. Donc, si votre application n'a pas trop d'opérations d'insertion et de suppression, mais pèse lourdement sur la recherche, l'arborescence AVL est probablement un bon choix.

std::map utilise l'arbre rouge-noir car il obtient un compromis raisonnable entre la vitesse d'insertion / suppression de nœuds et la recherche.

webbertiger
la source
1
Êtes-vous sûr de cela??? Personnellement, je pense que l'arbre rouge-noir est soit plus complexe, jamais plus simple. La seule chose, c'est dans l'arbre Rd-Black, le rééquilibrage se produit moins souvent que AVL.
Eric Ouellet
1
@Eric Théoriquement, à la fois l'arbre R / B et l'arbre AVL ont la complexité O (log n)) pour l'insertion et la suppression. Mais une grande partie du coût de fonctionnement est la rotation, qui est différente entre ces deux arbres. Veuillez vous reporter à discuter.fogcreek.com/joelonsoftware/… Quote: "équilibrer un arbre AVL peut nécessiter des rotations O (log n), tandis qu'un arbre rouge noir prendra au plus deux rotations pour le mettre en équilibre (bien qu'il doive examiner les nœuds O (log n) pour décider où les rotations sont nécessaires). " Modifié mes commentaires en conséquence.
webbertiger
26

Les arbres AVL ont une hauteur maximale de 1,44 logn, tandis que les arbres RB ont un maximum de 2 logn. L'insertion d'un élément dans un AVL peut impliquer un rééquilibrage à un moment de l'arborescence. Le rééquilibrage termine l'insertion. Après l'insertion d'une nouvelle feuille, la mise à jour des ancêtres de cette feuille doit être effectuée jusqu'à la racine, ou jusqu'à un point où les deux sous-arbres sont d'égale profondeur. La probabilité d'avoir à mettre à jour k nœuds est de 1/3 ^ k. Le rééquilibrage est O (1). La suppression d'un élément peut impliquer plus d'un rééquilibrage (jusqu'à la moitié de la profondeur de l'arbre).

Les arbres RB sont des arbres B d'ordre 4 représentés comme des arbres de recherche binaires. Un 4 nœuds dans l'arbre B donne deux niveaux dans le BST équivalent. Dans le pire des cas, tous les nœuds de l'arbre sont à 2 nœuds, avec une seule chaîne de 3 nœuds jusqu'à une feuille. Cette feuille sera à une distance de 2logn de la racine.

En descendant de la racine au point d'insertion, il faut changer 4 nœuds en 2 nœuds, pour s'assurer que toute insertion ne saturera pas une feuille. En revenant de l'insertion, tous ces nœuds doivent être analysés pour s'assurer qu'ils représentent correctement 4 nœuds. Cela peut également se faire en descendant dans l'arbre. Le coût global sera le même. Il n'y a pas de repas gratuit! La suppression d'un élément de l'arborescence est du même ordre.

Tous ces arbres nécessitent que les nœuds portent des informations sur la taille, le poids, la couleur, etc. Seuls les arbres Splay sont libres de ces informations supplémentaires. Mais la plupart des gens ont peur des arbres Splay, à cause de la nature aléatoire de leur structure!

Enfin, les arbres peuvent également transporter des informations sur le poids dans les nœuds, ce qui permet un équilibrage du poids. Divers schémas peuvent être appliqués. Il faut rééquilibrer lorsqu'un sous-arbre contient plus de 3 fois le nombre d'éléments de l'autre sous-arbre. Le rééquilibrage est à nouveau effectué soit par une rotation simple ou double. Cela signifie un pire cas de 2.4logn. On peut s'en tirer avec 2 fois au lieu de 3, un bien meilleur rapport, mais cela peut signifier laisser un peu moins de 1% des sous-arbres déséquilibrés ici et là. Rusé!

Quel type d'arbre est le meilleur? AVL à coup sûr. Ils sont les plus simples à coder et ont leur pire hauteur la plus proche de logn. Pour un arbre de 1000000 éléments, un AVL sera au maximum de hauteur 29, un RB 40, et un poids équilibré 36 ou 50 selon le rapport.

Il existe de nombreuses autres variables: caractère aléatoire, ratio d'ajouts, de suppressions, de recherches, etc.

user847376
la source
2
Bonne réponse. Mais si les AVL sont les meilleurs, pourquoi la bibliothèque standard implémente std :: map comme arbre RB?
Denis Gorodetskiy
13
Je ne suis pas d'accord pour dire que les arbres AVL sont incontestablement les meilleurs. Bien qu'ils soient de faible hauteur, ils nécessitent (au total) plus de travail pour effectuer le rééquilibrage que les arbres rouges / noirs (travail de rééquilibrage O (log n) versus travail de rééquilibrage amorti O (1)). Les arbres écartés pourraient être beaucoup, beaucoup mieux et votre affirmation selon laquelle les gens en ont peur n'est pas fondée. Il n'y a pas de "meilleur" schéma d'équilibrage d'arbre universel.
templatetypedef
Réponse presque parfaite. Pourquoi avez-vous dit qu'AVL est le meilleur. C'est tout simplement faux et c'est pourquoi la plupart des implémentations générales utilisent l'arbre rouge-noir. Vous devez avoir un ratio de lecture plus élevé pour choisir AVL. De plus, AVL a un peu moins d’encombrement mémoire que RB.
Eric Ouellet
Je suis d'accord que AVL a tendance à être meilleur dans la plupart des cas, car généralement les arbres sont recherchés plus souvent qu'ils ne sont insérés. Pourquoi l'arborescence RB est-elle si largement considérée comme meilleure alors que c'est celle qui présente un léger avantage dans le cas d'écriture et, plus important encore, un léger inconvénient dans le cas de lecture principalement? Croit-on vraiment que vous insérerez plus que vous n'en trouverez?
doug65536
25

Les réponses précédentes ne traitent que des alternatives d'arbres et le rouge noir ne reste probablement que pour des raisons historiques.

Pourquoi pas une table de hachage?

Un type requiert uniquement l' <opérateur (comparaison) comme clé dans une arborescence. Cependant, les tables de hachage nécessitent que chaque type de clé ait une hashfonction définie. Garder les exigences de type au minimum est très important pour la programmation générique afin que vous puissiez l'utiliser avec une grande variété de types et d'algorithmes.

Concevoir une bonne table de hachage nécessite une connaissance intime du contexte dans lequel elle sera utilisée. Doit-il utiliser l'adressage ouvert ou le chaînage lié? Quels niveaux de charge doit-il accepter avant de redimensionner? Doit-il utiliser un hachage coûteux qui évite les collisions, ou qui est rude et rapide?

Étant donné que la STL ne peut pas prévoir quel est le meilleur choix pour votre application, la valeur par défaut doit être plus flexible. Les arbres "fonctionnent" et évoluent bien.

(C ++ 11 a ajouté des tables de hachage avec unordered_map. Vous pouvez voir dans la documentation qu'il nécessite de définir des politiques pour configurer bon nombre de ces options.)

Et les autres arbres?

Les arbres rouges noirs offrent une recherche rapide et s'équilibrent automatiquement, contrairement aux BST. Un autre utilisateur a souligné ses avantages par rapport à l'arborescence AVL à équilibrage automatique.

Alexander Stepanov (le créateur de STL) a déclaré qu'il utiliserait un arbre B * au lieu d'un arbre rouge-noir s'il écrivait à std::mapnouveau, car il est plus convivial pour les caches de mémoire modernes.

L'un des plus grands changements depuis lors a été la croissance des caches. Les erreurs de cache sont très coûteuses, la localité de référence est donc beaucoup plus importante maintenant. Les structures de données basées sur des nœuds, qui ont une faible localité de référence, ont beaucoup moins de sens. Si je concevais STL aujourd'hui, j'aurais un ensemble différent de conteneurs. Par exemple, un arbre B * en mémoire est un bien meilleur choix qu'un arbre rouge-noir pour implémenter un conteneur associatif. - Alexander Stepanov

Les cartes devraient-elles toujours utiliser des arbres?

Une autre implémentation possible des cartes serait un vecteur trié (tri par insertion) et une recherche binaire. Cela fonctionnerait bien pour les conteneurs qui ne sont pas souvent modifiés mais qui sont fréquemment interrogés. Je fais souvent cela en C au fur qsortet à mesure que je suis bsearchintégré.

Dois-je même utiliser la carte?

Considérations de cache signifient il est rarement judicieux d'utiliser std::listou std::dequeplus , std:vectormême pour ces situations nous ont été enseignées à l' école (comme la suppression d' un élément du milieu de la liste). Appliquant ce même raisonnement, l'utilisation d'une boucle for pour rechercher une liste de manière linéaire est souvent plus efficace et plus propre que de créer une carte pour quelques recherches.

Bien sûr, le choix d'un conteneur lisible est généralement plus important que les performances.

Justin Meiners
la source
3

Mise à jour du 14/06/2017: webbertiger modifie sa réponse après avoir commenté. Je dois souligner que sa réponse est désormais bien meilleure à mes yeux. Mais j'ai gardé ma réponse tout comme des informations complémentaires ...

En raison du fait que je pense que la première réponse est fausse (correction: plus les deux) et la troisième a une affirmation erronée. Je sens que je devais clarifier les choses ...

Les 2 arbres les plus populaires sont AVL et Red Black (RB). La principale différence réside dans l'utilisation:

  • AVL: Mieux si le ratio de consultation (lecture) est supérieur à la manipulation (modification). L'empreinte mémoire est un peu inférieure à RB (en raison du peu nécessaire pour la coloration).
  • RB: Mieux dans les cas généraux où il y a un équilibre entre consultation (lecture) et manipulation (modification) ou plus de modification par rapport à la consultation. Une empreinte mémoire légèrement plus grande en raison du stockage du drapeau rouge-noir.

La principale différence vient de la coloration. Vous avez moins d'action de rééquilibrage dans l'arborescence RB qu'AVL car la coloration vous permet parfois d'ignorer ou de raccourcir les actions de rééquilibrage qui ont un coût relativement élevé. En raison de la coloration, l'arbre RB a également un niveau de nœuds plus élevé car il pourrait accepter des nœuds rouges entre les noirs (ayant la possibilité de ~ 2x plus de niveaux) rendant la recherche (lecture) un peu moins efficace ... mais parce que c'est un constante (2x), elle reste en O (log n).

Si vous considérez le résultat de performance pour une modification d'un arbre (significatif) VS le résultat de performance de consultation d'un arbre (presque insignifiant), il devient naturel de préférer RB à AVL pour un cas général.

Eric Ouellet
la source
2

C'est juste le choix de votre implémentation - ils peuvent être implémentés comme n'importe quel arbre équilibré. Les différents choix sont tous comparables avec des différences mineures. Par conséquent, tout est aussi bon que tout.

nécromancien
la source