Réponses:
Se chamailler sur les performances des arbres binaires n'a pas de sens - ils ne sont pas une structure de données, mais une famille de structures de données, toutes avec des caractéristiques de performance différentes. Bien qu'il soit vrai que les arbres binaires non équilibrés fonctionnent bien moins bien que les arbres binaires à équilibrage automatique pour la recherche, il existe de nombreux arbres binaires (tels que les essais binaires) pour lesquels "l'équilibrage" n'a pas de sens.
map
et set
dans les bibliothèques de nombreuses langues.La raison pour laquelle les arbres binaires sont utilisés plus souvent que les arbres n-aires pour la recherche est que les arbres n-aires sont plus complexes, mais n'offrent généralement aucun avantage réel en termes de vitesse.
Dans un arbre binaire (équilibré) avec des m
nœuds, passer d'un niveau au suivant nécessite une comparaison, et il y a des log_2(m)
niveaux, pour un total de log_2(m)
comparaisons.
En revanche, un arbre n-aire nécessitera des log_2(n)
comparaisons (à l'aide d'une recherche binaire) pour passer au niveau suivant. Puisqu'il y a des log_n(m)
niveaux totaux, la recherche nécessitera log_2(n)*log_n(m)
= log_2(m)
total de comparaisons. Ainsi, bien que les arbres n-aires soient plus complexes, ils n'offrent aucun avantage en termes de comparaisons totales nécessaires.
(Cependant, les arbres n-aires sont toujours utiles dans des situations de niche. Les exemples qui viennent immédiatement à l'esprit sont les arbres quadruples et autres arbres de partitionnement d'espace, où la division de l'espace en utilisant seulement deux nœuds par niveau rendrait la logique inutilement complexe; et Arborescences B utilisées dans de nombreuses bases de données, où le facteur limitant n'est pas le nombre de comparaisons effectuées à chaque niveau mais le nombre de nœuds pouvant être chargés à partir du disque dur à la fois)
Lorsque la plupart des gens parlent d'arbres binaires, ils pensent le plus souvent à la recherche binaire arbres de , donc je vais couvrir cela en premier.
Un arbre de recherche binaire non équilibré est en fait utile pour à peine plus que d'éduquer les étudiants sur les structures de données. En effet, à moins que les données n'entrent dans un ordre relativement aléatoire, l'arbre peut facilement dégénérer dans sa forme la plus défavorable, qui est une liste liée, car les arbres binaires simples ne sont pas équilibrés.
Un bon exemple: j'ai dû réparer un logiciel qui chargeait ses données dans un arbre binaire pour les manipuler et les rechercher. Il a écrit les données sous forme triée:
Alice
Bob
Chloe
David
Edwina
Frank
de sorte que, lors de sa relecture, se soit retrouvé avec l'arborescence suivante:
Alice
/ \
= Bob
/ \
= Chloe
/ \
= David
/ \
= Edwina
/ \
= Frank
/ \
= =
qui est la forme dégénérée. Si vous cherchez Frank dans cet arbre, vous devrez rechercher les six nœuds avant de le trouver.
Les arbres binaires deviennent vraiment utiles pour la recherche lorsque vous les équilibrez. Cela implique la rotation des sous-arbres à travers leur nœud racine afin que la différence de hauteur entre deux sous-arbres soit inférieure ou égale à 1. L'ajout de ces noms au-dessus d'un à la fois dans un arbre équilibré vous donnera la séquence suivante:
1. Alice
/ \
= =
2. Alice
/ \
= Bob
/ \
= =
3. Bob
_/ \_
Alice Chloe
/ \ / \
= = = =
4. Bob
_/ \_
Alice Chloe
/ \ / \
= = = David
/ \
= =
5. Bob
____/ \____
Alice David
/ \ / \
= = Chloe Edwina
/ \ / \
= = = =
6. Chloe
___/ \___
Bob Edwina
/ \ / \
Alice = David Frank
/ \ / \ / \
= = = = = =
Vous pouvez réellement voir des sous-arbres entiers tourner vers la gauche (aux étapes 3 et 6) lorsque les entrées sont ajoutées, ce qui vous donne un arbre binaire équilibré dans lequel la pire des recherches est O(log N)
plutôt que la O(N
) que donne la forme dégénérée. A aucun moment le plus haut NULL ( =
) ne diffère du plus bas par plus d'un niveau. Et, dans le dernier arbre ci-dessus, vous pouvez trouver Frank en ne regardant que trois nœuds ( Chloe
, Edwina
et, enfin,Frank
).
Bien sûr, ils peuvent devenir encore plus utiles lorsque vous en faites des arbres multi-voies équilibrés plutôt que des arbres binaires. Cela signifie que chaque nœud contient plus d'un élément (techniquement, ils contiennent N éléments et N + 1 pointeurs, un arbre binaire étant le cas particulier d'un arbre multidirectionnel à 1 voie avec 1 élément et 2 pointeurs).
Avec un arbre à trois, vous vous retrouvez avec:
Alice Bob Chloe
/ | | \
= = = David Edwina Frank
/ | | \
= = = =
Ceci est généralement utilisé pour gérer les clés d'un index d'éléments. J'ai écrit un logiciel de base de données optimisé pour le matériel où un nœud a exactement la taille d'un bloc de disque (disons, 512 octets) et vous mettez autant de clés que possible dans un seul nœud. Dans ce cas, les pointeurs étaient en fait des numéros d'enregistrement dans un fichier d'accès direct à enregistrement de longueur fixe distinct du fichier d'index (donc le numéro d'enregistrement X
pouvait être trouvé en cherchant simplement à le faire X * record_length
).
Par exemple, si les pointeurs sont de 4 octets et que la taille de clé est de 10, le nombre de clés dans un nœud de 512 octets est de 36. C'est 36 clés (360 octets) et 37 pointeurs (148 octets) pour un total de 508 octets avec 4 octets perdus par nœud.
L'utilisation de clés multidirectionnelles introduit la complexité d'une recherche en deux phases (recherche multidirectionnelle pour trouver le nœud correct combinée à une petite recherche séquentielle (ou binaire linéaire) pour trouver la clé correcte dans le nœud) mais l'avantage dans faire moins d'E / S disque que cela ne compense.
Je ne vois aucune raison de le faire pour une structure en mémoire, vous feriez mieux de vous en tenir à un arbre binaire équilibré et de garder votre code simple.
Gardez également à l'esprit que les avantages de O(log N)
over O(N)
n'apparaissent pas vraiment lorsque vos ensembles de données sont petits. Si vous utilisez un arbre multidirectionnel pour stocker les quinze personnes dans votre carnet d'adresses, c'est probablement exagéré. Les avantages viennent lorsque vous stockez quelque chose comme chaque commande de vos cent mille clients au cours des dix dernières années.
L'intérêt de la notation big-O est d'indiquer ce qui se passe à l' N
approche de l'infini. Certaines personnes peuvent ne pas être d'accord, mais il est même possible d'utiliser le tri à bulles si vous êtes sûr que les ensembles de données resteront en dessous d'une certaine taille, tant que rien d'autre n'est facilement disponible :-)
Quant aux autres utilisations des arbres binaires, il en existe un grand nombre, telles que:
Étant donné la quantité d'explications que j'ai générées pour les arbres de recherche, je suis réticent à entrer dans beaucoup de détails sur les autres, mais cela devrait suffire pour les rechercher, si vous le souhaitez.
L'organisation du code Morse est un arbre binaire.
Un arbre binaire est une structure de données arborescente dans laquelle chaque nœud a au plus deux nœuds enfants, généralement distingués comme "gauche" et "droit". Les nœuds avec enfants sont des nœuds parents et les nœuds enfants peuvent contenir des références à leurs parents. En dehors de l'arbre, il y a souvent une référence au nœud "racine" (l'ancêtre de tous les nœuds), s'il existe. Tout nœud dans la structure de données peut être atteint en commençant au nœud racine et en suivant à plusieurs reprises les références à l'enfant gauche ou droit. Dans un arbre binaire, un degré de chaque nœud est au maximum de deux.
Les arbres binaires sont utiles, car comme vous pouvez le voir sur l'image, si vous voulez trouver un nœud dans l'arbre, vous n'avez qu'à regarder un maximum de 6 fois. Si vous souhaitez rechercher le nœud 24, par exemple, vous commencerez à la racine.
Cette recherche est illustrée ci-dessous:
Vous pouvez voir que vous pouvez exclure la moitié des nœuds de l'arbre entier lors du premier passage. et la moitié du sous-arbre gauche sur le second. Cela rend les recherches très efficaces. Si cela a été fait sur 4 milliards d' éléments, il vous suffira de rechercher un maximum de 32 fois. Par conséquent, plus il y a d'éléments dans l'arborescence, plus votre recherche peut être efficace.
Les suppressions peuvent devenir complexes. Si le nœud a 0 ou 1 enfant, il suffit simplement de déplacer certains pointeurs pour exclure celui à supprimer. Cependant, vous ne pouvez pas facilement supprimer un nœud avec 2 enfants. Nous prenons donc un raccourci. Disons que nous voulions supprimer le nœud 19.
Comme il n'est pas facile de déterminer où déplacer les pointeurs gauche et droit, nous en trouvons un pour le remplacer. Nous allons dans le sous-arbre de gauche et allons aussi loin que possible à droite. Cela nous donne la deuxième plus grande valeur du nœud que nous voulons supprimer.
Maintenant, nous copions tout le contenu de 18, à l'exception des pointeurs gauche et droit, et supprimons le nœud 18 d'origine.
Pour créer ces images, j'ai implémenté un arbre AVL, un arbre à équilibrage automatique, de sorte qu'à tout moment, l'arbre ait au plus un niveau de différence entre les nœuds feuilles (nœuds sans enfants). Cela empêche l'arbre de devenir asymétrique et maintient le temps de O(log n)
recherche maximal , avec le coût d'un peu plus de temps requis pour les insertions et les suppressions.
Voici un exemple montrant comment mon arbre AVL s'est maintenu aussi compact et équilibré que possible.
Dans un tableau trié, les recherches prendraient toujours O(log(n))
, tout comme un arbre, mais l'insertion et la suppression aléatoires prendraient O (n) à la place de l'arbre O(log(n))
. Certains conteneurs STL utilisent ces caractéristiques de performance à leur avantage, de sorte que les temps d'insertion et de retrait prennent un maximum O(log n)
, ce qui est très rapide. Certains de ces conteneurs sont map
, multimap
, set
et multiset
.
Un exemple de code pour une arborescence AVL est disponible sur http://ideone.com/MheW8
L'application principale est les arbres de recherche binaires . Il s'agit d'une structure de données dans laquelle la recherche, l'insertion et la suppression sont toutes très rapides (à propos des log(n)
opérations)
Un exemple intéressant d'arbre binaire qui n'a pas été mentionné est celui d'une expression mathématique évaluée récursivement. C'est fondamentalement inutile d'un point de vue pratique, mais c'est une façon intéressante de penser à de telles expressions.
Fondamentalement, chaque nœud de l'arbre a une valeur qui lui est inhérente ou qui est évaluée par récursivité en opérant sur les valeurs de ses enfants.
Par exemple, l'expression (1+3)*2
peut être exprimée comme suit:
*
/ \
+ 2
/ \
1 3
Pour évaluer l'expression, nous demandons la valeur du parent. Ce nœud obtient à son tour ses valeurs de ses enfants, un opérateur plus et un nœud qui contient simplement «2». L'opérateur plus obtient à son tour ses valeurs des enfants avec les valeurs «1» et «3» et les ajoute, renvoyant 4 au nœud de multiplication qui renvoie 8.
Cette utilisation d'un arbre binaire s'apparente à une notation polonaise inverse dans un sens, en ce sens que l'ordre dans lequel les opérations sont effectuées est identique. Une autre chose à noter est qu'il ne doit pas nécessairement être un arbre binaire, c'est juste que les opérateurs les plus couramment utilisés sont binaires. À son niveau le plus élémentaire, l'arbre binaire ici n'est en fait qu'un langage de programmation purement fonctionnel très simple.
Applications de l'arbre binaire:
Je ne pense pas qu'il soit utile d'utiliser des arbres binaires "purs". (sauf à des fins éducatives) Arbres binaires équilibrés, tels que les arbres rouge-noir ou les arbres AVL sont beaucoup plus utiles, car ils garantissent des opérations O (logn). Les arbres binaires normaux peuvent finir par être une liste (ou presque une liste) et ne sont pas vraiment utiles dans les applications utilisant beaucoup de données.
Les arbres équilibrés sont souvent utilisés pour implémenter des cartes ou des ensembles. Ils peuvent également être utilisés pour le tri en O (nlogn), même s'il existe de meilleures façons de le faire.
Vous pouvez également utiliser pour rechercher / insérer / supprimer des tables de hachage , qui ont généralement de meilleures performances que les arbres de recherche binaires (équilibrés ou non).
Une application dans laquelle des arbres de recherche binaires (équilibrés) seraient utiles serait la recherche / l'insertion / la suppression et le tri. Le tri pourrait être en place (presque, en ignorant l'espace de pile nécessaire pour la récursivité), étant donné un arbre équilibré de construction prêt. Ce serait toujours O (nlogn) mais avec un facteur constant plus petit et aucun espace supplémentaire nécessaire (sauf pour le nouveau tableau, en supposant que les données doivent être placées dans un tableau). En revanche, les tables de hachage ne peuvent pas être triées (du moins pas directement).
Peut-être sont-ils également utiles dans certains algorithmes sophistiqués pour faire quelque chose, mais rien ne me vient à l'esprit. Si j'en trouve plus, je modifierai mon message.
D'autres arbres comme les arbres fe B + sont largement utilisés dans les bases de données
L'une des applications les plus courantes consiste à stocker efficacement les données sous forme triée afin d'accéder et de rechercher rapidement les éléments stockés. Par exemple, std::map
ou std::set
dans la bibliothèque standard C ++.
L'arbre binaire en tant que structure de données est utile pour diverses implémentations d'analyseurs d'expression et de solveurs d'expression.
Il peut également être utilisé pour résoudre certains problèmes de base de données, par exemple l'indexation.
Généralement, l'arbre binaire est un concept général de structure de données basée sur un arbre particulier et divers types spécifiques d'arbres binaires peuvent être construits avec différentes propriétés.
En C ++ STL, et de nombreuses autres bibliothèques standard dans d'autres langages, comme Java et C #. Les arbres de recherche binaires sont utilisés pour implémenter set et map.
L'une des applications les plus importantes des arbres binaires sont les arbres de recherche binaires équilibrés comme:
Ces types d'arbres ont la propriété que la différence de hauteur entre le sous-arbre gauche et le sous-arbre droit est maintenue petite en effectuant des opérations comme des rotations chaque fois qu'un nœud est inséré ou supprimé.
De ce fait, la hauteur globale de l'arbre reste de l'ordre de log n et les opérations telles que la recherche, l'insertion et la suppression des nœuds sont effectuées en temps O (log n). La STL de C ++ implémente également ces arbres sous forme d'ensembles et de cartes.
Sur le matériel moderne, un arbre binaire est presque toujours sous-optimal en raison d'un mauvais cache et d'un comportement spatial. Cela vaut également pour les variantes (semi) équilibrées. Si vous les trouvez, c'est là que les performances ne comptent pas (ou sont dominées par la fonction de comparaison), ou plus probablement pour des raisons historiques ou d'ignorance.
Un compilateur qui utilise un arbre binaire pour une représentation d'un AST peut utiliser des algorithmes connus pour analyser l'arbre comme postorder, inorder. Le programmeur n'a pas besoin de trouver son propre algorithme. Parce qu'un arbre binaire pour un fichier source est plus élevé que l'arbre n-aire, sa construction prend plus de temps. Prenez cette production: selstmnt: = "if" "(" expr ")" stmnt "ELSE" stmnt Dans un arbre binaire, il aura 3 niveaux de nœuds, mais l'arbre n-aire aura 1 niveau (de chids)
C'est pourquoi les OS basés sur Unix sont lents.