Lorsque vous apprenez à coder pour la première fois, il est courant d’apprendre les tableaux comme » structure de données principale « .
Éventuellement, vous apprendrez aussi à connaître hash tables
. Si vous poursuivez des études en informatique, vous devez suivre un cours sur la structure de données. Vous apprendrez également à connaître linked lists
queues
et stacks
. Ces structures de données sont appelées structures de données » linéaires » parce qu’elles ont toutes un début et une fin logiques.
Lorsque nous commençons à apprendre à propos de trees
et graphs
, cela peut devenir vraiment confus. Nous ne stockons pas les données de manière linéaire. Les deux structures de données stockent les données d’une manière spécifique.
Ce billet a pour but de vous aider à mieux comprendre la structure de données arborescente et de clarifier toute confusion que vous pourriez avoir à son sujet.
Dans cet article, nous allons apprendre :
- Ce qu’est un arbre
- Des exemples d’arbres
- Sa terminologie et son fonctionnement
- Comment mettre en œuvre des structures arborescentes dans le code.
Commençons ce voyage d’apprentissage. 🙂
Définition
Lorsque l’on débute en programmation, il est courant de mieux comprendre les structures de données linéaires que les structures de données comme les arbres et les graphes.
Les arbres sont bien connus comme étant une structure de données non linéaire. Ils ne stockent pas les données de manière linéaire. Ils organisent les données de manière hiérarchique.
Plongeons-nous dans des exemples concrets !
Qu’est-ce que je veux dire quand je dis de manière hiérarchique ?
Imaginez un arbre généalogique avec des relations de toutes les générations : grands-parents, parents, enfants, frères et sœurs, etc. Nous organisons communément les arbres généalogiques de manière hiérarchique.
Le dessin ci-dessus est est mon arbre généalogique. Tossico, Akikazu, Hitomi,
et Takemi
sont mes grands-parents.
Toshiaki
et Juliana
sont mes parents.
TK, Yuji, Bruno
, et Kaio
sont les enfants de mes parents (moi et mes frères).
La structure d’une organisation est un autre exemple de hiérarchie.
En HTML, le modèle d’objet de document (DOM) fonctionne comme un arbre.
La balise HTML
contient d’autres balises. Nous avons une balise head
et une balise body
. Ces balises contiennent des éléments spécifiques. La balise head
comporte les balises meta
et title
. La balise body
comporte des éléments qui apparaissent dans l’interface utilisateur, par exemple, h1
a
li
, etc.
Une définition technique
Une tree
est une collection d’entités appelées nodes
. Les nœuds sont reliés par edges
. Chaque node
contient une value
ou une data
, et elle peut avoir ou non une child node
.
La first node
de la tree
est appelée la root
. Si cette root node
est reliée par une autre node
, la root
est alors une parent node
et la node
connectée est une child
.
Toutes les Tree nodes
sont reliées par des liens appelés edges
. C’est une partie importante de trees
, car elle gère la relation entre nodes
.
Leaves
sont les derniers nodes
sur un tree.
Ce sont des nœuds sans enfants. Comme de vrais arbres, on a le root
branches
, et enfin le leaves
.
Les autres notions importantes à comprendre sont height
et depth
.
La height
d’une tree
est la longueur du plus long chemin vers une leaf
.
La depth
d’une node
est la longueur du chemin vers sa root
.
Résumé terminologique
- La racine est la
node
la plus haute de latree
- La bordure est le lien entre deux
nodes
- L’enfant est une
node
qui a uneparent node
- Le parent est . une
node
qui a uneedge
à unechild node
. - La feuille est une
node
qui ne possède pas dechild node
dans latree
- La hauteur est la longueur du chemin le plus long vers une
leaf
- La profondeur est la longueur du chemin vers son
root
.
Arbres binaires
Nous allons maintenant aborder un type spécifique de tree
. Nous l’appelons lebinary tree
.
« En informatique, un arbre binaire est une structure de données arborescente dans laquelle chaque nœud a au plus deux enfants, que l’on appelle l’enfant gauche et l’enfant droit. » – Wikipédia
Voyons donc un exemple d’un binary tree
.
Codons un arbre binaire
La première chose que nous devons garder à l’esprit lorsque nous implémentons un binary tree
est qu’il s’agit d’une collection de nodes
. Chaque node
possède trois attributs : value
left_child
, et right_child
.
Comment mettre en œuvre une simple binary tree
qui s’initialise avec ces trois propriétés ?
Voyons cela.
class BinaryTree: def __init__(self, value): self.value = value self.left_child = None self.right_child = None
Voici. Notre classe binary tree
.
Lorsque nous instancions un objet, nous passons le value
(les données du nœud) en paramètre. Regardez le left_child
et le right_child
. Les deux sont définis sur None
.
Pourquoi ?
Parce que lorsque nous créons notre node
, elle n’a pas d’enfants. Nous avons juste le node data
.
Testons-le :
tree = BinaryTree('a')print(tree.value) # aprint(tree.left_child) # Noneprint(tree.right_child) # None
C’est tout.
Nous pouvons passer le string
a
‘ comme value
à notre Binary Tree node
. Si nous imprimons les value
left_child
, et right_child
, nous pouvons voir les valeurs.
Passons à la partie insertion. Que devons-nous faire ici ?
Nous allons implémenter une méthode pour insérer un nouveau node
à la right
et à la left
.
Voici les règles :
- Si la
node
actuelle n’a pas deleft child
, nous créons simplement une nouvellenode
et la fixons à laleft_child
du nœud actuel. - Si elle possède bien la
left child
, nous créons un nouveau nœud et le mettons à la place de laleft child
actuelle. Allouez cetteleft child node
à laleft child
du nouveau nœud.
Dessinons-le. 🙂
Voici le code :
Encore, si le nœud actuel n’a pas de left child
, nous créons simplement un nouveau nœud et le définissons sur la left_child
du nœud actuel. Ou bien nous créons un nouveau nœud et le mettons à la place de la left child
actuelle. Affectons cette left child node
à la left child
du nouveau nœud.
Et nous faisons la même chose pour insérer une right child node
.
Fait. 🙂
Mais pas entièrement. Nous devons encore le tester.
Construisons letree
:
Pour résumer l’illustration de cet arbre :
-
a
node
sera laroot
de notrebinary Tree
. -
a
left child
estb
node
. -
a right child
estc
node
. -
b
right child
estd
node
b
node
n’a pas deleft child
) -
c
left child
este
node
-
c
right child
estf
node
- à la fois
e
etf
nodes
n’ont pas d’enfants
Voici donc le code pour la tree
:
L’insertion est faite.
Maintenant nous devons penser à la traversée de tree
.
Nous avons deux options ici : La recherche en profondeur (DFS) et la recherche en largeur (BFS).
- La DFS « est un algorithme de traversée ou de recherche de structure de données en arbre. On commence à la racine et on explore aussi loin que possible le long de chaque branche avant de revenir en arrière. » – Wikipédia
- BFS « est un algorithme de traversée ou de recherche de structure de données en arbre. Il commence à la racine de l’arbre et explore d’abord les nœuds voisins, avant de passer aux voisins du niveau suivant. » – Wikipédia
Par conséquent, plongeons dans chaque type de traversée d’arbre.
Recherche en profondeur (DFS)
DFS explore un chemin jusqu’à une feuille avant de revenir en arrière et d’explorer un autre chemin. Voyons un exemple avec ce type de traversée.
Le résultat pour cet algorithme sera 1-2-3-4-5-6-7.
Pourquoi ?
Décomposons-le.
- Débutez au
root
(1). Imprimez-le.
2. Allez à la left child
(2). Imprimez-le.
3. Allez ensuite dans la left child
(3). Imprimez-la. (Cette node
n’a pas d’enfants)
4. Revenez en arrière et allez à la right child
(4). Imprimez-la. (Cette node
n’a pas d’enfants)
5. Revenez à la root
node
et allez à la right child
(5). Imprimez-le.
6. Allez à la left child
(6). Imprimez-le. (Cette node
n’a pas d’enfants)
7. Revenez en arrière et allez à la right child
(7). Imprimez-le. (Ce node
n’a pas d’enfants)
8. Fait.
Lorsque nous allons profondément dans la feuille et que nous revenons en arrière, cela s’appelle l’algorithme DFS.
Maintenant que nous sommes familiers avec cet algorithme de traversée, nous allons discuter des types de DFS : pre-order
in-order
, et post-order
.
Pré-commande
C’est exactement ce que nous avons fait dans l’exemple ci-dessus.
- Imprimer la valeur de la
node
. - Aller à la
left child
et l’imprimer. C’est si, et seulement si, il a uneleft child
. - Allez à la
right child
et imprimez-la. Ceci si, et seulement si, il possède uneright child
.
En ordre
Le résultat de l’algorithme en ordre pour cet tree
exemple est 3-2-4-1-6-5-7.
La gauche en premier, le milieu en second et la droite en dernier.
Maintenant, codons-le.
def in_order(self): if self.left_child: self.left_child.in_order() print(self.value) if self.right_child: self.right_child.in_order()
- Voir la
left child
et l’imprimer. C’est si, et seulement si, elle a uneleft child
. - Imprimer la valeur de la
node
- Aller à la
right child
et l’imprimer. Ceci si, et seulement si, il possède uneright child
.
Post-ordre
Le résultat de l’algorithme post order
pour cet tree
exemple est 3-4-2-6-7-5-1.
La gauche en premier, la droite en second et le milieu en dernier.
Codons cela.
- Aller dans la
left child
et l’imprimer. C’est si, et seulement si, il a uneleft child
. - Allez à la
right child
et imprimez-la. Ceci si, et seulement si, il possède uneright child
. - Imprimer la valeur de la
node
Recherche en largeur et en premier (BFS)
L’algorithme BFS parcourt la tree
niveau par niveau et profondeur par profondeur.
Voici un exemple qui permet de mieux expliquer cet algorithme :
On traverse donc niveau par niveau. Dans cet exemple, le résultat est 1-2-5-3-4-6-7.
- Niveau/profondeur 0 : uniquement
node
avec la valeur 1 - Niveau/profondeur 1 :
nodes
avec les valeurs 2 et 5 - Niveau/Profondeur 2 :
nodes
avec les valeurs 3, 4, 6 et 7
Maintenant, codons-le.
Pour mettre en œuvre un algorithme BFS, nous utilisons la structure de données queue
pour nous aider.
Comment cela fonctionne-t-il ?
Voici l’explication.
- Premièrement, ajoutez la
root
node
dans laqueue
avec la méthodeput
. - Itérez tant que la
queue
n’est pas vide. - Recevoir le premier
node
dans lequeue
, puis imprimer sa valeur. - Ajouter à la fois
left
etright
children
dans laqueue
(si lanode
actuelle achildren
). - Fait. Nous allons imprimer la valeur de chaque
node,
niveau par niveau, avec notrequeue
aide.
Arbre de recherche binaire
« Un arbre de recherche binaire est parfois appelé arbre binaire ordonné ou trié, et il conserve ses valeurs dans un ordre trié, de sorte que la recherche et d’autres opérations peuvent utiliser le principe de la recherche binaire » – Wikipédia
Une propriété importante d’un Binary Search Tree
est que la valeur d’une Binary Search Tree
node
est plus grande que la valeur de la descendance de sa left child
, mais inférieure à la valeur de la descendance de son right child.
«
Voici une décomposition de l’illustration ci-dessus :
- A est inversé. Le
subtree
7-5-8-6 doit être à droite, et lesubtree
2-1-3 doit être à gauche. - B est la seule option correcte. Elle satisfait à la propriété
Binary Search Tree
. - C a un problème : le
node
avec la valeur 4. Il doit être à gauche duroot
car il est plus petit que 5.
Codons un arbre de recherche binaire!
Maintenant il est temps de coder!
Que verrons-nous ici ? Nous allons insérer de nouveaux nœuds, rechercher une valeur, supprimer des nœuds, et le bilan de la tree
.
Commençons.
Insertion : ajouter de nouveaux nœuds à notre arbre
Imaginez que nous avons une tree
vide et que nous voulons ajouter de nouvelles nodes
avec les valeurs suivantes dans cet ordre : 50, 76, 21, 4, 32, 100, 64, 52.
La première chose que nous devons savoir est si 50 est la root
de notre arbre.
Nous pouvons maintenant commencer à insérer node
par node
.
- 76 est supérieur à 50, donc insérer 76 à droite.
- 21 est inférieur à 50, donc insérer 21 à gauche.
- 4 est inférieur à 50.
Node
avec la valeur 50 a uneleft child
21. Puisque 4 est plus petit que 21, insérez-le sur le côté gauche de cettenode
. - 32 est plus petit que 50.
Node
avec la valeur 50 a uneleft child
21. Puisque 32 est supérieur à 21, insérez 32 à droite de cettenode
. - 100 est supérieur à 50.
Node
avec la valeur 50 a uneright child
76. Puisque 100 est supérieur à 76, insérez 100 à droite de cettenode
. - 64 est supérieur à 50.
Node
avec la valeur 50 a uneright child
76. Puisque 64 est plus petit que 76, insérez 64 sur le côté gauche de cettenode
. - 52 est supérieur à 50.
Node
avec la valeur 50 a uneright child
76. Puisque 52 est plus petit que 76,node
avec la valeur 76 a uneleft child
64. 52 est plus petit que 64, donc insérez 54 sur le côté gauche de cettenode
.
Vous remarquez un modèle ici ?
Décomposons-le.
- La nouvelle valeur de
node
est-elle supérieure ou inférieure à la valeur actuelle denode
? - Si la valeur de la nouvelle
node
est supérieure à lanode,
actuelle, passez à lasubtree
de droite. Si lanode
actuelle n’a pas deright child
, insérez-la à cet endroit, sinon revenez à l’étape #1. - Si la valeur de la nouvelle
node
est inférieure à celle de lanode
actuelle, passez à lasubtree
de gauche. Si lanode
actuelle n’a pas deleft child
, insérez-la à cet endroit, sinon revenez à l’étape #1. - Nous n’avons pas traité les cas particuliers ici. Lorsque la valeur d’une nouvelle
node
est égale à la valeur actuelle de lanode,
, utilisez la règle numéro 3. Pensez à insérer des valeurs égales à gauche dusubtree
.
Maintenant, codons-le.
Il semble très simple.
La partie puissante de cet algorithme est la partie récursion, qui se trouve à la ligne 9 et à la ligne 13. Les deux lignes de code appellent la méthode insert_node
, et l’utilisent pour ses left
et right
children
, respectivement. Les lignes 11
et 15
sont celles qui font l’insertion pour chaque child
.
Recherche de la valeur du nœud… Ou pas…
L’algorithme que nous allons construire maintenant consiste à faire des recherches. Pour une valeur donnée (nombre entier), nous allons dire si notre Binary Search Tree
possède ou non cette valeur.
Un élément important à noter est la façon dont nous avons défini l’algorithme d’insertion d’arbre. Nous avons d’abord notre root
node
. Toutes les subtree
nodes
de gauche auront des valeurs plus petites que la root
node
. Et toutes les bonnes subtree
nodes
auront des valeurs supérieures à la root
node
.
Regardons un exemple.
Imaginez que nous avons cette tree
.
Nous voulons maintenant savoir si nous avons un nœud basé sur la valeur 52.
Décomposons.
- Nous commençons avec la
root
node
comme notrenode
actuelle. La valeur donnée est-elle plus petite que la valeur actuelle denode
? Si oui, alors nous la rechercherons sur la gauchesubtree
. - La valeur donnée est-elle supérieure à la valeur actuelle
node
? Si oui, alors nous la chercherons à droitesubtree
. - Si les règles #1 et #2 sont toutes deux fausses, nous pouvons comparer la valeur actuelle
node
et la valeur donnée si elles sont égales. Si la comparaison renvoietrue
, alors nous pouvons dire : » Oui ! Notretree
a la valeur donnée « , sinon, nous disons : » Nooon, ce n’est pas le cas ! «
Maintenant, codons-le.
Découpons le code :
- Les lignes 8 et 9 relèvent de la règle n°1.
- Les lignes 10 et 11 relèvent de la règle n°2.
- La ligne 13 relève de la règle n°3.
Comment le tester ?
Créons notre Binary Search Tree
en initialisant la root
node
avec la valeur 15.
bst = BinarySearchTree(15)
Et maintenant nous allons insérer plusieurs nouvelles nodes
.
Pour chaque node
insérée, nous allons tester si notre méthode find_node
fonctionne vraiment.
Oui, elle fonctionne pour ces valeurs données ! Testons pour une valeur qui n’existe pas dans notre Binary Search Tree
.
print(bst.find_node(0)) # False
Oh oui.
Notre recherche est terminée.
Suppression : enlever et organiser
La suppression est un algorithme plus complexe car nous devons gérer différents cas. Pour une valeur donnée, nous devons supprimer le node
avec cette valeur. Imaginons les scénarios suivants pour cette node
: elle n’a pas de children
, a une seule child
, ou a deux children
.
- Scénario #1 : Une
node
sanschildren
leaf
node
).
# |50| |50|# / \ / \# |30| |70| (DELETE 20) ---> |30| |70|# / \ \# |20| |40| |40|
Si la node
que nous voulons supprimer n’a pas d’enfants, nous la supprimons simplement. L’algorithme n’a pas besoin de réorganiser le tree
.
- Scénario #2 : Une
node
avec un seul enfant (left
ouright
enfant).
# |50| |50|# / \ / \# |30| |70| (DELETE 30) ---> |20| |70|# / # |20|
Dans ce cas, notre algorithme doit faire en sorte que le parent de la node
pointe vers le nœud child
. Si la node
est la left child
, nous faisons en sorte que le parent de la left child
pointe vers la child
. Si la node
est la right child
de son parent, on fait pointer le parent de la right child
vers la child
.
- Scénario #3 : Une
node
avec deux enfants.
# |50| |50|# / \ / \# |30| |70| (DELETE 30) ---> |40| |70|# / \ /# |20| |40| |20|
Lorsque la node
a 2 enfants, nous devons trouver la node
avec la valeur minimale, en partant de la node
‘sright child
. Nous mettrons cette node
à valeur minimale à la place de la node
que nous voulons supprimer.
Il est temps de coder.
- Premièrement : Notez les paramètres
value
etparent
. Nous voulons trouver lanode
qui possède cettevalue
, et le parent de lanode
est important pour la suppression de lanode
. - Deuxièmement : notez la valeur de retour. Notre algorithme renvoie une valeur booléenne. Il renvoie
True
s’il trouve lenode
et le supprime. Sinon, il renvoieFalse
. - De la ligne 2 à la ligne 9 : Nous commençons à chercher la
node
qui possède lavalue
que nous recherchons. Si lavalue
est plus petite que lacurrent nodevalue
, on va vers laleft subtree
, de manière récursive (si, et seulement si, lacurrent node
possède uneleft child
). Si lavalue
est supérieure, on passe à laright subtree
, de manière récursive. - Ligne 10 : On commence à réfléchir à l’algorithme
remove
. - De la ligne 11 à la ligne 13 : Nous couvrons le
node
sanschildren
, et c’est leleft child
de sonparent
. Nous supprimons lanode
en mettant laparent
de laleft child
enNone
. - Lignes 14 et 15 : Nous recouvrons la
node
sanschildren
, et c’est laright child
de saparent
. Nous supprimons lanode
en mettant laparent
de laright child
None
. - Méthode Clear node : Je vais montrer le code
clear_node
ci-dessous. Il définit les nœudsleft child , right child
, et sonvalue
enNone
. - De la ligne 16 à la ligne 18 : Nous couvrons la
node
avec une seulechild
left child
), et c’est laleft child
de saparent
. Nous définissons laparent
de laleft child
sur lanode
de laleft child
(le seul enfant qu’elle possède). - De la ligne 19 à la ligne 21 : Nous couvrons la
node
avec une seulechild
left child
), et c’est laright child
de saparent
. Nous fixons laparent
de laright child
à lanode
de laleft child
(le seul enfant qu’elle a). - De la ligne 22 à la ligne 24 : Nous couvrons la
node
avec une seulechild
right child
), et c’est laleft child
de saparent
. Nous réglons laparent
de laleft child
sur lanode
de laright child
(le seul enfant qu’elle a). - De la ligne 25 à la ligne 27 : Nous couvrons la
node
avec une seulechild
right child
) , et c’est laright child
de saparent
. Nous mettons laparent
de laright child
sur lanode
de laright child
(le seul enfant qu’elle a). - De la ligne 28 à la ligne 30 : Nous couvrons la
node
avec les deuxleft
etright
enfants. On récupère lanode
avec la plus petitevalue
(le code est indiqué ci-dessous) et on la place sur lavalue
de lacurrent node
. Terminez-la en supprimant le plus petitnode
. - Ligne 32 : Si nous trouvons le
node
que nous cherchons, il doit retournerTrue
. De la ligne 11 à la ligne 31, nous gérons ce cas. Il suffit donc de retournerTrue
et c’est tout.
- Pour utiliser la méthode
clear_node
: définissez la valeurNone
sur les trois attributs – (value
left_child
, etright_child
)
def clear_node(self): self.value = None self.left_child = None self.right_child = None
- Pour utiliser la méthode
find_minimum_value
: descendez tout en bas à gauche. Si nous ne trouvons plus de nœuds, nous avons trouvé le plus petit.
def find_minimum_value(self): if self.left_child: return self.left_child.find_minimum_value() else: return self.value
Maintenant, testons-le.
Nous allons utiliser cette tree
pour tester notre remove_node
algorithme.
# |15|# / \# |10| |20|# / \ / \# |8| |12| |17| |25|# \# |19|
Supprimons la node
avec la value
8. C’est une node
sans enfant.
print(bst.remove_node(8, None)) # Truebst.pre_order_traversal()# |15|# / \# |10| |20|# \ / \# |12| |17| |25|# \# |19|
Maintenant, supprimons la node
avec la value
17. C’est une node
avec un seul enfant.
print(bst.remove_node(17, None)) # Truebst.pre_order_traversal()# |15|# / \# |10| |20|# \ / \# |12| |19| |25|
Enfin, nous allons supprimer une node
avec deux enfants. C’est le root
de notre tree
.
print(bst.remove_node(15, None)) # Truebst.pre_order_traversal()# |19|# / \# |10| |20|# \ \# |12| |25|
Les tests sont maintenant terminés. 🙂
C’est tout pour le moment !
Nous avons beaucoup appris ici.
Félicitations pour avoir terminé ce contenu dense. C’est vraiment difficile de comprendre un concept que l’on ne connaît pas. Mais vous l’avez fait 🙂
C’est un pas de plus dans mon voyage pour apprendre et maîtriser les algorithmes et les structures de données. Vous pouvez voir la documentation de mon parcours complet ici sur ma publication Renaissance Developer.
Amusez-vous bien, continuez à apprendre et à coder.
Mon Twitter & Github. ☺
Ressources supplémentaires
- Introduction à la structure de données des arbres par mycodeschool
- Tree par Wikipedia
- How To Not Be Stumped By Trees par le talentueux Vaidehi Joshi
- Intro to Trees, Lecture par le professeur Jonathan Cohen
- Intro to Trees, Conférence du professeur David Schmidt
- Intro aux arbres, Conférence du professeur Victor Adamchik
- Les arbres avec Gayle Laakmann McDowell
- Mise en œuvre et tests d’arbres binaires par TK
- Coursera Course : Structures de données par l’Université de Californie, San Diego
- Coursera Course : Structures de données et performances par l’Université de Californie, San Diego
- Concepts et mise en œuvre de l’arbre de recherche binaire par Paul Programming
- Mise en œuvre et tests de l’arbre de recherche binaire par TK
- Tree Traversal par Wikipedia
- Arbre de recherche binaire : algorithme de suppression des nœuds par GeeksforGeeks
- Arbre de recherche binaire : algorithme de suppression des nœuds par Algolist
- Apprentissage de Python de zéro à héros
.