Partie III

Télécharger au format pdf ou txt
Télécharger au format pdf ou txt
Vous êtes sur la page 1sur 151

Université Ibn Tofail

Ecole Supérieure de Technologie


Kénitra

Algorithmique Avancé
et Structures de Données Abstraites

Partie III : Implémentation des Types de Données


Abstraits en C

DUT : Génie Informatique (S2)

Pr. Moulay Youssef HADI


1 Les listes chainées (Linked List)

2
1.1 Généralités
 Les éléments d’un tableau sont placés de façon adjacent en
mémoire.

 Pour créer un tableau, il faut connaître sa taille.


 Si vous voulez supprimer un élément au milieu du tableau,
 il vous faut recopier les éléments temporairement,
 réallouer de la mémoire pour le tableau,
 puis le remplir à partir de l'élément supprimé.

➔Bref, beaucoup de manipulations coûteuses en ressources.


3
1.1 Généralités
 Une liste chaînée est différente dans le sens où les éléments
sont répartis dans la mémoire et reliés entre eux par des liens
(pointeurs).

 L’ajout et la suppression des éléments d'une liste


chaînée peut être à n'importe quel endroit, à n'importe quel
instant, sans devoir recréer la liste entière.

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.

 Les opérations sur une liste peuvent être:


 Créer une liste
 Supprimer une liste
 Rechercher un élément particulier
 Insérer un élément (en début, en n ou au milieu)
 Supprimer un élément particulier
 Permuter deux éléments
 Concaténer deux listes
 ...
6
1.2 Types de liste chainée
 Les listes peuvent être :
 Simplement chaînées

 Doublement chaînées

 Circulaires (chaînage simple ou double)

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.

 Chaque élément (Noeud) d'une liste simplement chaînée est formé


de deux parties:
 un champ (ou plusieurs champs) : contenant la donnée (ou un
pointeur vers celle-ci),
 un pointeur : vers l'élément suivant de la liste.

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:

typedef char Type;

typedef struct Noeud Noeud;

struct Noeud{
Type info;
Noeud * suivant;
};
typedef Noeud * Liste;

9
1.3.1 Listes simplement chaînées

 Le premier élément d'une liste est sa tête


 le dernier élément d'une liste est sa queue.
 Le pointeur du dernier élément est initialisé à une valeur
sentinelle, par exemple la valeur NULL en C.

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;

typedef struct Noeud Noeud;

struct Noeud {
Type info;
Noeud * suivant;
Noeud * precedent;
};

typedef Noeud * Liste;

struct NoeudD {
int taille;
Liste tete;
Liste queue;
};

typedef struct NoeudD NoeudD;

13 typedef struct NoeudD * ListeD;


1.3.3 Listes circulaires
 Une liste circulaire peut être simplement ou doublement
chaînée.
 Sa particularité est de ne pas comporter de queue.
 Le dernier élément de la liste pointe vers le premier.
 Un élément possède donc toujours un suivant.

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).

typedef char Type;

typedef struct Noeud Noeud;

struct Noeud{
Type info;
Noeud * suivant;
};

typedef Noeud * Liste;

15
1.4.1 Insertion d’un élément

 Pour insérer un élément dans une liste chaînée, il faut savoir


où l'insérer.
 Les trois insertions possibles dans une liste chaînée sont :
 en tête de liste
 en fin de liste
 à une position donnée

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

 A ce niveau, on a tout intérêt d’améliorer notre programme


en créant des fonctions réalisant les opérations sur la liste
simplement chainée crée:
 initialiserListe : fonction d’initialisation de la liste
 insererEnTete : fonction d’insertion en tête
 afficherListe : fonction d’affichage de la liste

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 ;
}

void insererEnTete(Liste * adrListe, Type valElement){


Noeud * nouveau ;
nouveau = (Noeud*)malloc(sizeof(Noeud));
if(nouveau != NULL ){
nouveau->suivant = (*adrListe);
nouveau->info = valElement;
(*adrListe) = nouveau;
}
}

void afficherListe(Liste liste){


while(liste!=NULL){
printf("%c ", liste->info);
liste = liste->suivant;
}
printf("\n");
}
23
1.4.1.1 Insertion de nœuds en tête de
liste chaînée
 Le programme main sera ainsi :

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 :

