Explorer les Livres électroniques
Catégories
Explorer les Livres audio
Catégories
Explorer les Magazines
Catégories
Explorer les Documents
Catégories
4.1 Introduction
Tout programme utile à besoin de données pour fonctionner. Quand ces données ont une
structure particulière, il est judicieux d'en tenir compte an d'obtenir des programmes
concis et ecaces. Ainsi, si des données se présentent sous la forme d'une hiérarchie
d'éléments, ça serait pure perte d'information, (et peut être aussi de temps de calcul),
de les considérer comme une simple collection d'éléments sans aucun lien entre eux. Par
ailleurs, les structures de données présentées dans les chapitres précédents sont de simples
structures linéaires, à l'exception des tableaux multi-dimensionnels, qui n'en demeurent
pas moins linéaire au niveau de chaque dimension.
Dans le présent chapitre, nous étudions les structures de données arborescentes
, qui
interviennent de manière naturelle dans beaucoup de problèmes concrets. Les structures
arborescentes rompent avec la linéarité an de représenter, de manière adéquate, des
données organisées selon une hiérarchie, où il y a, par exemple, un chef, plusieurs sous-
chefs qui sont au même niveau de la hiérarchie, pour terminer avec beaucoup d'ouvriers
qui sont, également, au même niveau de la hiérarchie.
Par ailleurs, le langage C met à la disposition du programmeur les outils nécessaires
pour implémenter des structures de données arborescentes et ceci à l'aide des notions de
pointeur et destructure étudiées dans le Chapitre 3. Dans ce qui suit, nous étudions et
implémentons, en langage C, les arborescences binaires ordonnées puis les arbres binaires
de recherche (ABR), pour terminer avec les tas-min et les tas-max. Mais, tout d'abord,
commençons par rappeler quelques notions de la théorie des graphes qui sont nécessaires
pour la clarté de notre exposée.
Nous commençons par rappeler la dénition d'un graphe simple, dont les structures arbo-
rescentes sont des cas particuliers.
1
2 CHAPITRE 4. STRUCTURES DE DONNÉES ARBORESCENTES
Dénition 1. Un graphe simple est un couple (S, A), où S est un ensemble ni
d'éléments appelés sommets et A ⊆ est un ensemble d'éléments appelés arêtes .
S
2
a
a. S
2 désigne l'ensemble de toutes les paires de S.
Les graphes sont très souvent représentés graphiquement d'une manière qui reète plus
leurs structures. Dans la Figure 4.1, on peut voir la représentation graphique d'un graphe
simple composé de six sommets et huit arêtes.
Une chaîne, entre deux sommets s et s0 d'un graphe simple (S, A) est une séquence de
sommets (s1 , . . . , sp ) telle que : s1 = s, sp = s0 et {si , si+1 } ∈ A, pour tout i : 1, . . . , p − 1.
La longueur d'une chaîne est le nombre d'arêtes formant la chaîne. Un cycleest une
chaîne dont les deux extrémités sont identiques. Un graphe simple est dit connexe s'il
existe une chaîne entre toute paire de sommets du graphe.
r
prof(b)=2
a e haut(r)=3
b c f i
sous-arbre enraciné en a
d g h
Figure 4.2 Un arbre enraciné. La racine est le n÷ud r. Les n÷uds internes sont sur
fond maron clair et les feuilles sont sur fond vert clair.
un arbre enracinés qui est formé d'un n÷ud racine, d'un sous-arbre enraciné
binaire ordonné gauche et un sous-arbre enraciné binaire ordonné droit.
a. Il s'agit de l'arbre enraciné vide
Une particularité essentielle des arbres enracinés binaires ordonnés est que dans ces
arbres, on fait la distinction entre le ls gauche et le ls droit d'un même n÷ud, même si
l'un des deux est absent. En fait, le qualicatif ordonné
est dû à cette distinction. Ainsi, les
deux arbres enracinés binaires ordonnés de la Figure 4.3 sont considérés comme diérents,
4 CHAPITRE 4. STRUCTURES DE DONNÉES ARBORESCENTES
r r
a e a e
b c f i b c i f
d g h d g h
La technique de chaînage utilisée pour implémenter les listes chaînées, (voir Chapitre 3),
peut être enrichie pour stocker toute collection homogène et structurée d'éléments ; et
c'est le cas des arbres enracinés. Dans ce qui suit, nous nous limitons à l'implémentation
des arbres enracinés binaires ordonnés et ceci à l'aide de structures de données utilisant
la technique de chaînage.
Le point de départ dans l'implémentation d'un arbre enraciné binaire ordonné est
l'implémentation de la composante élémentaire de tout arbre, à savoir, le n÷ud. Pour ce
faire, on utilise une structure comportant quatre champs : un champs pour mémoriser
l'information portée par le n÷ud et trois autres champs qui vont respectivement pointer
sur le parent, le ls gauche et le ls droit du n÷ud (voir le Programme 2). Les branches de
l'arbre enraciné binaire ordonné sont implicitement représentées par les chaînages entre
les n÷ud parents et les n÷ud ls. Pour le n÷ud racine de l'arbre, qui n'a pas de parent,
4.3. ARBORESCENCES BINAIRES ORDONNÉES 5
.. noeud->elt = elt;
.
#dene alloc(t) (t*) malloc(sizeof(t)) noeud->parent = parent;
typedef struct sNoeud noeud->fg = fg;
{ noeud->fd = fd;
tElt elt; return noeud;
struct sNoeud *parent; }
struct sNoeud *fg; int main()
struct sNoeud *fd; {
..
} tNoeud; .
tNoeud *noeud;
tNoeud *InitNoeud(tNoeud *noeud, noeud = alloc(tNoeud);
tElt elt, InitNoeud(noeud, elt, NULL, NULL,
tNoeud *parent, NULL);
tNoeud *fg, ..
.
tNoeud *fd) }
{
Programme 1: Dénition du type générique tNoeud, déclaration d'une variable
pointeur sur tNoeud et création puis initialisation d'un nouveau n÷ud.
le champ parent aura la constante NULL comme valeur. Pareillement, si l'un des ls d'un
n÷ud n'existe pas alors le pointeur correspondant aura la constante NULL comme valeur
et si l'arbre est vide alors il sera représenté par le pointeur NULL.
Pour ce qui est de parcourir tous les n÷uds d'un arbre enraciné binaire ordonné, (on parle
aussi d'explorer l'arbre en sa totalité), on distingue deux parcours classiques :
le parcours en profondeur d'abord et
le parcours en largueur d'abord .
Le parcours en profondeur d'abord admet, à sont tour, trois variantes qui diérent par
l'ordre de traitement du n÷ud par rapport au traitement des sous-arbres enracinés en ses
ls. On distingue alors :
Le parcours préxé, où on traite le n÷ud courant d'abord, le sous-arbre gauche,
puis le sous-arbre droit.
Le parcours post-xé, où on traite le sous-arbre gauche, le sous-arbre droit puis le
n÷ud courant.
Le parcours inxe, où l'on traite le sous-arbre gauche, le n÷ud courant puis le
sous-arbre droit.
Algorithmiquement, la manière la plus concise et la plus élégante permettant d'eec-
tuer un parcours en profondeur d'abord, avec ses trois variantes, est de recourir à des
procédures récursives, qui s'appelle elle même à deux reprises an de traiter le sous-arbre
gauche et le sous-arbre droit de chaque n÷ud. Les trois premières procédures du Pro-
gramme 2 sont dédiées aux trois variantes du parcours en profondeur d'abord.
Le parcours en largeur d'abord permet de parcourir les n÷uds d'un arbre enraciné
6 CHAPITRE 4. STRUCTURES DE DONNÉES ARBORESCENTES
if (root)
void Prexe(tNoeud *root) {
{ Inxe(root->fg);
if (root) Traiter(root->elt);
{ Inxe(root->fd)
Traiter(root->elt); }
Préxe(root->fg); }
Préxe(root->fd) void LargeurDabord(tNoeud *root)
} {
} tFile F;
void Postxe(tNoeud *root) InitFile(F);
{ Emler(F,root);
if (root) while (!FileVide(F))
{ {
Postxe(root->fg); root = Deler(F);
Postxe(root->fd); if (root)
Traiter(root->elt) {
} Traiter(root->elt);
} Emler(F,root->fg);
void Inxe(tNoeud *root) Enler(F,root->fd);
{ }
}
}
Programme 2: Les trois variantes du parcours en profondeur d'abord d'un arbre
binaire ordonné, plus le parcours en largeur d'abord d'un arbre enraciné binaire
ordonné. Ce parcours a été rendu possible grâce à l'utilisation d'une le.
4.3. ARBORESCENCES BINAIRES ORDONNÉES 7
1 1
2 6 2 3
3 4 7 10 4 5 6 7
5 8 9 8 9 10
Figure 4.4 Ordre d'exploration des n÷uds d'une arborescence par un parcours en
profondeur d'abord (gauche) vs
par un parcours en largueur d'abord (droite).
suivant un ordre qui dière de celui suivi lors d'un parcours en profondeur d'abord (voir
Figure 4.4). Il s'agit de parcourir l'arbre niveau par niveau, de la racine jusqu'aux feuilles.
Les n÷uds d'un même niveau sont explorés de gauche à droite. Ainsi, les n÷uds qui se
trouvent à une même profondeur sont explorés de manière successive. Par conséquent,
tous les n÷uds se trouvant à une profondeur p doivent avoir été traités avant que le
premier n÷ud se trouvant à la profondeur p + 1 ne le soit. Un tel parcours suppose que
l'on dispose d'un moyen pour se souvenir de l'ensemble des n÷uds qui appartiennent à un
même niveau et qui ne sont pas encore traités. Pour ce faire, le plus adéquat est d'utiliser
une le, comme c'est le cas dans le Programme 2. La structure de le est appropriée pour
le parcours en largeur d'abord comme il a été décrit ci-dessus, car les règles d'accès à une
le assurent que les n÷uds qui entrent dans la le en premier seront traités en premier ; et
ceci est compatible avec une exploration par niveau où les n÷uds de chaque niveau sont
explorés de gauche à droite.
Exercice 4. Les arbres de Fibonacci sont des arbres enracinés binaires ordonnés
qu'on peut dénir comme suit : L'arbre se réduisant à un seul n÷ud est l'arbre de
Fibonacci A . L'arbre composé d'une racine et de deux ls est l'arbre de Fibonacci
A . L'arbre de Fibonacci A , pour h ≥ 2, est l'arbre enraciné binaire ordonné dont
0
Les ABRs peuvent être implémentés de la même façon que les arbres enracinés binaires
ordonnés quelconques, c'est-à-dire, en utilisant la technique de chaînage. On peut donc
4.4. ARBRES BINAIRES DE RECHERCHE (ABR) 9
5 4
2 9 2 8
1 4 7 10 1 3 6 9
3 6 8 5 7 10
Figure 4.5 Deux ABRs distincts qui stockent les entiers de 1 à 10.
utiliser le type tNoeud déni dans le Programme 1 pour implémenter les n÷uds et les
branches d'un ABR.
Pour le parcours total d'un ABR, il est aussi possible d'utiliser les procédures détaillées
dans le Programme 2. Toutefois, il y a parmi ces diérents parcours, un qui est particuliè-
rement adapté au parcours d'un ABR. Il s'agit du parcours en profondeur d'abord dans sa
variante inxe (voir la procédure Inxe du Programme 2). Ce parcours permet de traiter
les éléments stockés dans un ABR suivant l'ordre sous-jacent à leur stockage. Ce parcours
permet, par exemple, d'acher les éléments stockés dans un ABR dans l'ordre croissant.
On peut aussi avoir besoin d'eectuer un parcours partiel dans un ABR an de dé-
terminer le plus petit ou le plus grand élément qui se trouve dans l'ABR. Il sut alors
de parcourir la chaîne la plus à gauche de l'ABR pour trouver le plus petit élément au
bout de cette chaîne. Pour le plus grand élément, c'est plutôt la chaîne la plus à droite
de l'ABR qu'il faut parcourir (voir le code de la Figure 4.7).
Proposition 2.
La recherche d'un élément donné dans un ABR se fait en O(h), où h désigne
la hauteur de l'ABR.
L'accès à l'élément minimal et maximal dans un ABR se fond en O(h), où
h désigne la hauteur de l'ABR.
10 CHAPITRE 4. STRUCTURES DE DONNÉES ARBORESCENTES
if return
2 {
if
4
−> e l t )
return
5 ( elt < root
−>f g
else
6 ChercheABR ( r o o t , elt ) ;
return
7
9 }
10
while
12 {
−> e l t )
if
13 ( root && elt != root
else
15 root =
16
−>f d ;
return
17 root = root
18 root ;
19 }
1 tNoeud ∗MinABR ( t N o e u d ∗ r o o t )
2 {
while
4
5 ( root )
6 {
7 pred = root ;
return
9 }
10 pred ;
11 }
12
13 t N o e u d ∗MaxABR( t N o e u d ∗ r o o t )
14 {
while
16
17 ( root )
18 {
19 pred = root ;
return
21 }
22 pred ;
23 }
Exercice 5. Écrivez une fonction C qui reçoit, comme paramètre, un pointeur sur
la racine d'un ABR et retourne la hauteur de ce dernier.
Exercice 6.
Écrivez une fonction C qui reçoit, comme paramètre, un pointeur sur la ra-
cine d'un ABR et un entier k et retourne un pointeur sur le n÷ud qui contient
le kième élément, selon l'ordre croissant des éléments, de l'ABR.
On s'intéresse, à présent, à localiser le n÷ud médian d'un ABR, c'est à dire
celui qui contient l'élément qui, dans la liste des éléments en ordre croissant,
se trouve à la position (n + 1) div 2, où n est le nombre d'éléments de l'ABR.
Proposez une fonction C qui permet de localiser le n÷ud médian à partir d'un
pointeur sur la racine d'un ABR.
L'avantage des structures dynamiques, tels que les ABRs, c'est qu'il est possible de de-
mander de l'espace mémoire supplémentaire ou d'en libérer pendant l'exécution du pro-
gramme. Cette dynamique est le résultat d'ajouts et de suppressions d'éléments à/de la
collection d'éléments. Pour les ABR, c'est deux opérations doivent être exécutées de sorte
à conserver la principale propriété des ABR (voir la Propriété 1).
La fonction InsereABR(.,.) détaillée dans la Figure 4.8 prend comme paramètre
un pointeur sur la racine d'un ABR et un nouvel élément de type générique tElt et
retourne un pointeur sur la racine, éventuellement changée, de l'ABR. InsereABR(.,.)
permet d'insérer le nouvel élément dans une position de sorte à conserver la propriété
fondamentale des ABR (voir la Propriété 1). Pour introduire le moins de changement
possible sur la structure de l'ABR, le nouvel élément sera stocké dans une nouvelle feuille
de l'ABR.
An de localiser la position où le nouvel élément doit être inséré, la fonction InsereABR
procède comme suit : A l'aide de deux pointeurs pred et cour, on parcours un chemin de
l'arbre, qui part de la racine, à la recherche d'un n÷ud qui a zéro ou un ls. Au niveau
de chaque n÷ud traversé sur le chemin, seul un de ses ls sera exploré. Le choix entre
les deux ls est décidé en comparant le nouvel élément à insérer, et l'élément stocké dans
le n÷ud courant. Si le nouvel élément n'est pas plus grand que l'élément stocké dans le
n÷ud courant alors le parcours bifurque vers le ls gauche, sinon le parcours bifurque
vers le ls droit. Le rôle du pointeur pred est de sauvegarder l'adresse du n÷ud que la
fonction InsereABR(.,.) atteindra au bout du chemin. Le nouvel élément sera inséré
dans un nouveau n÷ud nouv, qui sera le ls du n÷ud se trouvant au bout du chemin
exploré (pred). Si le nouvel élément n'est pas plus grand que l'élément stocké dans pred
alors nouv sera un ls gauche sinon il sera un ls droit. Remarquez que, dans tous les cas,
le nouveau n÷ud sera un n÷ud feuille.
12 CHAPITRE 4. STRUCTURES DE DONNÉES ARBORESCENTES
2 {
5 pred = NULL ;
6 cour = root ;
while
7
8 ( cour )
9 {
if
10 pred = cour ;
else
12 cour =
13
16
18
if
19
else
21 root = cour ;
if
22
25
return
27
28 root ;
29 }
La position prise par nouv dans l'ABR lui garantit le fait que, pour tout ancêtre anc
de nouv, ce dernier descendra du ls gauche de anc si l'élément qu'il stocke, (le nouvel
élément), n'est pas plus grand que l'élément stocké dans anc. Sinon nouv descendra du
ls droit de anc. C'est cette propriété qui garantit que l'arbre gardera sa propriété d'ABR
après insertion.
5 5
2 9 2 9
1 4 7 10 1 3 7
3 6 8 6 8
la Figure 4.11, fait appel à la fonction ChercheABR(.,.), (voir Figure 4.6). Cet appel
renvoi l'adresse du n÷ud, adr, contenant l'élément à supprimer. On distingue alors trois
cas, qui sont les suivants, (voir aussi l'illustration dans les Figures 4.9 et 4.10) :
Si le n÷ud adr n'a pas d'enfant alors il sut de faire pointer sur NULL, le champs
(fg ou fd) du parent d'adr qui pointe sur ce dernier.
Si le n÷ud adr n'a qu'un seul ls alors ce ls prendra la place de son parent. Il
sut, alors, de faire pointer le champ du parent d'adr, qui pointe sur ce dernier,
sur l'unique ls d'adr.
Enn, si le n÷ud adr a deux ls alors ce n÷ud sera désormais occupé par le
prédécesseur immédiat, selon l'ordre sous-jacent à l'ABR, de l'élément à supprimer.
Ceci veut dire que le n÷ud adr ne sera pas physiquement supprimé de l'arbre,
mais c'est son contenu qui va changer. Notez que le prédécesseur immédiat de
l'élément à supprimer est le plus grand élément du sous-arbre ayant pour racine
le ls gauche du n÷ud adr. Le n÷ud contenant cet élément peut être localisé à
l'aide de la fonciton MaxABR(.). Une fois localisé, ce n÷ud doit être physiquement
supprimé, car l'élément qu'il contient va être sauvegardé dans le n÷ud adr. Une
autre propriété du n÷ud qui contient le prédécesseur immédiat c'est qu'il a, au plus,
un ls et donc sa suppression physique sera plus facile que celle du n÷ud adr, qui
lui à deux ls. On procède alors à la localisation du prédécesseur immédiat par
un appel à la fonction MaxABR(.) à partir du ls gauche de adr (voir Ligne 10
du programme de la Figure 4.11). Enn, puisque le n÷ud qui sera physiquement
supprimé a, au plus, un ls, on se retrouve, désormais, dans un des deux cas
précédant. Ces deux cas sont traités par les lignes de code de 15 à 24 de la fonction
SupprimeABR(.,.), (voir Figure 4.11).
Il existe une autre opération très utile qu'on peut appliquer sur un ABR ; il s'agit de
larotation d'ABR. Une rotation consiste à restructurer un ABR en faisant remonter
certains n÷uds et à en faire descendre d'autres. Dans la plus part du temps, le but d'une
telle man÷uvre est d'équilibrer l'ABR en diminuant sa hauteur. Ceci peut avoir pour
eet l'accélération de la recherche d'élément dans cet ABR. En eet, quand un sous-
arbre d'un ABR subit une rotation, la hauteur de l'un de ses deux sous-arbres augmente
14 CHAPITRE 4. STRUCTURES DE DONNÉES ARBORESCENTES
5 6
2 9 2 9
1 4 7 10 1 4 7 10
3 6 8 3 8
2 {
4 adr = ChercheABR ( r o o t , e l t ) ;
if
5
6 ( adr )
if
7 {
9 {
12 adr = ptr ;
13 }
17 ( fls ) f l s −>p a r e n t = p t r ;
if
18
if
19 ( ptr )
20 ( ptr −>f g == a d r )
−>f g = f l s ;
else
21 ptr
22
−>f d
else
23 ptr = fls ;
24
25 root = fls ;
26
27 f r e e ( adr ) ;
28 }
return
29
30 root ;
31 }
Exercice 8. Proposez une fonction C qui reçoit, comme paramètre, un pointeur sur
la racine d'un ABR et procède à l'équilibrage de ce dernier. Précisons qu'équilibrer
un ABR veut dire le restructurer de façon à minimiser sa hauteur.
Propriété 3. Dans un tas-min, un élément stocké dans un n÷ud ne peut être plus
grand que les éléments stockés dans ses n÷ud ls.
Si c'est un tas-max, on doit avoir l'inverse. Par conséquent, dans un tas-min, le plus
16 CHAPITRE 4. STRUCTURES DE DONNÉES ARBORESCENTES
1 tNoeud ∗ R o t a t i o n D r o i t e ( tNoeud ∗ r )
if
2 {
3 (r)
4 {
∗g −>f g ;
if
5 tNoeud = r
6 (g)
7 {
−>f g = g−>f d ;
if
8 r
10 g−>f d = r ;
11 r −>p a r e n t = g ;
12 r = g;
13 }
return
14 }
15 r ;
16 }
17
18 t N o e u d ∗ R o t a t i o n G a u c h e ( tNoeud ∗ r )
if
19 {
20 (r)
21 {
∗d −>f d ;
if
22 tNoeud = r
23 (d)
24 {
−>f d = d−>f g ;
if
25 r
27 d−>f g = r ;
28 r −>p a r e n t = d ;
29 r = d;
30 }
return
31 }
32 r ;
33 }
Figure 4.12 Fonctions C permettant d'eectuer une rotation droite ou gauche sur un
n÷ud d'un ABR.
4.5. LES TAS 17
1 tNoeud ∗ E q u i l i b r e r A B R ( tNoeud ∗ r o o t )
if return
2 {
3 ( ! root ) NULL ;
while
7 root = RotationGauche ( r o o t ) ;
return
9 root = RotationDroite ( root ) ;
10 root ;
11 }
int
12
13 t N o e u d ∗ S e l e c t ( tNoeud ∗ r o o t , ∗k )
if return
14 {
15 ( ! root ) NULL ;
∗ −>f g
if return
16 tNoeud ptr = S e l e c t ( root , k) ;
if −− ∗ return
17 ( ptr ) ptr ;
return
18 ( ( k ) ==0) root ;
19 −
S e l e c t ( root >f d , k ) ;
20 }
petit élément est porté par la racine du tas, alors que le plus grand élément va se trouvé
dans une des feuilles (voir Figure 4.14).
Une remarque importante concernant la position relative des n÷uds frères dans un
tas, c'est que les frères ne sont pas forcement ordonnés entre eux. On peut voir ceci dans
le tas-min de la Figure 4.14, où pour les ls du n÷ud 1, c'est le ls gauche qui est le plus
petit, alors que pour les ls du n÷ud 2, c'est l'inverse.
2 3
4 5 7 6
8 9 10
Un tas, (min ou max), peut être implémenté à l'aide d'un simple tableau uni-dimensionnel.
Ceci est possible car, il y a moyen de stocker les éléments du tas dans le tableau de telle
sorte qu'à partir de l'indice de la case qui contient un n÷ud donné, on peut aisément
retrouver les indices des cases qui contiennent le parent et les deux ls (s'ils existent).
Ce stockage astucieux vérie la règle suivante : si un n÷ud du tas est stocké dans la case
i du tableau alors sont parent, (s'il existe), se trouvera dans la case d'indice i div 2,
alors que ses ls gauche et droit, (s'ils existent), se trouveront dans les cases d'indices
respectifs 2*i et 2*i+1. Il sut alors de stocker la racine du tas dans la case 1, pour
retrouver les indices des cases de tous les n÷uds du tas. On pourra alors convenir que
l'indice 0 désignera un parent inexistant. Le bout de code de la Figure 4.15 contient la
dénition d'un type générique tTas. Il s'agit d'une structure composée de deux champs :
un champ taille servant à mémoriser la taille du tas et un champ elts, qui est un
tableau uni-dimensionnel de tElt réservé au stockage des éléments du tas. Le bout de
code de la Figure 4.15 contient aussi le code des fonctions qui permettent le passage d'un
n÷ud à ces parents.
Un tas est une structure dynamique qui peut augmenter et diminuer de taille pendant
l'exécution du programme. Il est donc possible d'insérer de nouveaux éléments dans un
tas. Toutefois, cette insertion doit conserver les propriétés du tas. Après insertion, on
doit donc avoir un arbre enraciné binaire ordonné quasi-complet dont les feuilles doivent
être entassées vers la gauche. De plus, un n÷ud doit toujours porter un élément inférieur
ou égal à ceux portés par ses ls pour les tas-min et l'inverse pour les tas-max. Pour
conserver ces propriétés, on procède comme suit, lors de l'insertion dans un tas-min. Le
nouvel élément est stocké dans le premier emplacement libre, en partant de la gauche,
du dernier niveau, qui correspond à la case d'indice taille-du-tas plus 1 (voir procédure
Entasser de la Figure 4.16). Ensuite, le nouvel élément est comparé avec son parent, au
niveau de la procédure EntasserAux(.,.). Si le parent est plus grand que le ls, ce qui
n'est pas conforme avec l'ordre des tas-min, alors le parent et le ls échangent de cases. A
son tour, le nouveau parent est comparé avec son parent et un échange aura lieu si l'ordre
des tas-min n'est pas respecté, et ainsi de suite. Dans le cas extrême, le nouvel élément
va se trouver dans la racine du tas, c'est-à-dire, dans la case d'indice 1. Cette éventuelle
remontée du nouvel élément vers la racine du tas est assurée par la boucle while qui ce
trouve à la Ligne 6 du code de la Figure 4.16.
4.5. LES TAS 19
#include
#include
1 < s t d i o . h>
#define int
2 < l i m i t s . h>
#define
3 tElt
#define
4 MAX_TAS 512
5 INVALID_ELT INT_MAX
typedef struct
6
int
8 {
9 taille ;
10 tElt e l t s [MAX_TAS+ 1 ] ;
11 } tTas ;
void
12
13 I n i t T a s ( tTas ∗ tas )
14 {
15 ( ∗ tas ) . taille = 0;
16 }
int int
17
18 Parent ( i )
return
19 {
20 i /2;
21 }
int int
22
23 Fg ( i , tTas tas )
if ∗
24 {
return
25 (2 i > tas . t a i l l e )
else
26 0;
return
27
28 2 ∗i ;
29 }
int int
30
31 Fd ( i , tTas tas )
if ∗
32 {
return
33 (2 i +1 > tas . t a i l l e )
else
34 0;
return
35
36 2 ∗ i +1;
37 }
int
2 {
3 p = Parent ( i ) ;
4 tElt tmp ;
while
5
7 {
11 i = p;
12 p = Parent ( i ) ;
13 }
14 }
void
15
if ∗
17 {
18 (( tas ) . t a i l l e == MAX_TAS)
else
19 p r i n t f ( " Erreur tas p l e i n \n" ) ;
20
21 {
25 }
Exercice 10. Proposez une procédure C qui reçoit un tableau d'entiers et produit
un tas-max avec les éléments de ce tableau. Indication : an de ne pas utiliser de
l'espace mémoire supplémentaire, on supposera que les élément du tableau de départ
se trouvent dans le tableau implémentant le tas.
void int
1
int
3 {
if
5
7 min = fg ;
if
8
10 min = fd ;
if
11
12 ( min != root )
13 {
17 E x t r a i r e A u x ( min , t a s ) ;
18 }
19 }
20
if
22 {
23 (( ∗ tas ) . taille == 0)
24 {
return
25 p r i n t f ( " Erreur tas v i d e \n" ) ;
26 INVALID_ELT ;
else
27 }
28
29 {
return
32 ExtraireAux ( 1 , t a s ) ;
33 tmp ;
34 }
Exercice 12. (Tri par tas) Le tri par tas tire prot de la propriété des tas pour
eectuer un tri en O(n log n) . La démarche de ce tri est la suivant :
On commence par construire un tas-max à partir du tableau qu'on veut trier
dans le sens croisant.
On procède, ensuite, à l'échange de l'élément maximal du tas-max, qui se
trouve à la racine de ce dernier, avec le dernier élément du tas, (l'élément
qui occupe la dernière case du tableau). Cette opération garantie qu'un élé-
ment maximal du tableau à trier à pris sa place nale. En contre partie,
cette permutation peut faire perdre, au tas-max, sa propriété de tas-max, car
l'élément qui occupe désormais la racine du tas, à la suite de la permutation,
n'est pas forcement un élément maximal du tas.
On décrémente alors la taille du tas, pour que l'élément maximal, qui occupe
la dernière case du tableau, ne soit plus déplacé. On restaure, ensuite, la
propriété du tas-max en entassant l'élément déplacé à la racine du tas. On
itère le même processus avec l'élément qui occupe l'avant dernière case du
tableau, et ainsi de suite, jusqu'à l'élément qui occupe la deuxième case du
tableau.
Proposez une procédure C qui implémente la démarche du tri par tas. Indication :
an de ne pas utiliser de l'espace mémoire supplémentaire, on supposera que le
tableau à trier se trouve au départ dans le tableau implémentant le tas.
Exercice 13. On se propose d'ajouter à la structure de tas-min un tableau Booléen
supplémentaire qui permettra d'imposer la condition que tout ls gauche est inférieur
ou égal à son frère (le ls droit).
Redénissez les fonctions FG() et FD() pour imposer la condition décrire
ci-dessus sans avoir à restructurer le tas.
Étant donné un tas-min, proposez une procédure qui initialise le tableau boo-
léen en tenant compte du contenu du tas.