Partie III
Partie III
Partie III
Algorithmique Avancé
et Structures de Données Abstraites
2
1.1 Généralités
Les éléments d’un tableau sont placés de façon adjacent en
mémoire.
4
1.1 Généralités
Une liste chaînée donc est un ensemble ni d'éléments notée
L = e1, e2, …, en
Ou e1 est le premier élément, e2 le deuxième, etc...
Lorsque n=0 on dit que la liste est vide.
Les listes servent à gérer un ensemble de données, un peu
comme les tableaux :
Les listes sont cependant plus efficaces pour réaliser des
opérations comme l'insertion et la suppression d'éléments.
Les listes utilisent par ailleurs l'allocation dynamique de mémoire
et peuvent avoir une taille qui varie pendant l'exécution.
5
1.1 Généralités
Remarque :
Un tableau peut aussi être défini dynamiquement mais pour modifier
sa taille, il faut en créer un nouveau, transférer les données puis
supprimer l'ancien.
L'allocation (ou la libération) se fait élément par élément.
Doublement chaînées
7
1.3.1 Listes simplement chaînées
Une liste simplement chaînée est composée d'éléments
distincts liés par un simple pointeur.
Information Suivant
Données Pointeur
8
1.3.1 Listes simplement chaînées
On implémente un élément d’une liste simple contenant une
donnée de type caractère (char) sous forme d'une
structure C:
struct Noeud{
Type info;
Noeud * suivant;
};
typedef Noeud * Liste;
9
1.3.1 Listes simplement chaînées
10
1.3.1 Listes simplement chaînées
Pour accéder à un élément d'une liste simplement chaînée, on
part de la tête et on passe d'un élément à l'autre à l'aide du
pointeur suivant associé à chaque élément.
En pratique, les éléments étant créent par allocation
dynamique.
➔Ne sont pas adjacents en mémoire contrairement à un tableau.
La suppression d'un élément sans précaution ne permet plus
d'accéder aux éléments suivants.
D'autre part, une liste simplement chaînée ne peut être
parcourue que dans un sens (de la tête vers la queue).
11
1.3.2 Listes doublement chaînées
Les listes doublement chaînées sont constituées d'éléments
comportant trois champs:
un champ : contenant l’information (donnée ou pointeur
vers celle-ci).
un pointeur : vers l‘élément suivant de la liste.
un pointeur : vers l‘élément précèdent de la liste.
Elles peuvent donc être parcourues dans les deux sens.
12
1.3.2 Listes doublement chaînées
En C, on implémente un élément d’une liste doublement chainée
contenant une donnée entière sous forme d'une structure :
typedef char Type;
struct Noeud {
Type info;
Noeud * suivant;
Noeud * precedent;
};
struct NoeudD {
int taille;
Liste tete;
Liste queue;
};
14
1.4 Opérations sur une liste
simplement chainée
La liste chaînée de caractères utilisée sera vide au départ.
L’exemple suivant défini le type abstrait Noeud permettant
de créer le type Liste représentant une liste chaînée de
caractères (char).
struct Noeud{
Type info;
Noeud * suivant;
};
15
1.4.1 Insertion d’un élément
16
1.4.1.1 Insertion de nœuds en tête de
liste chaînée
L’insertion en tête, se fait en créant un élément, lui assigner
la valeur que l'on veut insérer, puis pour terminer, raccorder
cet élément à la liste.
Lors d'une insertion en tête, on devra donc assigner au
suivant l'adresse du premier élément de la liste.
17
1.4.1.1 Insertion de nœuds en tête de
liste chaînée
Illustrons un chaînage en tête : au départ, la liste chaînée est
vide, et on suppose que l’on saisit, dans l’ordre, les caractères
'X', '#' et 'a'.
Initialisation de la liste à NULL :
// ...
main(){
Liste tete;
tete = NULL;
}
18
1.4.1.1 Insertion de nœuds en tête de
liste chaînée
Ajout de 'X' :
Ajout de ‘#' :
Ajout de ‘a' :
19
1.4.1.1 Insertion de nœuds en tête de
liste chaînée
Le programme suivant implémente les différentes étapes illustrées :
// ...
main(){
// ...
// Création du noeud contenant 'X'
Noeud * nouveau = (Noeud*)malloc(sizeof(Noeud));
nouveau->info = 'X';
nouveau->suivant = tete;
tete = nouveau;
// Création du noeud contenant '#'
nouveau = (Noeud*)malloc(sizeof(Noeud));
nouveau->info = '#';
nouveau->suivant = tete;
tete = nouveau;
// Création du noeud contenant 'a'
nouveau = (Noeud*)malloc(sizeof(Noeud));
nouveau->info = 'a';
nouveau->suivant = tete;
tete = nouveau;
}
20
1.4.1.1 Insertion de nœuds en tête de
liste chaînée
Pour afficher le contenu de la liste, on doit parcourir la liste
avec un pointeur, pour ne pas perdre la tête de la liste,
jusqu'au bout et afficher toutes les valeurs qu'elle contient :
// ...
main(){
// ...
Liste tmp = tete;
while(tmp != NULL){
printf("%c ", tmp->info);
tmp = tmp->suivant;
}
}
a # X
21
1.4.1.1 Insertion de nœuds en tête de
liste chaînée
22
1.4.1.1 Insertion de nœuds en tête de
liste chaînée
La définition des fonction est la suivante :
void initialiserListe(Liste * adrListe){
(*adrListe) = NULL ;
}
main(){
Liste tete;
initialiser(&tete);
insererEnTete(&tete, 'X');
insererEnTete(&tete, '#');
insererEnTete(&tete, 'a');
afficher(tete);
}
a # X
24
1.4.1.2 Insertion de nœuds en fin de la
liste chaînée
L’insertion en fin de liste est comme suit:
Créer un nouvel élément, lui assigner sa valeur, et mettre
l'adresse de l'élément suivant à NULL.
Faire pointer le dernier élément de liste originale sur le nouvel
élément que nous venons de créer
Créer un pointeur temporaire sur l’élément qui va se déplacer d'élément
en élément, et regarder si cet élément est le dernier de la liste.
Un élément sera forcément le dernier de la liste si NULL est assigné à
son champ suivant.
25
1.4.1.2 Insertion de nœuds en fin de la
liste chaînée
Le code C de la fonction d’insertion en fin est le suivant :
26
1.4.2 Suppression d’un élément
27
1.4.2.1 Supprimer un élément en tête
Pour supprimer le premier élément de la liste, il faut utiliser
la fonction free.
Si la liste n'est pas vide, on stocke l'adresse du premier
élément de la liste après suppression (i.e. l'adresse du 2ème
élément de la liste originale), on supprime le premier
élément, et on renvoie la nouvelle liste.
Attention quand même à ne pas libérer le premier élément
avant d'avoir stocké l'adresse du second, sans quoi il sera
impossible de la récupérer.
28
1.4.2.1 Supprimer un élément en tête
29
1.4.2.2 Supprimer un élément en fin
de liste
Pour Supprimer un élément en fin de lise, il va falloir
parcourir la liste jusqu'à son dernier élément, indiquer que
l'avant-dernier élément va devenir le dernier de la liste et
libérer le dernier élément pour enfin retourner le pointeur
sur le premier élément de la liste d'origine.
30
1.4.2.2 Supprimer un élément en fin
de liste
31
1.4.3 Rechercher un élément dans une
liste
Le but est de renvoyer l'adresse du premier élément trouvé
ayant une certaine valeur.
Si aucun élément n'est trouvé, on renverra NULL.
L'intérêt est de pouvoir, une fois le premier élément trouvé,
chercher la prochaine occurrence en recherchant à partir de
elementTrouve->suivant.
On parcourt donc la liste jusqu'au bout, et dès qu'on trouve
un élément qui correspond à ce que l'on recherche, on
renvoie son adresse.
32
1.4.3 Rechercher un élément dans une
liste
33
1.4.4 Compter le nombre d'occurrences
d'une valeur
Nous allons utiliser la fonction précédente permettant de
rechercher un élément.
On cherche une première occurrence : si on la trouve, alors
on continue la recherche à partir de l'élément suivant, et ce
tant qu'il reste des occurrences de la valeur recherchée.
Il est aussi possible d'écrire cette fonction sans utiliser la
précédente bien entendu, en parcourant l'ensemble de la liste
avec un compteur que l'on incrémente à chaque fois que l'on
passe sur un élément ayant la valeur recherchée.
Cette fonction n'est pas beaucoup plus compliquée, mais il est
intéressant d'un point de vue algorithmique de réutiliser des
34
fonctions pour simplifier nos codes.
1.4.4 Compter le nombre
d'occurrences d'une valeur
int nombreOccurences(Liste liste, Type valElement){
int i = 0;
// Si la liste est vide, on renvoie 0
if(liste == NULL)
return 0;
// Sinon, tant qu'il y a encore un élément ayant la val = valeur
while((liste = rechercherElement(liste, valElement)) != NULL){
// On incrémente
liste = liste->suivant;
i++;
}
// Et on retourne le nombre d'occurrences
return i;
}
35
1.4.5 Compter le nombre d'éléments
d'une liste chaîné
On parcoure la liste de bout en bout et incrémentez un compteur
pour chaque nouvel élément trouvé.
Jusqu'à maintenant, nous n'avons utilisé que des algorithmes
itératifs qui consistent à boucler tant que l'on n'est pas au bout.
Cette fois-ci, on va créer un algorithme récursif.
36
1.4.5 Compter le nombre d'éléments
d'une liste chaîné
37
1.4.1.7 Recherche du kème élément
38
1.4.1.7 Recherche du kème élément
Noeud * kiemeNoeud(Liste liste, int k){
int i;
// On se déplace de k cases, tant que c'est possible
for(i=0; i<k && liste != NULL; i++){
liste = liste->suivant;
}
// Si l'élément est NULL, c'est que la liste contient
// moins de i éléments
if(liste == NULL){
return NULL;
}
else{
// Sinon on renvoie l'adresse de l'élément i
return liste;
}
}
39
1.4.7 Effacer tous les éléments ayant
une certaine valeur
Dans cette dernière fonction, nous allons encore une fois
utiliser un algorithme récursif.
void supprimerElement(Liste * adrListe, Type valElement){
if((*adrListe) != NULL){
if((*adrListe)->info == valElement){
Noeud * tmp = (*adrListe)->suivant;
free(*adrListe);
(*adrListe) = tmp;
supprimerElement(&tmp, valElement);
}
else{
supprimerElement(&((*adrListe)->suivant), valElement);
}
}
}
40
2 Les piles (Stack)
41
2.1 Introduction
Une pile est une structure qui stocke des éléments, mais rend accessible
uniquement un seul d'entre eux, appelé le sommet de la pile.
Quand on ajoute un élément, celui-ci devient le sommet de la pile,
c'est-à-dire le seul élément accessible.
Quant on retire un élément de la pile, on retire toujours le sommet, et
le dernier élément ajouté avant lui devient alors le sommet de la pile.
42
2.1 Introduction
Pour résumer, le dernier élément ajouté dans la pile est le
premier élément à en être retiré.
Cette structure est également appelée une liste LIFO
Last In, First Out
Généralement, il y a deux façons pour représenter une pile :
La première s'appuie sur la structure de liste chaînée vue
précédemment
et la seconde utilise un tableau (pointeur).
43
2.2 Modélisation par liste chaînée
La première façon de modéliser une pile consiste à utiliser une
liste chaînée en n'utilisant que les opérations ajouterTete et
retirerTete.
Dans ce cas, on s'aperçoit que le dernier élément entré est
toujours le premier élément sorti.
La figure suivante représente une pile modélisée par liste
chainée.
La pile contient les chaînes de caractères suivantes qui ont été
ajoutées dans cet ordre: "Fès", "Errachidia", "Meknès" et
"Rabat".
44
2.2 Modélisation par liste chaînée
Pour cette modélisation, la structure d'une pile est celle
d'une liste chaînée.
struct Noeud{
Type info;
Noeud * suivant;
};
45
2.3 Modélisation par tableau
La deuxième manière de modéliser une pile consiste à utiliser un
tableau.
L'ajout d'un élément se fera à la suite du dernier élément du
tableau.
Le retrait d'un élément de la pile se fera en enlevant le dernier
élément du tableau.
La figure suivante représente une pile par cette modélisation.
Elle reprend la même pile que pour l'exemple de modélisation
par liste chaînée.
46
2.3 Modélisation par tableau
La structure de données correspondant à une pile représentée
par un tableau est comme suit :
#define NMAX 8
struct PileTab{
Type tab[NMAX];
int sommet;
};
typedef struct PileTab PileTab;
48
2.4.1 Opérations pour la modélisation
par liste chaînée
Initialiser la pile :
Cette fonction initialise les valeurs de la structure représentant
la pile, afin que celle-ci soit vide.
Dans le cas d'une représentation par liste chaînée, il suffit
d'initialiser la liste chaînée qui représente la pile.
49
2.4.1 Opérations pour la modélisation
par liste chaînée
Pile vide ? :
Cette fonction indique si la pile pile est vide.
Dans le cas d'une représentation par liste chaînée, la pile est
vide si la liste qui la représente est vide.
50
2.4.1 Opérations pour la modélisation
par liste chaînée
Sommet d'une pile :
Cette fonction retourne l'élément au sommet de la pile pile.
Dans le cas d'une représentation par liste chaînée, cela revient à
retourner la valeur de l'élément en tête de la liste.
A n'utiliser que si la pile pile n'est pas vide.
51
2.4.1 Opérations pour la modélisation
par liste chaînée
Empiler un élément sur une pile :
Cette fonction empile l'élément valElement au sommet de la
pile d’adresse adrPile.
Pour la représentation par liste chaînée, cela revient à ajouter
l'élément valElement en tête de la liste.
52
2.4.1 Opérations pour la modélisation
par liste chaînée
Dépiler un élément d'une pile :
Cette fonction dépile l'élément au sommet de la pile d’adresse
adrPile.
Pour la représentation par liste chaînée, cela revient à supprimer
(si possible) la valeur de l'élément en tête de liste de cette
dernière.
53
2.4.2 Opérations pour la modélisation
par tableau
Initialiser la pile
Cette fonction initialise les valeurs de la structure représentant
une pile, afin que celle-ci soit vide.
Dans le cas d'une représentation par tableau, il suffit de rendre le
sommet à nul.
54
2.4.2 Opérations pour la modélisation
par tableau
Pile vide ? :
Cette fonction indique si la pile pile est vide.
Dans le cas d'une représentation par tableau, la pile est vide si le
sommet est nul.
55
2.4.2 Opérations pour la modélisation
par tableau
Pile pleine ? :
Cette fonction indique si la pile pile est pleine.
Dans le cas d'une représentation par tableau, la pile est pleine si
le champ sommet est égal à NMAX.
56
2.4.2 Opérations pour la modélisation
par tableau
Sommet d'une pile :
Cette fonction retourne l'élément au sommet de la pile pile.
Dans le cas d'une représentation par tableau, cela revient à
retourner la valeur du nième élément du tableau (i.e. l'élément
d'indice sommet - 1).
A n'utiliser que si la pile pile n'est pas vide.
57
2.4.2 Opérations pour la modélisation
par tableau
Empiler un élément sur une pile :
Cette fonction empile l'élément valElement au sommet de la
pile d’adresse adrPile.
Pour la représentation par tableau, cela revient à ajouter
l'élément valElement à la fin du tableau.
S'il reste de la place dans l'espace réservé au tableau,
l’empilement peut avoir lieu.
void empiler(PileTab * adrPile, Type valElement){
if(!pilePleine(*adrPile)){
adrPile ->tab[p->sommet] = valElement;
adrPile ->sommet ++;
}
}
58
2.4.2 Opérations pour la modélisation
par tableau
Dépiler un élément d'une pile :
Cette fonction dépile l'élément au sommet de la pile pile.
Pour la représentation par tableau, cela revient à diminuer d'une
unité le champ sommet.
Si la pile n'est pas déjà vide, on peut dépiler.
59
3 Les files (queue)
60
3.1 Introduction
Une file d'attente est une structure qui stocke des éléments, mais
rend accessible uniquement un seul d'entre eux, appelé la tête de
la file.
Quant on ajoute un élément, celui-ci devient le dernier élément
qui sera accessible.
Quant on retire un élément de la file, on retire toujours la tête,
celle-ci étant le premier élément qui a été placé dans la file.
61
3.1 Introduction
Pour résumer, le premier élément ajouté dans la pile est le
premier élément à en être retiré.
Cette structure est également appelée une liste FIFO (First In,
First Out).
Généralement, il y a deux façons pour représenter une file
d'attente.
La première s'appuie sur la structure de liste chaînée vue
précédemment.
La seconde manière utilise un tableau d'une façon assez
particulière que l'on appelle modélisation par "tableau
circulaire".
62
3.2 Modélisation par liste chaînée
La première façon de modéliser une file d'attente consiste à
utiliser une liste chaînée en n'utilisant que les opérations
ajouterQueue et retirerTete.
Dans ce cas, on s'aperçoit que le premier élément entré est
toujours le premier élément sorti.
63
3.2 Modélisation par liste chaînée
Pour cette modélisation, la structure d'une file d'attente est
celle d'une liste chaînée (avec tête et queue).
struct Noeud{
Type info;
Noeud * suivant;
};
64
3.3 Modélisation par tableau circulaire
La deuxième manière de modéliser une file d'attente consiste à
utiliser un tableau.
L'ajout d'un élément se fera à la suite du dernier élément du tableau.
Le retrait d'un élément de la file se fera en enlevant le premier
élément du tableau.
Il faudra donc deux indices pour ce tableau, le premier qui indique le
premier élément de la file et le deuxième qui indique la fin de la file.
65
3.3 Modélisation par tableau circulaire
On peut noter que progressivement, au cours des opérations
d'ajout et de retrait, le tableau se déplace sur la droite dans son
espace mémoire.
A un moment, il va en atteindre le bout de l'espace.
Dans ce cas, le tableau continuera au début de l'espace
mémoire comme si la première et la dernière case étaient
adjacentes, d'où le terme "tableau circulaire".
66
3.3 Modélisation par tableau circulaire
Voici la structure de données correspondant à une file d'attente
représentée par un tableau circulaire.
NMAX est une constante représentant la taille du tableau alloué.
tete est l'indice de tableau qui pointe sur la tête de la file d'attente.
fin est l'indice de tableau qui pointe sur la case suivant le dernier élément du
tableau, c'est-à-dire la prochaine case libre du tableau.
#define NMAX 10
struct FileTab{
Type tab[NMAX];
int tete;
int fin;
};
67 typedef struct FileTab FileTab;
3.4 Opérations sur la structure
Voici les opérations que nous allons détailler pour ces deux
modélisations.
Initialise une file vide.
Indiquer si la file file est vide.
Retourner l'élément en tête de la file file.
Entrer l'élément valElement dans la file file.
Sortir l'élément en tête de la file file.
Les prototypes de ces opérations (paramètres et type de
retour) sont les mêmes quelque soit la modélisation choisie.
68
3.4.1 Opérations pour la modélisation
par liste chaînée
Initialiser une file :
La fonction suivante permet d’initialiser les valeurs de la
structure représentant la file d’adresse adrFile pour que celle-
ci soit vide.
Dans le cas d'une représentation par liste chaînée, il suffit
d'initialiser la liste chaînée qui représente la file d'attente.
69
3.4.1 Opérations pour la modélisation
par liste chaînée
File vide ? :
Cette fonction indique si la file file est vide.
Dans le cas d'une représentation par liste chaînée, la file est vide
si la liste qui la représente est vide.
70
3.4.1 Opérations pour la modélisation
par liste chaînée
Tête d'une file :
Cette fonction retourne l'élément en tête de la file file.
Dans le cas d'une représentation par liste chaînée, cela revient à
retourner la valeur de l'élément en tête de la liste.
A n'utiliser que si la file file n'est pas vide.
71
3.4.1 Opérations pour la modélisation
par liste chaînée
Entrer un élément dans une file :
Cette fonction place un élément valElement en queue de la
file d’adresse adrFile.
Pour la représentation par liste chaînée, cela revient à ajouter
l'élément valElement en queue de la liste.
void entrerElement(FileLst * adrFile, Type valElement){
insererEnQueue(adrFile, valElement);
}
72
3.4.1 Opérations pour la modélisation
par liste chaînée
Sortir un élément d'une file :
Cette fonction retire l'élément en tête de la file d’adresse
adrFile.
Pour la représentation par liste chaînée, cela revient à supprimer
l'élément en tête de liste.
void sortirElement(FileLst * adrFile){
if(!fileVide(*adrFile))
supprimerEnTete(adrFile);
}
73
3.4.2 Opérations pour la modélisation
par tableau circulaire
Initialiser une file :
Cette fonction initialise les valeurs de la structure représentant
la file d’adresse adrFile pour que celle-ci soit vide.
Dans le cas d'une représentation par tableau circulaire, on
choisira de considérer la file vide lorsque :
adrFile->tete = adrFile->fin
Arbitrairement, on choisit ici de mettre ces deux indices à 0
pour initialiser une file d'attente vide.
74
3.4.2 Opérations pour la modélisation
par tableau circulaire
File vide ? :
Cette fonction indique si la file file est vide.
Dans le cas d'une représentation par tableau circulaire, la file est
vide lorsque file.tete = file.fin.
int fileVide(FileTab file){
return (file.tete == file.fin) ? 1 : 0;
}
75
3.4.2 Opérations pour la modélisation
par tableau circulaire
Tête d'une file :
Cette fonction retourne l'élément en tête de la file file.
Dans le cas d'une représentation par tableau circulaire, il suffit
de retourner l'élément d'indice de tête dans le tableau.
A n'utiliser que si la file file n'est pas vide.
76
3.4.2 Opérations pour la modélisation
par tableau circulaire
Entrer un élément dans une file :
Cette fonction place un élément valElement en queue de la file
d’adresse adrFile.
Pour la représentation par tableau circulaire, l'élément est placé dans la
case pointée par l'indice fin.
Ce dernier est ensuite augmenté d'une unité, en tenant compte du fait
qu'il faut revenir à la première case du tableau s’il a atteint la fin de
celui-ci (%).
78
4 Les arbres (Tree)
79
4.1 Généralités
On s'intéresse aux structures de données arborescentes.
Ces structures dynamiques récursives constituent un outil
important couramment utilisé dans de nombreuses applications :
codage, interfaces graphiques, SGBD, expressions arithmétiques,
intelligence artificielle, génomique, etc.
Nous distinguerons dans un premier temps les arbres binaires,
avant d’étendre et de généraliser cette structure.
Dans un arbre, on distingue deux catégories d'éléments :
Feuilles : éléments ne possédant pas de fils dans l'arbre ;
Nœuds internes : éléments possédant des fils (sous-branches).
80
4.2 Définitions
Un arbre est une structure qui peut se définir de manière
récursive :
➔un arbre est un arbre qui possède des liens ou des pointeurs
vers d'autres arbres.
Cette définition plutôt étrange au premier abord résume
bien la démarche qui sera utilisé pour réaliser cette structure
de données.
81
4.2 Définitions
On distingue deux grands types d'arbres :
Arbres enracinés
82
Arbres enracinés
Un arbre enraciné est un arbre hiérarchique dans lequel
on peut établir des niveaux.
Il ressemblera plus à un arbre généalogique tel qu'on le conçoit
couramment.
83
Arbres non enracinés
Un arbre non enraciné est un arbre où il n'y a pas de
relation d'ordre ou de hiérarchie entre les éléments qui
composent l'arbre.
On peut passer d'un arbre non enraciné à un arbre enraciné.
Il suffit de choisir un élément comme sommet de l'arbre et de
l'organiser de façon à obtenir un arbre enraciné.
84
4.2.2 Terminologie
Chaque élément d'un arbre se nomme un nœud.
Les nœuds sont reliés les uns aux autres par des relations
d'ordre ou de hiérarchie.
Ainsi on dira qu'un nœud possède un père, c'est à dire un nœud
qui lui est supérieur dans cette hiérarchie.
Il possède éventuellement un ou plusieurs fils.
La racine est un nœud qui n'a pas de père.
un nœud feuille est un nœud qui n'a pas de fils.
Parfois on appelle une feuille un nœud externe tout autre
nœud de l'arbre sera alors appelé un nœud interne.
85
4.2.2 Terminologie
Voici donc un schéma qui résume les différents composants
d'un arbre :
86
4.2.3 Arité d'un arbre
L'arité de l'arbre est le nombre de fils qu'il possède.
Un arbre dont les nœuds ne comporteront qu'au maximum n
fils sera d'arité n.
➔arbre n-aire
Il existe un cas particulièrement utilisé : c'est l'arbre binaire.
les nœuds ont au maximum 2 fils (fils gauche et de fils droit).
L'arité n'impose pas le nombre minimum de fils, il s'agit d'un
maximum, ainsi un arbre d'arité 3 pourra avoir des nœuds
qui ont 0, 1, 2 ou 3 fils, mais en tout cas pas plus.
Degré d'un nœud est le nombre de fils que possède ce nœud.
87
4.2.4 Taille et hauteur d'un arbre
Taille d'un arbre, le nombre de nœud interne qui le compose.
➔le nombre nœud total moins le nombre de feuille de l'arbre.
La profondeur d'un nœud est la distance en terme de nœud
par rapport à l'origine.
Par convention, la racine est de profondeur 0.
Exemple : le nœud F est de profondeur 2 et le nœud H est de
profondeur 3.
88
4.2.4 Taille et hauteur d'un arbre
89
4.2.5 Arbre localement complet,
dégénéré, complet
Un arbre binaire localement complet est un arbre binaire dont
chacun des nœuds possèdent soit 0 soit 2 fils.
➔Les nœuds internes auront tous deux fils.
➔Un arbre binaire localement complet de taille n aura n+1 feuille.
90
4.2.5 Arbre localement complet,
dégénéré, complet
Un arbre dégénéré (appelé aussi filiforme) est un arbre dont les
nœuds ne possède qu'un et un seul fils.
➔ Cet arbre est donc tout simplement une liste chaînée.
➔ un arbre dégénéré de taille n a une hauteur égale à n+1 feuille.
91
4.2.5 Arbre localement complet,
dégénéré, complet
Un arbre binaire complet tout arbre qui est localement
complet et dont toutes les feuilles ont la même profondeur.
➔Dans ce type d'arbre, on peut exprimer le nombre de nœuds n
de l'arbre en fonction de la hauteur h : n = 2^(h+1) -1.
92
4.3 Implémentation des arbres n-aire:
Un nœud est une structure (ou un enregistrement) qui
contient au minimum trois champs :
un champ contenant l'élément du nœud, c'est l'information qui
est importante.
Les deux autres champs sont le fils gauche et le fils droit du
nœud.
Ces deux fils sont en fait des arbres, on les appelle généralement les sous
arbres gauches et les sous arbres droit du nœud.
De part cette définition, un arbre ne pourra donc être qu'un pointeur sur
un nœud.
93
4.3 Implémentation des arbres n-aire
Voici donc une manière d'implémenter un arbre binaire en
langage C :
typedef int Type;
struct Noeud {
Type info;
Noeud * filsG;
Noeud * filsD;
};
94
4.3 Implémentation des arbres n-aire
On remplacera le type Type par le type ou la structure de
données que l'on veut utiliser comme entité significative des
nœuds de l'arbre.
De cette définition, on peut donc aisément constater que
l'arbre vide sera représenté par la constante NULL.
95
4.3 Implémentation des arbres n-aire
Maintenant, si on veut représenter un autre type d'arbre,
nous avons deux solutions :
soit nous connaissons à l'avance le nombre maximal de fils des
nœuds (ce qui veut dire que l'on connaît l'arité de l'arbre),
soit nous ne la connaissons pas.
96
4.3 Implémentation des arbres n-aire
Dans le premier cas, nous pouvons utiliser un tableau pour
stocker les fils.
Ainsi pour un arbre d'arité 4 nous aurons l'implémentation
suivante :
struct Noeud {
Type info;
Noeud * fils[4];
};
97
4.3 Implémentation des arbres n-aire
Ainsi dans ce cas, les fils ne seront pas identifiés par leur
position (droite ou gauche) mais par un numéro.
98
4.3 Implémentation des arbres n-aire
La deuxième solution consiste à utiliser une liste chainée
pour la liste des fils.
typedef int Type;
101
4.4 Les fonctions de base sur la
manipulation des arbres
Maintenant, prenons deux fonctions qui vont nous permettre
de récupérer le fils gauche ainsi que le fils droit d'un arbre:
103
4.4 Les fonctions de base sur la
manipulation des arbres
Enfin, nous pouvons créer une dernière fonction bien que
très peu utile : déterminer si un nœud est un nœud interne.
int estNoeudInterne(Arbre * adrArbre){
return !estFeuille(adrArbre);
}
104
4.5 Algorithmes de base sur les arbres
binaires
Nous présentons les algorithmes de base sur les arbres en
exploitant les propriétés de la récursivité.
Il faut savoir qu'il n'y a pas d'autres alternatives.
De plus, le schéma est quasiment le même, une fois que vous
aurez vu deux ou trois fois ce schéma.
105
4.5.1 Calcul de la hauteur d'un arbre
Pour calculer la hauteur d'un arbre, nous allons nous baser sur
la définition récursive :
un arbre vide est de hauteur 0.
un arbre non vide a pour hauteur 1 + la hauteur maximale entre
ses fils.
hauteur(filsDroit(adrArbre)));
}
106
4.5.1 Calcul de la hauteur d'un arbre
La fonction max n'est pas définie c'est ce que nous faisons
maintenant :
107
4.5.2 Calcul du nombre de nœud
Le calcul est très simple.
On définit le calcul du nombre de nœud en utilisant la
définition récursive :
Si l'arbre est vide : renvoyer 0.
Sinon renvoyer 1 + la somme du nombre de nœuds des sous
arbres.
108
4.5.3 Calcul du nombre de feuilles
Le calcul du nombre de feuille repose sur la définition
récursive :
un arbre vide n'a pas de feuille.
un arbre non vide a son nombre de feuille défini de la façon
suivante :
si le nœud est une feuille alors on renvoie 1
si c'est un nœud interne alors le nombre de feuille est la somme du
nombre de feuille de chacun de ses fils.
109
4.5.3 Calcul du nombre de feuilles
Voici ce que cela peut donner en langage C :
nbFeuille(filsDroit(adrArbre));
}
110
4.5.4 Nombre de nœud internes
Le calcule du nombre de nœud interne repose sur le même
principe que le calcul du nombre de feuille.
La définition récursive est la suivante :
un arbre vide n'a pas de nœud interne.
si le nœud en cours n'a pas de fils alors renvoyer 0
si le nœud a au moins un fils, renvoyer 1 + la somme des nœuds
interne des sous arbres.
112
4.6.1 Parcours en profondeur
Le parcours en profondeur de l'arbre suivant donne :
1,2,3,4,5,6,7,8,9,10 :
113
4.6.1 Parcours en profondeur
114
4.6.1 Parcours en profondeur
Le parcours préfixe (RGD : Racine Gauche Droit) : affiche la
racine de l'arbre, parcourt tout le sous arbre de gauche, une
fois qu'il n'y a plus de sous arbre gauche parcourt les
éléments du sous arbre droit.
Le parcours infixe affiche la racine après avoir traité le sous
arbre gauche, après traitement de la racine, on traite le sous
arbre droit (c'est donc un parcours GRD).
Le parcours postfixe effectue donc le dernier type de schéma:
sous arbre gauche, sous arbre droit puis la racine, c'est donc
un parcours GDR.
115
4.6.1 Parcours en profondeur
Commençons par le parcours préfixe, celui ci traite la racine
d'abord.
116
4.6.1 Parcours en profondeur
Maintenant le parcours infixe :
117
4.6.1 Parcours en profondeur
Et enfin le parcours suffixe :
118
4.6.1 Parcours en profondeur
Maintenant que nous avons la définition des types de parcours,
exprimons ceci en C.
La fonction traiterRacine, est une fonction que vous
définissez vous même, il s'agit par exemple d'une fonction
d'affichage de l'élément qui est à la racine.
119
4.6.1 Parcours en profondeur
Voilà pour les trois fonctions de parcours.
Nous pouvons donc traiter ceci facilement, afin de ne pas à avoir à
écrire trois fois de suite le même code.
Voici donc le code que l'on peut avoir en C :
121
4.6.2 Parcours en largeur (ou par
niveau)
Le parcours en largeur traite les nœuds un par un sur un même
niveau.
On passe ensuite sur le niveau suivant, et ainsi de suite.
Le parcours en largeur de l'arbre suivant est :
1 2 3 4 5 6 7 8 9 10.
122
4.6.2 Parcours en largeur (ou par
niveau)
Une méthode pour réaliser un parcours en largeur consiste à utiliser
une structure de données de type file d'attente.
Le principe est le suivant, lorsque nous somme sur un nœud nous
traitons ce nœud (par exemple nous l'affichons) puis nous mettons
les fils gauche et droit non vides de ce nœud dans la file d'attente,
puis nous traitons le prochain nœud de la file d'attente.
Au début, la file d'attente ne contient rien, nous y plaçons donc la
racine de l'arbre que nous voulons traiter.
L'algorithme s'arrête lorsque la file d'attente est vide.
En effet, lorsque la file d'attente est vide, cela veut dire qu'aucun
des nœuds parcourus précédemment n'avait de sous arbre gauche ni
de sous arbre droit.
Par conséquent, on a donc bien parcouru tous les nœuds de l'arbre.
123
4.6.2 Parcours en largeur (ou par
niveau)
Voici donc le code C de ladite fonction :
124
4.6.2 Parcours en largeur (ou par
niveau)
Appliquons ce code à l'arbre suivant :
125
4.6.2 Parcours en largeur (ou par
niveau)
Au tout début de l'algorithme, la file ne contient rien, on y
ajoute donc l'arbre, la file d'attente devient donc :
126
4.6.2 Parcours en largeur (ou par
niveau)
On traite la racine puis on y ajoute les fils droits et gauche, la
file vaut donc :
127
4.6.2 Parcours en largeur (ou par
niveau)
On traite ensuite le prochain élément de la file d'attente.
Ce nœud n'a pas de sous arbre, on ajoute donc rien à la file.
Celle ci vaut donc :
128
4.6.2 Parcours en largeur (ou par
niveau)
On traite le prochain nœud dans la file d'attente.
Celui ci a un fils droit, nous l'ajoutons donc à la file d'attente.
Cette dernière ne contient donc maintenant plus qu'un nœud:
On traite ce nœud.
Celui ci n'ayant pas de fils, nous n'ajoutons donc rien à la file.
La file est désormais vide, l'algorithme se termine.
129
4.7 Opérations élémentaires sur un
arbre
Maintenant que nous savons parcourir un arbre, que nous
savons obtenir des informations sur un arbre, il serait peut
être temps de créer un arbre, de l'alimenter et enfin de
supprimer des éléments.
130
4.7.1 Création d'un arbre
On peut distinguer deux types de création d'un arbre :
création d'un arbre vide
création d'un arbre à partir d'un élément et de deux sous arbres
La première méthode est très simple, étant donné que nous
avons crée un arbre comme étant un pointeur, un arbre vide
est donc un pointeur NULL.
La fonction de création d'un arbre vide est donc une fonction
qui nous renvoie la constante NULL.
131
4.7.1 Création d'un arbre
Dans la deuxième fonction, il faut tout d'abord créer un
nœud, ensuite, on place dans les fils gauche et droit les sous
arbres que l'on a passés en paramètre ainsi que la valeur
associée au nœud.
Enfin, il suffit de renvoyer ce nœud.
En fait, ce n'est pas tout à fait exact, puisque ce n'est pas un
nœud mais un pointeur sur un nœud qu'il faut renvoyer. Mais
nous utilisons le terme nœud pour spécifier qu'il faut allouer
un nœud en mémoire.
132
4.7.1 Création d'un arbre
Ceci donnera donc en C :
134
4.7.2 Ajout d'un élément
Nous pouvons donc écrire le code C correspondant :
136
4.7.2 Ajout d'un élément
Dans ce type d'arbre, il y a une cohérence entre les nœuds,
c'est à dire que la hiérarchie des nœuds respecte une règle.
Celle ci est simple. Pour un arbre binaire de recherche
contenant des entiers, nous considérerons que les valeurs des
nœuds des sous arbres gauche sont inférieures à la racine de
ce nœud et les valeurs des sous arbres droit sont supérieures
à cette racine.
Voici un exemple d'un tel arbre :
137
4.7.2 Ajout d'un élément
Puisque nous sommes dans la partie ajout de nœud, voyons
comment ajouter un nœud dans un tel arbre :
Le principe d'insertion est simple :
si on a un arbre vide, alors il s'agit de la création d'un arbre.
Sinon, on compare la valeur de l'élément à insérer avec la valeur
de la racine.
Si l'élément est plus petit alors on insère à gauche sinon, on
insère dans le fils droit de la racine.
138
4.7.2 Ajout d'un élément
Voici le code en C que l'on peut écrire pour insérer un
élément dans un arbre binaire de recherche.
void insererArbreRecherche(Arbre * adrArbre, Type val){
if(estVide(adrArbre)){
*adrArbre = creerNoeud(val, NULL, NULL);
}
else{
if (val < (*adrArbre)->info){
insererArbreRecherche(filsGauche(adrArbre), val);
}
else{
insererArbreRecherche(filsDroit(adrArbre), val);
}
}
}
139
4.7.2 Ajout d'un élément
Vous remarquerez que l'on insère les éléments qui sont égaux
à la racine du coté droit de l'arbre.
Autre remarque, vous constaterez que nous effectuons des
comparaisons sur les entités du type Type, ceci n'est valable
que pour des valeurs numériques (entier, flottant et
caractère).
S'il s'agit d'autres types, vous devrez utiliser votre propre
fonction de comparaison (comme strcmp pour les chaînes de
caractère etc.).
140
4.7.3 Recherche dans un arbre
Après avoir alimenté notre arbre, il serait peut être temps
d'effectuer des recherches sur notre arbre. Il y a principalement
deux méthodes de recherche. Elles sont directement liées au type
de l'arbre :
si l'arbre est quelconque et
si l'arbre est un arbre de recherche.
Nos recherches se contenteront seulement de déterminer si la
valeur existe dans l'arbre. Avec une petite adaptation, on peut
récupérer l'arbre dont la racine contient est identique à l'élément
cherché.
Commençons par chercher l'élément dans un arbre quelconque.
Cette méthode est la plus intuitive : on cherche l'élément dans
tous les nœuds de l'arbre. Si celui ci est trouvé, on renvoie vrai, si
ce n'est pas le cas, on renvoie faux.
141
4.7.3 Recherche dans un arbre
Voici le code C associé à cette recherche :
142
4.7.3 Recherche dans un arbre
143
4.7.3 Recherche dans un arbre
C'est pour cela que sont apparus les arbres binaires de
recherche.
En effet, on les nomme ainsi parce qu'ils optimisent les
recherches dans un arbre.
Pour savoir si un élément existe, il faut parcourir seulement
une branche de l'arbre.
Ce qui fait que le temps de recherche est directement
proportionnel à la hauteur de l'arbre.
L'algorithme se base directement sur les propriétés de
l'arbre, si l'élément que l'on cherche est plus petit que la
valeur du nœud alors on cherche dans le sous arbre de
gauche, sinon, on cherche dans le sous arbre de droite.
144
4.7.3 Recherche dans un arbre
Cela se traduit en C de la façon suivante :
147
4.7.4 Suppression d'un arbre
En fait cet algorithme est un simple parcours d'arbre.
En effet, lorsque nous devrons traiter la racine, nous
appellerons une fonction de suppression de la racine.
Comme nous avons dis plutôt que nous ne supprimerons que
des feuilles, avant de supprimer la racine, il faut supprimer
les sous arbres gauche et droit.
On en déduit donc que l'algorithme de suppression est un
parcours d'arbre postfixe.
148
4.7.4 Suppression d'un arbre
Voici le code C associé :
149
4.7.4 Suppression d'un arbre
On peut se demander pourquoi nous passons par un pointeur
sur un arbre pour effectuer notre fonction.
Ceci est tout simplement du au fait que le C ne fait que du
passage de paramètre par copie et par conséquent si on veut
que l'arbre soit réellement vide (i.e. : qu'il soit égal à NULL)
après l'exécution de la fonction, il faut procéder ainsi.
Les appels à filsGauche et filsDroit au début ne posent
aucun problème dans le cas où adrArbre vaut NULL.
En effet, dans ce cas, les deux fonctions renverront le pointeur
NULL.
150
Fin de la partie II
151