void insererEnQueue(Liste * adrListe, Type valElement){


Noeud * nouveau = (Noeud*)malloc(sizeof(Noeud));
nouveau->info = valElement;
nouveau->suivant = NULL;
if((*adrListe) == NULL){
(*adrListe) = nouveau;
}
else{
Liste tmp = (*adrListe);
while(tmp->suivant != NULL){
tmp = tmp->suivant;
}
tmp->suivant = nouveau;
}
}

26
1.4.2 Suppression d’un élément

 Supprimer un élément en tête


 Supprimer un élément en fin de liste

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

void supprimerEnTete(Liste * adrListe){


if((*adrListe) != NULL){
Liste tmp = (*adrListe)->suivant;
free(*adrListe);
(*adrListe) = tmp;
}
}

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

void supprimerEnFin(Liste * adrListe){


if((*adrListe) != NULL){
if((*adrListe)->suivant == NULL){
free(*adrListe);
(*adrListe) = NULL;
}
Liste tmp = (*adrListe);
Liste ptmp = (*adrListe);
while(tmp->suivant != NULL){
ptmp = tmp;
tmp = tmp->suivant;
}
ptmp->suivant = NULL;
free(tmp);
}
}

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

Noeud * rechercherElement(Liste liste, Type valElement){


Noeud * tmp = liste;
// Tant que l'on n'est pas au bout de la liste
while(tmp != NULL){
if(tmp->info == valElement){
// Si l'élément a la valeur recherchée,
// on renvoie son adresse
return tmp;
}
tmp = tmp->suivant;
}
return NULL;
}

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.

int nombreElements(Liste liste){


// Si la liste est vide, il y a 0 élément
if(liste == NULL)
return 0;
// Sinon, il y a un élément (celui que l'on est en train de traiter)
// plus le nombre d'éléments contenus dans le reste de la liste
return nombreElements(liste->suivant)+1;
}

36
1.4.5 Compter le nombre d'éléments
d'une liste chaîné

int nombreElements(Liste liste){


// Si la liste est vide, il y a 0 élément
if(liste == NULL)
return 0;
// Sinon, il y a un élément (celui que l'on est en train de traiter)
// plus le nombre d'éléments contenus dans le reste de la liste
return nombreElements(liste->suivant)+1;
}

37
1.4.1.7 Recherche du kème élément

 Il suffit de se déplacer k fois à l'aide du pointeur tmp le long


de la liste chaînée et de renvoyer l'élément à l'indice k.
 Si la liste contient moins de i élément(s), alors nous
renverrons NULL.

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.

typedef char Type;

typedef struct Noeud Noeud;

struct Noeud{
Type info;
Noeud * suivant;
};

typedef Noeud * PileLst;

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

typedef int Type;

struct PileTab{
Type tab[NMAX];
int sommet;
};
typedef struct PileTab PileTab;

 NMAX représentant la taille du tableau alloué et sommet est


47
le nombre d'éléments dans la pile.
2.4 Opérations sur la structure
 Voici les opérations que nous allons détailler pour ces deux
modélisations.
 Initialiser une pile vide.
 Tester si une pile est vide.
 Retourner l'élément au sommet d’une pile.
 Empiler un élément au sommet d’une pile.
 Dépiler l'élément au sommet d’une pile.
 Les prototypes de ces opérations (paramètres et type de
retour) sont les mêmes quelque soit la modélisation choisie.

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.

void initialiserPile(PileLst * adrPile){


initialiserListe(adrPile);
}

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.

int pileVide(PileLst pile){


return listeVide(pile);
}

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.

Type sommetPile(PileLst pile){


if(!pileVide(pile))
return teteListe(pile);
}

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.

void empiler(PileLst * adrPile, Type valElement){


insererEnTete(adrPile, valElement);
}

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.

void depiler(PileLst * adrPile){


if(!pileVide(*adrPile))
supprimerEnTete(adrPile);
}

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.

void initialiserPile(PileTab * adrPile){


adrPile->sommet = 0;
}

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.

int pileVide(PileTab pile){


return (pile.sommet == 0) ? 1 : 0;
}

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.

