Introduction aux Types Abstraits de Données
Introduction aux Types Abstraits de Données
1. Concept d’abstraction
2. Concept général de Type Abstrait de Données
3. Type Abstrait de Données
4. Implémentation d’un Type Abstrait de Données
A. Tableau
B. Liste Chaînée
C. Pile
D. File d’Attente
E. Graphe
F. Arbre
5. Complexité des Algorithmes
6. Aspects Généraux et Basiques de Tri
BIBLIOGRAPHIE
The Art of computer programming - Donald E. Knuth - Ed. Addison Wesley - 1970
Type de donnée détermine le genre de contenu d’une donnée et les opérations pouvant être
effectuées sur la variable correspondante.
devront être implémentés par les classes filles, mais sans nécessairement
implémenter ces méthodes, de cette façon, on a l'assurance que les classes
filles respecteront le contrat (interface de programmation) défini par la classe
mère abstraite.
Un TAD met au même niveau les données et les opérations, et permet l’élaboration des
algorithmes :
1. d’abord d’une manière abstraite en faisant appel aux données et à des opérations
abstraites du TAD (couche supérieure),
2. ensuite relativement à un choix de représentation du TAD en mémoire
(couche inférieure).
La couche supérieure est ainsi une référence qui restera inchangée quelle que soit la
modification apportée à la couche inférieure.
Toute donnée a besoin d’une représentation en mémoire, qui doit être en adéquation
± exacte avec sa nature.
Rôle d’une structure de données fournir une implémentation de ces données en mémoire.
Une structure de données = modèle logique qui correspond à l’implémentation d’un TAD et
qui permet d’organiser les données d’une manière particulière
dans une machine afin de pouvoir être utilisées efficacement.
Chaque algorithme doit en fait être un module, une composante logique qui spécifie un seul
thème en encapsulant solidement ses fonctionnalités et ses types.
Donc, une abstraction dans l’organisation des données définit un espace de raisonnement,
dénué des détails inutiles, où l’algorithmique correspondante peut s’exprimer
plus efficacement et plus rapidement.
Un module ne vaut rien s’il ne dispose pas de structures de données appropriée pour
manipuler (lire, stocker, écrire, …) ses données.
Les axiomes sont comme des règles d’inférence logiques qui régissent les opérations du type
défini.
Ce système d'axiomes doit être non contradictoire (consistance) et
d’une complétude suffisante.
Exemples :
Type Booléen
Opérations vrai : → Booléen
faux : → Booléen
non : Booléen → Booléen
et : Booléen × Booléen → Booléen
ou : Booléen × Booléen → Booléen
Préconditions
Ø
Axiomes soient a, b : Booléen
non (vrai) = faux
non (non (a)) = a
vrai et a = a
faux et a = faux
a ou b = non (non (a) et non (b))
Type Ensemble
Opérations
créer : → Ensemble
vide : Ensemble → Booléen
ajouter : Élément x Ensemble → Booléen
supprimer : Élément x Ensemble → Booléen
appartient : Élément x Ensemble → Booléen
cardinal : Ensemble → Entier
Préconditions Ø
Axiomes soient A, B : Ensemble,
si A = B alors A a les mêmes éléments que B et
B a les mêmes éléments que A,
si A = B alors cardinal(A) = cardinal(B),
si cardinal(A)=0 alors A= Ø (A est vide),
si A Ո B = Ø alors il n’existe aucun élément qui
appartient à la fois à A et à B.
Type Segment
Utilise Entier, Elément
Opérations
seg : Entier → Segment
changer_ième : Segment x Entier x Elément → Segment
ième : Segment x Entier → Elément
taille : Segment → Entier
Préconditions
seg (i) est_défini_ssi i ≥ 0
ième (s,i) est_défini_ssi 0 ≤ i < taille (s)
changer_ième (s,i,e) est_défini_ssi 0 ≤ i < taille (s)
Axiomes
soient i, j : Entier, e : Elément, s : Segment
si 0 ≤ i < taille (s) alors ième (changer_ième (s,i,e),i) = e
si 0 ≤ i < taille (s) et 0 ≤ j < taille (s) et i ≠ j alors ième (changer_ième (s,i,e),j) = ième (s,j)
taille (seg (i)) = i
taille (changer_ième (s,i,e)) = taille (s)
Le type Segment qui est défini ci-avant réutilise (via le mot-clé Utilise) des types déjà définis
(Entier et Elément).
La signature de Segment est donc l'union des signatures des types utilisés
(Entier et Elément) et qui est enrichie des nouvelles opérations.
Le type Segment hérite donc des propriétés des types qui le constituent.
4. IMPLEMENTATION D’UN TAD
1. Suppression d’éléments
Complexité défavorable (en temps) = O(m),
Composante principale = nombre de déplacements
2. Insertion d’éléments
Complexité défavorable (en temps) = O(m),
Composante principale = nombre de déplacements
4. Accès séquentiel
Complexité défavorable (en temps) = O(m),
Composante principale = nombre d’accès
5. Recherche
tab[1:m] non trié recherche séquentielle
Complexité défavorable (en temps) = O(m),
Composante principale = nombre de comparaisons
6. Fusion
Si 2 tableaux a[1:n] et b[1:m] à fusionner,
Complexité maximale en temps = O(n+m),
Composante principale = nombre d’accès.
Tableaux Multidimensionnels
Physiquement, ce sont les lignes ou les colonnes du tableau multidimensionnel qui sont
logées dans des blocs de mémoire successifs et contigus par le système d’exploitation.
Si donc w est la taille des éléments d’un tel tableau, alors l’adresse de l’élément
tab[k1,k2,k3, … ,kn] en mémoire est :
- en stockage colonnes : base(tab) + w * (((…(en tn-1 + en-1 )tn-2 + … + e3)t2 + e2)t1 + e1),
- en stockage lignes : base(tab) + w * (((… (e1 t2 + e2) t3 + e3) t4 + … + en-1) tn + en),
où ti = fi - di + 1,
ei = ki - di (indice effectif qui exprime le déplacement de ki par rapport à l’indice à la
position 1 et relativement dans l’intervalle (di:fi)).
Tableaux Parallèles
Les tableaux parallèles sont une collection des tableaux de même dimension et
de même taille où l’ensemble des éléments situés à la même position sur chacun de
ces tableaux spécifient une sémantique particulière et déterminée.
Ainsi, les n tableaux de dimension 1 t1[d1:f1], t2[d2:f2], …, tn[dn:fn] peuvent constituer des
tableaux parallèles s’ils sont de même taille. Dans ce cas, l’ensemble des éléments situés à
la même position relative i sur chacun de ces tableaux t1[i], t2[i], …, tn[i] définissent un
objet ou un concept particulier.
Par exemple, soient les 3 tableaux Nom[1:n] de type String, DNaiss[1:n] de type Date et
Pds[1:n] de type Réel, l’ensemble des données Nom[k], DNaiss[k] et Pds[k] désignent une
personne dont le nom est Nom[k], la date de naissance est DNaiss[k] et le poids est Pds[k].
Sous forme des tableaux parallèles, les informations concernant l’objet ou le concept
particulier sont donc éparpillées sur plusieurs tableaux.
Pour éviter cet éparpillement, on fait parfois appel à la notion d’enregistrement, un méta-type
complexe hiérarchique qui peut regrouper dans une seule structure un ensemble d'éléments
(champs) de types différents.
Le tableau est une structure de données basique qui peut permettre d’implémenter la plupart
des autres structures de données.
LISTE
Dans ces conditions, les données successives (ordonnées) de la liste n’ont plus besoin
d’occuper des positions adjacentes en mémoire vive.
La liste supporte aisément et naturellement les opérations d’insertion et de suppression de
données.
base(lst) = adresse du 1er élément (tête) de la liste dont le nom est lst
taille d’une liste = nombre de ses nœuds.
Une liste peut être représentée sous forme symbolique (ou mathématique),
native (ou abstraite) ou sous forme de tableaux parallèles (et dynamiques).
Exemple, la liste sous forme symbolique anl = (Lion,Oiseau,Chien,Canard) peut,
sous forme native, être :
base(anl)=59
Oiseau 36
45
et, sous forme des tableaux parallèles, être une configuration où l’on aura réservé 2
tableaux de même taille (NomAl[1:p] pour stocker le nom de chaque animal et
Suivant[1:p] pour stocker l’indice de l’animal suivant, avec p très grand), ainsi chaque
animal k est représenté par la paire NomAl[k] et Suivant[k].
liste monodirectionnelle = liste simple qui permet de chaîner ses éléments dans un seul sens
(p. ex, du premier élément que l’on a inséré dans la liste vers le
dernier élément)
liste multidirectionnelle = liste qui permet de chaîner ses éléments dans plusieurs sens
(p. ex, liste bidirectionnelle qui permet de chaîner ses éléments
dans 2 sens, particulièrement, de gauche à droite et de
droite à gauche)
Opérations - Soit une liste monodirectionnelle sous forme native dotée d’1 seul type de nœud
avec la configuration :
p info suiv
1. Construction de liste
Quelle que soit la forme, une liste est construite de proche en proche via des opérations
d’insertion (en début, en fin ou au milieu de la liste).
Complexité en temps d’une opération d’insertion en début de liste = O(constante).
2. Affichage de contenu d’une liste
Un algorithme possible permettant d’accéder successivement (par un accès séquentiel) à
chaque nœud de la liste est :
début
p ← base ()
tant que p≠nil faire afficher [Link]
p ← [Link]
fin
début
p ← base ()
t←0
tant que p≠nil faire t ← t+1
p ← [Link]
afficher t
fin
début
p ← base ()
tant que (p≠nil) et ([Link]≠c) faire p ← [Link]
si p=nil alors afficher «aucun nœud ne possède les critères c»
sinon afficher «noeud trouvé»
fin
Une liste circulaire X à en-tête vide est telle que base (X).suiv = base (X).
L’avantage principal d’une telle liste est d’accélérer certaines opérations et de les rendre
moins complexes, car :
- l’utilisation du pointeur nil n’est plus nécessaire,
- chaque nœud (ordinaire) possède un précédent et un suivant, et 1er nœud de la liste ne
constitue plus obligatoirement un cas particulier.
Liste Inversée
Dans une liste inversée, chaque sous-liste est généralement représentée par un tableau de
pointeurs accessible à partir de chaque nœud de la liste principale.
Une liste inversée est en fait une structure qui peut ainsi porter plusieurs dynamiques.
Remarque : Toutes les opérations qui ont été exécutées ci-dessus sur une
liste monodirectionnelle peuvent être8 aisément s’envisager, mutatis mutandis,
sur une liste circulaire à en-tête, sur une liste multidirectionnelle et
sur une liste inversée.
Gestion dynamique de la mémoire
Les diverses manipulations sur une liste (suppressions, désaffectations, allocations abusives)
Afin de contrôler et de gérer l’espace de mémoire vive, il faut une gestion dynamique de la
mémoire.
Cette gestion (qui peut être totalement prise en charge par le système d’exploitation ou
par le programme d’application qui manipule des listes) doit pouvoir :
1. marquer les nœuds encore utiles, au sein des listes, et
2. libérer tous les nœuds non marqués afin de libérer l’espace de mémoire qu’ils occupent.
Structure linéaire et dynamique d’une suite de données homogènes ou non, à laquelle il n’est
possible d’ajouter et de retirer des données qu’à une seule des extrémités (le sommet).
La pile est une queue lifo (Last In First Out) dans laquelle le dernier élément ajouté doit être
le premier à en être retiré.
Il existe des situations fréquentes dans la vie, dans la gestion, … où il est souhaitable de
restreindre les insertions et les suppressions de données à un seul bout de la structure de
gestion utilisée et pas ailleurs, sinon il apparaît instantanément une situation chaotique ou
indésirable (implémentation des appels de sous-programmes,
calcul des expressions arithmétiques, gestion de la récursivité, …).
Une pile peut être logiquement ou physiquement implémentée sous forme native,
sous forme d’une liste monodirectionnelle ou sous forme d’un tableau simple (et dynamique).
Par exemple, la pile sous forme native : pile contenant 5 cellules,
où chaque cellule de la pile est constituée d’un champ de
d données et d’un pointeur vers la cellule précédente,
a opérations natives : push, pop et nil pour une pile vide,
z
y
x
peut être représentée sous la forme d’un tableau simple par : un tableau pile[i:k]
adéquatement alloué, une variable qui doit servir à contenir l’adresse relative (indice) de
l’élément au sommet de la pile (p. ex, top) et une variable qui doit servir à indiquer le
nombre maximum d’éléments qui peuvent être accommodés par la pile (p. ex, max),
comme suit :
… x y z a d …
i j j+1 j+4 k
Structure linéaire et dynamique d’une suite de données homogènes ou non, dans laquelle les
insertions ne se font qu’à une extrémité (queue de la file) et les suppressions ne se font qu’à
l’autre extrémité (tête de la file). Elle permet de mettre en attente des données dans le but
de les récupérer plus tard, dans leur ordre d’arrivée (processus en attente pour exécution,
accès à un périphérique, …).
La file d’attente est une queue fifo (First In First Out) dans laquelle le premier élément ajouté
doit être le premier à en sortir. Elle ne donne accès qu’à un seul élément, la tête de la file.
Une file peut être logiquement ou physiquement implémentée sous forme native (ou
abstraite), sous forme d’une liste monodirectionnelle ou sous forme d’un tableau simple
(et dynamique).
Par exemple, la file suivante (avec 5 cellules), sous forme native :
tête d a z y x queue
avec comme opérations natives défiler (dequeue), enfiler (enqueue), nil pour une file vide,
où chaque cellule de la file est constituée d’un champ de données et d’un pointeur vers la
cellule suivante,
peut être représentée sous la forme d’un tableau simple par un tableau file[i:k]
adéquatement alloué, une variable qui doit servir à contenir l’adresse relative (indice) de
l’élément de tête de la file (p. ex, tête) et une variable qui doit servir à contenir l’adresse
relative (indice) de l’élément de queue de la file (p. ex, queue), comme suit :
… d a z y z …
i j j+1 j+4 k
Donc, après n insertions, le dernier élément de la file occupera une place file[j], même si
toutes les places devant ce dernier élément sont vides ou occupées par des données
obsolètes.
Si donc un nouvel élément doit être inséré dans une file qui n’est pas pleine mais pour
laquelle i<tête, il faudra :
- déplacer toute la file vers le début du tableau file[i:k],
- modifier, de façon adéquate, les variables tête et queue,
- insérer ce nouvel élément.
queue ← queue + 1
file[queue] ← x
3. Désenfilement d’un élément
Il s’agit de supprimer l’élément de tête de la file (accommodée par la portion file[i:k] du
tableau simple qui implémente cette file). Un algorithme possible de désenfilement doit
contenir la séquence suivante :
x ← file[tête]
tête ← tête+1
File d’attente dans laquelle un niveau de priorité (relatif à l’opération de défilement) est
affecté à chacun de ses éléments dans la file
(ex : file d’attente à niveaux de priorité d’un système d’exploitation multiprogrammé à temps
partagé).
Généralement, ce niveau de priorité conditionne le retrait de ces éléments selon les principes
suivants :
- un élément de priorité supérieure est traité avant tout élément de priorité inférieure,
- deux éléments de même priorité sont extraits et traités selon l’ordre dans lequel ils ont été
insérés dans la file d’attente.
File (d’attente) à niveaux de priorité
File d’attente dans laquelle les éléments peuvent être ajoutés ou supprimés à l’une ou
à l’autre extrémité (file dotée donc de 2 queues ou 2 têtes).
Variantes de deque :
- deque à entrée restreinte, qui n’accepte des insertions qu’à une seule de ses extrémités,
mais permet des extractions à ses 2 extrémités,
- deque à sortie restreinte, qui n’accepte des extractions qu’à une seule de ses extrémités,
mais permet des insertions à ses 2 extrémités.
Remarque : Toutes les opérations qui ont été exécutées ci-dessus sur une file peuvent,
mutatis mutandis, aisément s’envisager sur une file à niveaux de priorité et
sur une deque.
GRAPHE
Chemin = suite quelconque d’arêtes ou d’arcs d’un graphe, notée p=(u0, u1, u2, …, un), dont
les extrémités sont u0 et un, et telle que ui et ui+1 sont des nœuds adjacents pour
tout i (0 ≤ i ≤ n-1)
Chemin orienté = chemin dont les arcs sont tous orientés dans le même sens
Cycle = chemin p=(u0, u1, u2, …, un) fermé dans lequel u0 = un
Graphe orienté (diagraphe) = graphe composé d’arcs, chaque arc étant est repéré par une
paire ordonnée x=(ui,uk), ( (ui,uk) ≠ (uk,ui) ).
Dans l’arc x=(ui,uk), ui est l’origine (extrémité initiale) de x,
uk est la destination (extrémité terminale) de x,
ui est le prédécesseur de uk, uk est le successeur de ui.
Graphe unilatéralement connecté = graphe orienté dans lequel 2 nœuds ui et uk, quels qu’ils
soient, sont tels qu’il existe un chemin orienté de
ui vers uk ou de uk vers ui.
Graphe étiqueté = graphe dans lequel chaque arête ou arc porte un poids
Graphe connecté = graphe dans lequel 2 nœuds ui et uk, quels qu’ils soient, sont toujours
reliés par un chemin simple
Graphe connexe = graphe non orienté dans lequel tout nœud peut être relié à tout autre
nœud par une arête ou par un chemin, un tel graphe tient donc d’un seul
tenant.
Un graphe peut être logiquement ou physiquement implémenté (représenté) sous forme
native (ou abstraite), sous forme d’un tableau à 2 dimensions
(matrice d’adjacence ou matrice de contigüité),
sous forme d’une liste où chaque nœud ui est dotée de l’ensemble de ses nœuds successeurs
(liste simple, liste inversée ou tableaux parallèles).
Par exemple, le graphe orienté sous forme native suivant : a d
e
b c
Tous les nœuds d’un chemin dont l’origine est uk (c’est-à-dire tous les successeurs de uk) sont
chaînés en une seule suite derrière dest(succ(k)).
Ainsi, si départ=4, sous forme de tableaux parallèles, le graphe ci-dessus peut être implémenté
comme suit :
indice dest lnk
1. Recherche de nœud
Si z sont des critères de recherche, alors un algorithme possible de recherche du 1er nœud
du graphe qui répond à z est :
RechercheNoeud ()
début
i ← départ
tant que (i≠/) et (nœud[i] ne contient pas «z») faire i ← suiv[i]
si i=/ alors retourner «aucun nœud trouvé»
sinon retourner i
fin
2. Recherche d’arc
Afin de connaître l’emplacement ou de vérifier l’existence de l’arc (ui,uk), il faudra, étant
donné l’indice i du nœud ui et l’indice k du nœud uk, chercher uk parmi les successeurs
de ui. Un algorithme possible de recherche de cet arc est :
RechercheArc ()
début
i ← RechercheNoeud () (1)
k ← RechercheNoeud () (2)
si (i=/) ou (k=/) alors retourner /
sinon retourner s ← RechercheNoeud () (3)
fin
InsertionNoeud ()
début
i ← ChercherIndiceLibre (nœud[:]) (1)
si i = / alors renvoyer «Insertion impossible du nœud»
sinon nœud [i] ← uk
suiv [i] ← départ
succ[i] ← /
départ ← i
renvoyer «Insertion accomplie»
fin
InsertionArc ()
début
i ← RechercheNoeud ()
k ←RechercheNoeud ()
j ← ChercherIndiceLibre (dest[:]) (1)
si j=/ alors renvoyer «Insertion impossible de l’arc»
sinon dest (j) ← k
lnk (j) ← succ (i)
succ (i) ← j
renvoyer «Insertion accomplie»
fin
Nettoyage (nœud[:],suiv[],départ,uk,z)
début
z ← faux
si départ≠/
alors si nœud[départ] = uk alors p ← départ; départ ← suiv[départ]; z ← vrai
sinon p ← suiv[départ]; s ← départ
tant que (p≠/) et (z=faux) faire si nœud [p] = uk
alors suiv [s] ← suiv [p]
z ← vrai
s←p
p ← suiv [p]
fin
alors pour supprimer le nœud uk du graphe, il suffit de :
- trouver l’indice k de ce nœud uk,
- supprimer tous les arcs dont le nœud uk est la destination (c’est-à-dire supprimer le nœud uk
dans la suite des successeurs de chaque nœud du graphe),
- supprimer tous les arcs qui sont sur les chemins dont le nœud uk est l’origine
(c’est-à-dire trouver l’indice d du 1er successeur du nœud uk et l’indice f du dernier
successeur du nœud uk de tous ces chemins),
comme suit :
SuppressionNoeud ()
début
z ← faux
k ← RechercheNoeud ()
si k≠/ alors p ← départ
tant que p≠/ faire Nettoyage (dest[:],lnk[:],succ[p],k,z) (1)
p ← suiv[p]
(1) : suppression du nœud[k] du tableau dest[:] en commençant la recherche de ce nœud[k] à partir de la position
succ[p]. Tous les arcs dont le nœud uk est la destination seront ainsi supprimés
(2) : suppression de tous les arcs qui sont sur les chemins dont le nœud u k est l’origine
6. Parcours séquentiel de graphe
Opération pour accéder séquentiellement à tous ou à une grande partie de nœuds d’un
graphe afin d’y appliquer un (même) traitement.
2 manières principales d’exécuter ce parcours :
- parcours par contagion, qui utilise une file d’attente pour ranger les nœuds qui sont en
attente du traitement,
- parcours par sondage, qui utilise une pile.
Quel que soit le parcours, tout nœud uk du graphe sera dans l’un des 3 états :
- état «prêt» : état initial de uk,
- état «attente» : état d’attente de uk dans la file d’attente ou dans la pile,
- état «traîté» : état final d’un noeud uk traité.
Un algorithme possible du parcours par sondage est :
début
mettre tous les nœuds de G à l’état «prêt»,
empiler le nœud de départ uk dans la pile Q et faire passer son état à «attente»,
tant que pile Q non vide faire désempiler le nœud um,
traiter le nœud um,
faire passer l’état du nœud um à «traité»,
empiler les nœuds voisins du nœud um (ceux qui sont à l’état «prêt») dans la pile,
et faire passer leur état à «attente»
fin
Exemple : soit un graphe orienté représentant les vols quotidiens d’une compagnie
aérienne entre diverses villes,
w
f c b
d z g
j k
- empiler j Q : j
- désempiler j, traiter j et empiler ses voisins Q : d, k
- désempiler k, traiter k et empiler ses voisins Q : d, z, g
- désempiler g, traiter g et empiler ses voisins Q : d, z, c (le nœud voisin z du nœud g a déjà été empilé)
- désempiler c, traiter c et empiler ses voisins Q : d, z, f
- désempiler f, traiter f et empiler ses voisins Q : d, z
- désempiler z, traiter z et empiler ses voisins Q : d (les nœuds voisins c et j du nœud z ont déjà été traités)
- désempiler d, traiter d et empiler ses voisins Q : nil.
La pile Q est vide, les nœuds du graphe accessibles à partir du nœud j sont :
j, k, g, c, f, z et d
Tri Topologique
Soit un graphe G=(V,E) dans lequel chaque arc exprime une relation hiérarchique, de
précédence ou de préséance.
Par exemple, un graphe dans lequel chaque nœud uk représente une tâche et chaque arc
(ui,uk) spécifie une relation de précédence signifiant que l’achèvement de la tâche ui est un
préalable (représenté par ‘<‘) pour que la tâche uk démarre.
Une telle relation permet de trier topologiquement le graphe G qui est alors comparable à un
ensemble partiellement ordonné.
Un tri topologique de G fournit donc un ordonnancement linéaire de ses nœuds.
Si le graphe G est un graphe orienté fini sans circuit alors il existe un tri topologique T de G
dont l’idée principale tourne itérativement autour de 2 opérations basiques :
- trouver un nœud uk tel que D-(uk)=0, et
- supprimer le nœud uk et ses arcs.
Une procédure de tri topologique dans laquelle une file d’attente est utilisée pour ranger
temporairement les nœuds dont le degré intérieur D-()=0 est :
Procedure TriTopologique ()
début
pour tout nœud uk, calculer D-(uk),
mettre dans queue tout nœud ui dont D-(ui) = 0,
tant que queue non vide faire extraire nœud um de queue
pour chaque voisin up de um répéter
D-(up) ← D-(up) - 1
si D-(up) = 0 alors mettre up dans queue
fin
ARBRE
Graphe G=(V,E), simple non orienté, connexe et sans cycle, dans lequel :
- l’ajout d’une arête quelconque crée un cycle,
- G n’est plus connecté si l’on retire n’importe qu’elle arête,
- tout couple de nœuds de G sont reliés par un chemin unique.
Formellement, un arbre est une structure des données non linéaire qui est surtout utilisée
pour des informations dont les éléments sont liés par des relations hiérarchiques
(tables de matières, index, répertoires, arbres généalogiques, …).
Mathématiquement, un arbre T = ensemble fini, éventuellement vide, de nœuds (dont un
nœud particulier ur appelé racine) dans lequel :
- tout nœud ui (sauf la racine) possède un nœud parent (prédécesseur) up d’où est
directement issu ui,
- tout nœud ui peut avoir des nœuds enfants (successeurs) us qui proviennent
directement de ui.
Les nœuds qui n’ont pas de nœud enfant sont des feuilles.
Tous les autres nœuds de l’arbre (sauf sa racine) sont des nœuds descendants de ur,
c’est-à-dire qu’ils sont issus directement ou indirectement de ur.
2 arbres T et V sont similaires s’ils ont la même structure avec le même nombre et le même
type d’enfants à chaque sous arbre.
2 arbres T et V sont des copies, s’ils sont similaires et s’ils présentent le même contenu
aux nœuds correspondants.
Arbres Remarquables
* Arbre Binaire
T est un arbre binaire si, étant donné la racine ur de T, les autres nœuds forment une paire
ordonnée de sous-arbres disjoints T1 et T2 avec
T1 = sous-arbre de gauche de ur et T2 = sous-arbre de droite de ur.
La racine de T1, si elle existe, est le successeur gauche de ur,
la racine de T2, si elle existe, est le successeur droit de ur.
Tout nœud uk d’un arbre binaire T possède donc 2 successeurs au maximum, et tout nœud
doté d’un ou de 2 enfants est un nœud interne, tout nœud sans enfant est
un nœud externe.
Exemple : l’expression algébrique e=(a-b) / ((c*d)+e) peut être représentée par un arbre
binaire où les opérateurs sont dans les nœuds internes et les opérandes sont des
nœuds externes :
- +
a b * e
c d
* Arbre Binaire Complet
Arbre binaire où : - chaque niveau, sauf éventuellement le dernier, comporte 2 nœuds,
- tous les nœuds terminaux qui apparaissent au dernier niveau et qui
sont seuls, sont à gauche.
Donc, il n’existe qu’un seul arbre complet T comportant exactement n nœuds.
Profondeur d’un arbre binaire complet doté de n nœuds = (log2n) +1.
Exemple : le seul arbre binaire complet comportant 12 nœuds se présente comme suit :
2 3
4 5 6 7
8 9 10 11 12
* Arbre Binaire Ordonné
Arbre binaire complet où tout nœud ui possède une des propriétés suivantes :
- la valeur de ui est supérieure ou égale à celle de chacun de ses enfants (et donc à celle de
tous ses descendants) arbre binaire ordonné est une maxipile,
- la valeur de ui est inférieure ou égale à celle de chacun de ses enfants (et donc à celle de
tous ses descendants arbre binaire ordonné est une minipile.
97
88 95
77 55 95 48
77 35 48 55 62
* Arbre Binaire Etendu (Pair)
Arbre binaire dans lequel chaque nœud possède 0 ou 2 enfants (dans un tel arbre binaire
chaque nœud interne est doté de 2 enfants).
Tout arbre binaire peut être transformé en arbre binaire étendu :
- en remplaçant chaque sous-arbre vide par un nœud nouveau,
- en ajoutant 2 nœuds externes aux nœuds terminaux.
Exemple : l’arbre binaire peut être transformé en l’arbre binaire étendu en ajoutant des nouveaux arcs (en pointillé)
9 9
1 5 1 5
4 7 4 N 7 N
3 N N N 3
N N
Soit T un arbre binaire étendu où :
x = nombre de nœuds externes,
i = nombre de nœuds internes,
li = somme de toutes les longueurs de chemin j allant de la racine vers chaque
nœud interne,
le = somme de toutes les longueurs de chemin j allant de la racine vers chaque
nœud externe,
alors x = i + 1 et le = li + (2 * i).
Lorsque l’on affecte un poids (non négatif) wi à chaque nœud externe, on peut calculer
la longueur p de chemin pondérée en vue de résoudre efficacement une classe de
problèmes de recherche modélisés à l’aide de l’arbre binaire.
Le problème de recherche des arbres ayant une longueur de chemin pondérée minimum se
pose dans beaucoup de domaines et l’algorithme de Huffman, dans la plupart de cas, a été
une réponse à ce problème.
Le parcours d’un tel arbre dans un certain ordre fournira une liste ordonnée de ses nœuds.
* Arbre Généralisé
Un arbre généralisé est simplement un arbre T, un ensemble non vide de nœuds ayant :
- une racine, et
- des sous-arbres disjoints T1, T2, …, Tm, et où il n y a ni enfant de gauche,
ni enfant de droite.
50 et 50
22 30 22 30
Par exemple, pour implémenter un arbre binaire T sous la forme de tableau unique, on peut
allouer le tableau arbre[1:n] où :
- la racine ur de l’arbre binaire sera rangée en arbre[1],
- si un nœud est en arbre[k], alors son enfant de gauche sera rangé en arbre[2k] et son
enfant de droite sera rangé en arbre[2k+1].
La représentation d’un arbre de profondeur d sous cette forme nécessite un tableau dont la
taille est 2d.
Par exemple, pour implémenter un arbre binaire T sous la forme de tableaux parallèles,
on peut allouer les tableaux :
- info[1:n], pour contenir les informations des nœuds,
- left [1:n], pour contenir les indices des enfants qui sont à gauche,
- right [1:n], pour contenir les indices des enfants qui sont à droite.
- rac, une variable pour contenir l’indice de la racine de l’arbre.
Ces opérations peuvent être aisément étendues, mutatis mutandis, sur n’importe quelle autre
implémentation et pour n’importe quel arbre généralisé.
1. Parcours séquentiel
Lorsqu’il faut appliquer un traitement à tous les nœuds ou à une grande partie des nœuds
d’un arbre binaire T de racine ur, on peut utiliser 3 accès séquentiels standardisés :
+ :
a - - -
b c d e + h
f g
début
empiler ‘/’ sur la pile Q
p ← rac
tant que p ≠ / faire traiter info [p]
si right [p] ≠ / alors empiler right [p]
6 20
Il n y a qu’un seul endroit possible où cette insertion peut s’effectuer et le nouvel arbre de
recherche binaire sera : 10
6 20
8
5. Suppression dans un arbre binaire de recherche
La manière dont le nœud uk doit être supprimé d’un arbre binaire de recherche T dépend
du nombre d’enfants de ce nœud uk :
a. si uk n’a pas d’enfants, uk est alors supprimé de l’arbre, et l’adresse de uk dans le nœud
parent de uk est remplacée par /,
b. si uk a 1 seul enfant ue, uk est supprimé de l’arbre, et l’adresse de uk dans
le nœud parent de uk est remplacée par celle de l’enfant unique ue,
c. si uk a 2 enfants, soit alors us le successeur en parcours in-ordre de uk
(ce qui signifie que us n’a pas d’enfant à sa gauche), il faut alors :
- écarter temporairement ce nœud successeur us (en appliquant le cas 1 ou le cas 2
ci-dessus),
- remplacer le nœud uk par us.
L’algorithme de suppression de nœud dans un arbre de recherche binaire peut donc être
efficacement spécifié comme 2 procédures :
SuppressionZeroOuUn (info[], left[], right[], rac, sc, psc) suppression temporaire du successeur
‘in-ordre’ du nœud uk
si pt ≠ nil alors si left [pt] = k alors left [pt] ← sc
sinon right [pt] ← sc
sinon rac ← sc
left [sc] ← left [k] les 2 anciens enfants du nœud uk deviennent les nouveaux enfants de son
right [sc] ← right [k] successeur
fin
Finalement, l’algorithme de suppression du nœud uk, présentant un critère donné x, dans
un arbre de recherche binaire peut alors formellement s’écrire :
fin
6. Construction d’arbre
Tout arbre se construit par insertion de nœuds.
Tout arbre binaire ordonné se construit par insertion, migration et permutation de nœuds
en gardant l’arbre complet.
Par exemple, soit la maxipile : 60 dans laquelle le nœud 77 doit être inséré. Les étapes 3 sont :
50 55
22 30 44
insertion : 60
50 55
22 30 44 77
50 77
22 30 44 55
50 60
22 30 44 55
5. CONCEPT D’ABSTRACTION
Implémentation du cahier des charges d’un TAD écriture des algorithmes performants et
efficaces.
Complexité en temps et en espace des algorithmes = fonction f(n1, n2, … ), où n1, n2, …
sont les tailles des données à traiter
supposées croître indéfiniment.
Comme la fonction f(n1, n2, … ) doit tenir compte de la distribution des données et d’autres
paramètres statistiques de manière pratique, cette fonction f est comparée à
une fonction de référence via la fonction O( ), qui se lit «Ordre de».
6. ASPECTS GENERAUX ET BASIQUES DE TRI
Tri = opération vitale pour des processus de recherche et de traitement en masse exigeant
que les données à traiter soient triés (respect des spécifications, garantie des temps de
réponse et des performances acceptables).
- tri non comparatif, classe qui tient compte de la structure des éléments qui sont complexes