int pilePleine(PileTab pile){


return (pile.sommet == NMAX) ? 1 : 0;
}

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.

Type sommetPile(PileTab pile){


if(!pileVide(pile))
return (pile.tab[pile.sommet - 1]);
}

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.

void depiler(PileTab * adrPile){


if(!pileVide(*adrPile)){
adrPile->sommet --;
}
}

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).

typedef char Type;

typedef struct Noeud Noeud;

struct Noeud{
Type info;
Noeud * suivant;
};

typedef Noeud * FileLst;

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

typedef int Type;

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.

void initialiserFile(FileLst *adrFile){


initialiserListe(adrFile);
}

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.

int fileVide(FileLst file){


return listeVide(file);
}

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.

Type teteFile(FileLst file){


if(!fileVide(file))
return teteListe(file);
}

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.

void initialiserFile(FileTab * adrFile){


adrFile->tete = 0;
adrFile->fin = 0;
}

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.

Type teteFile(FileTab file){


if(!fileVide(file))
return (file.tab[file.tete]);
}

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 (%).

void entrerElement(FileTab * adrFile, Type valElement){


if((adrFile->fin + 1) % NMAX != adrFile->tete){
adrFile->tab[adrFile->fin] = valElement;
adrFile->fin = (adrFile->fin + 1) % NMAX;
}
else
printf("Entrer : File pleine !!\n");
77 }
3.4.2 Opérations pour la modélisation
par tableau circulaire
 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 tableau circulaire, cela revient à
augmenter l'indice tete d'une unité.
 Il ne faut pas oublier de ramener tete à la première case du
tableau au cas où il a atteint la fin de ce dernier (%).

void sortirElement(FileTab * adrFile){


if(!(fileVide(*adrFile)))
adrFile->tete = (adrFile->tete + 1) % NMAX;
}

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

 Arbres non 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

 On peut aussi définir la hauteur de manière récursive :


 la hauteur d'un arbre est le maximum des hauteurs de ses fils.
 C'est à partir de cette définition que nous pourrons exprimer un
algorithme de calcul de la hauteur de l'arbre.
 La hauteur d'un arbre est très importante : c'est un repère de
performance.

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;

typedef struct Noeud Noeud;

struct Noeud {
Type info;
Noeud * filsG;
Noeud * filsD;
};

typedef struct Noeud * Arbre;

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 :

typedef int Type;

typedef struct Noeud Noeud;;

struct Noeud {
Type info;
Noeud * fils[4];
};

typedef struct Noeud * Arbre;

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;

typedef struct Noeud * Arbre;

typedef struct Cell * List;

typedef struct Cell{


Arbre fils;
List suivant;
}Cell;

typedef struct Noeud{


Type info;
List fils;
99 }Noeud;
4.3 Implémentation des arbres n-aire
 Ce type d'implémentation est déjà un peu plus compliqué, en
effet, nous avons une récursivité mutuelle.
 Le type List à besoin du type Arbre pour s'utiliser mais le type
Arbre a besoin du type List pour fonctionner.
 Le langage C autorise ce genre de construction du fait que le
type List et le type Arbre sont définis via des pointeurs
Noeud).
 Nous nous contenterons que de la première implémentation:
 l'implémentation des arbres binaires.
 Mais sachez qu'avec un peu d'adaptation, les algorithmes que
nous allons voir sont parfaitement utilisables sur des arbres n-
aires.
100
4.4 Les fonctions de base sur la
manipulation des arbres
 Afin de faciliter notre manipulation des arbres, nous allons
créer quelques fonctions.
 La première détermine si un arbre est vide.
➔Il est plus simple de comprendre la signification de
estVide(adrArbre) plutôt que adrArbre==NULL.

int estVide(Arbre * adrArbre){


return (*adrArbre == NULL) ? 1 : 0;
}

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:

Arbre * filsGauche(Arbre * adrArbre) {


if (estVide(adrArbre))
return NULL;
else
return &(*adrArbre)->filsG;
}

Arbre * filsDroit(Arbre * adrArbre) {


if (estVide(adrArbre))
return NULL;
else
return &(*adrArbre)->filsD;
}
102
4.4 Les fonctions de base sur la
manipulation des arbres
 Passons à une autre fonction qui peut nous être utile : savoir si
nous sommes sur une feuille.
int estFeuille(Arbre * adrArbre){
if(estVide(adrArbre))
return 0;
else
if(estVide(filsGauche(adrArbre)) &&
estVide(filsDroit(adrArbre)))
return 1;
else
return 0;
}

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.

unsigned hauteur(Arbre * adrArbre){


if(estVide(adrArbre))
return 0;
else
return 1 + max(hauteur(filsGauche(adrArbre)),

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 :

unsigned max(unsigned a,unsigned b){


return (a>b)? a : b ;
}

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.

unsigned nbNoeud(Arbre * adrArbre){


if(estVide(adrArbre))
return 0;
else
return 1 + nbNoeud(filsGauche(adrArbre))
+ nbNoeud(filsDroit(adrArbre));
}

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 :

unsigned nbFeuille(Arbre * adrArbre){


if(estVide(adrArbre))
return 0;
else
if (estFeuille(adrArbre) )
return 1;
else
return nbFeuille(filsGauche(adrArbre)) +

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.

unsigned nbNoeudInterne(Arbre * adrArbre){


if(estVide(adrArbre))
return 0;
else
if(estFeuille(adrArbre))
return 0;
else
return 1 + nbNoeudInterne(filsGauche(adrArbre))
+ nbNoeudInterne(filsDroit(adrArbre));
111 }
4.6 Parcours d'un arbre

 Les algorithmes de parcours d'un arbre permettent de visiter


tous les nœuds de l'arbre et éventuellement appliquer une
fonction sur ces nœuds.
 Nous distinguerons deux types de parcours :
 parcours en profondeur : permet d'explorer l'arbre en
explorant jusqu'au bout une branche pour passer à la suivante
 parcours en largeur : permet d'explorer l'arbre niveau par
niveau.

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

 Un parcours en profondeur à gauche est simple à


comprendre, cela signifie que l'on parcourt les branches de
gauche avant les branches de droite.
 On aura donc deux types de parcours :
 parcours à gauche
 parcours à droite
 Dans la plupart des cas, nous utiliserons un parcours à gauche.

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.

void traiterRacine(Arbre * adrArbre){


if(!estVide(adrArbre))
printf("%d ", (*adrArbre)->info);
else
printf("NULL ");
}

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 :

void DFS(Arbre * adrArbre, char type){


if(!estVide(adrArbre)){
if(type ==1){
traiterRacine(adrArbre);
}
DFS(filsGauche(adrArbre), type);
if(type == 2){
traiterRacine(adrArbre);
}
DFS(filsDroit(adrArbre), type);
if(type == 3){
traiterRacine(adrArbre);
}
}
120 }
4.6.1 Parcours en profondeur
 Ainsi à partir de ce code, on peut facilement créer trois
fonctions qui seront respectivement le parcours préfixe, le
parcours infixe et le parcours suffixe.
void DFSPrefix(Arbre * adrArbre){
DFS(adrArbre,1);
}
void DFSInfix(Arbre * adrArbre){
DFS(adrArbre,2);
}
void DFSPostfix(Arbre * adrArbre){
DFS(adrArbre,3);
}

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 :

void ParcoursEnLargeur(Arbre A){


Arbre Temp;
File F;
if(!estVide(A)){
emfiler(&F,T);
while(!estVide(F)){
Temp = teteFile(F);
sortirElement(&F);
/* Traiter la racine */
if(!estVide(filsGauche(&Temp)) )
emfiler(&F,Temp);
if(!estVide(filsDroit(&Temp)) )
emfiler(&F,Temp);
}
}
}

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 :

Arbre creerNoeud(Type val, Arbre ag, Arbre ad){


Arbre res;
res = (Arbre)malloc(sizeof(Noeud));
if(res == NULL){
printf("Impossible d'allouer le noeud");
return NULL;
}
res->info = val;
res->filsG = ag;
res->filsD = ad;
return res;
}

 Notre fonction renvoie NULL s'il a été impossible d'allouer le


nœud. Ceci est un choix arbitraire, vous pouvez très bien
133 effectuer d'autres opérations à la place.
4.7.2 Ajout d'un élément
 On distingue lors de l'ajout d'un élément plusieurs cas :
 Soit on insère dès que l'on peut
 Soit on insère de façon à obtenir un arbre qui se rapproche le
plus possible d'un arbre complet
 Soit on insère de façon à garder une certaine logique dans
l'arbre.

134
4.7.2 Ajout d'un élément
 Nous pouvons donc écrire le code C correspondant :

void ajouterNoeud(Arbre * adrArbre, Type val){


if (*adrArbre == NULL ){
*adrArbre = creerNoeud(val, NULL, NULL);
}
else{
if(estVide(filsGauche(adrArbre))){
(*adrArbre)->filsG = creerNoeud(val, NULL, NULL);
}
else{
if(estVide(filsDroit(adrArbre))){
(*adrArbre)->filsD = creerNoeud(val, NULL, NULL);
}
else{
ajouterNoeud(filsGauche(adrArbre), val);
}
}
}
135 }
4.7.2 Ajout d'un élément
 On remarque donc qu'ici nous créons des arbres qui ne sont pas
performant (nous verrons ceci dans la recherche d'élément).
 Afin d'améliorer ceci, on peut éventuellement effectuer notre
appel récursif soit à gauche soit à droite et ceci de façon aléatoire.
 Ceci permet un équilibrage des insertions des nœuds mais ce n'est
pas parfait.
 La solution pour obtenir des arbres les plus équilibrés possible
consiste en l'utilisation d'arbres binaires dit rouge-noir.
 Ceci est assez compliqué et nous ne les verrons pas.
 Cependant, nous allons voir un type d'arbre qui facilite les
recherches :
 les arbres binaires de recherche.

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 :

int existeArbreSimple(Arbre * adrArbre, Type val){


if(estVide(adrArbre)){
return 0;
}
else{
if((*adrArbre)->info == val){
return 1;
}
else{
return (existeArbreSimple(filsGauche(adrArbre), val) ||
existeArbreSimple(filsDroit(adrArbre), val));
}
}
}

142
4.7.3 Recherche dans un arbre

 Nous renvoyons un ou logique entre le sous arbre gauche et


le sous arbre droit, pour pouvoir renvoyer vrai si l'élément
existe dans l'un des sous arbres et faux sinon.
 Ce genre de recherche est correct mais n'est pas très
performant.
 En effet, il faut parcourir quasiment tous les nœuds de l'arbre
pour déterminer si l'élément existe.

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 :

int existArbreRecherche(Arbre *adrArbre, Type val){


if(estVide(adrArbre)){
return 0;
}
else{
if((*adrArbre)->info == val){
return 1;
}
else{
if ((*adrArbre)->info > val){
return existArbreRecherche(filsGauche(adrArbre), val);
}
else{
return existArbreRecherche(filsGauche(adrArbre), val);
}
}
}
145 }
4.7.4 Suppression d'un arbre
 Par suppression de nœud, nous entendrons suppression d'une
feuille.
 En effet, un nœud qui possède des fils s'il est supprimé, entraîne
une réorganisation de l'arbre.
 Que faire alors des sous arbres du nœud que nous voulons
supprimer ?
 La réponse à cette question dépend énormément du type
d'application de l'arbre.
 On peut les supprimer, on peut réorganiser l'arbre (si c'est un
arbre de recherche) en y insérant les sous arbres qui sont
devenus orphelins.
 Bref, pour simplifier les choses, nous allons nous contenter de
supprimer complètement l'arbre.
146
4.7.4 Suppression d'un arbre

 L'algorithme de suppression de l'arbre est simple :


 on supprime les feuilles une par unes.
 On répète l'opération autant de fois qu'il y a de feuilles.
 Cette opération est donc très dépendante du nombre de nœud.

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é :

void supprimerNoeud(Arbre * adrArbre){


Arbre * ag = filsGauche(adrArbre);
Arbre * ad = filsDroit(adrArbre);
if(!estVide(adrArbre)){
supprimerNoeud(ag);
supprimerNoeud(ad);
free(*adrArbre);
*adrArbre = NULL;
}
}

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

Vous aimerez peut-être aussi