Structure en C
Structure en C
Structures de données
1
Plan du cours
Rappel du langage C
Listes chainées
Listes doubles chainées
Pile/file
Arbres
Tables de hachage
Algorithme de tri
2
Plan de la séance
float réel 4 o
double réel 8 o
Les nombres de type double sont codés sur 64 bits dont :
Noter qu’en C, les crochets sont réservés aux tableaux et que les
parenthèses sont réservées aux arguments de fonctions.
5
Les tableaux
L’initialisation d’un tableau peut être faite dans la déclaration, par exemple
double taba[3]={0,1,2};
double tab[10]={1,1,1};
/* les 7 autres éléments sont mis à 0 */
double tabc[]={10,7,4};
Dans la dernière ligne, la dimension a été omise ; cela signifie qu’un tableau
de trois éléments est créé.
}
if, case, switch, break
11
Comparaison et assignation
12
Comparaison et assignation
Par exemple
if (i==1){...} else {...}
signifie que si i est égal à 1 le premier groupe d’instructions
est exécuté
13
Comparaison et assignation
14
Alloc. dynamique, pointeurs
Le langage C permet de modifier l’espace mémoire disponible pour le
programme, à condition bien entendu que la machine puisse en fournir.
Une fois une variable déclarée et initialisée, par exemple une variable a, on
peut avoir accès à l’adresse de cette variable par l’opérateur d’indirection &.
16
Allocation dynamique
Un pointeur est une variable qui contient l’adresse d’une variable d’un
type donné. La déclaration d’un pointeur se fait de la manière suivante ;
type du pointeur, opérateur * et le nom du pointeur
double *b;
17
double * a=NULL;
Allocation dynamique
on utilise la fonction système malloc (ou calloc) dont la déclaration a été faite
dans le fichier d’en-tête stdlib.h.
Une manière simple de le savoir est de vérifier une fois l’allocation dynamique
effectuée que le pointeur a possède la valeur d’une adresse 19différente de la
valeur assignée au départ.
Malloc, calloc et realloc
Allocation et Initialisation :
malloc alloue la mémoire sans l’initialiser.
calloc alloue la mémoire et l’initialise à zé[Link] modifie la taille d’un bloc
mémoire déjà alloué, en conservant le contenu.
Performance :
malloc est généralement plus rapide que calloc parce qu'il n'initialise pas la
mémoire.
calloc prend un peu plus de temps en raison de l'initialisation à zéro.
Utilité :
Utilisez malloc lorsque vous n'avez pas besoin d’une initialisation immédiate de la
mémoire.
Utilisez calloc lorsque vous avez besoin d’une mémoire initialisée à zéro.
Utilisez realloc pour ajuster dynamiquement la taille de la mémoire allouée.
20
Allocation dynamique
Par exemple, l’instruction suivante placée après l’instruction malloc
conduit à une interruption du programme si l’allocation dynamique n’a
pas fonctionné, ce qui évite un arrêt dans l’exécution à un endroit du
programme où l’on fait appel à un tableau qui n’a pas pu être créé.
if (a == NULL)
{fprintf(stderr," l’allocation dynamique n’a pas fonctionnée \n");
return(EXIT_FAILURE);}
Une fois créé, les éléments du tableau sont accessibles à partir d’une
syntaxe à fait identique à celle des tableaux statiques.
Avant de quitter le programme il est nécessaire de libérer la mémoire qui
a été demandée par l’utilisateur. L’instruction correspondante est alors
free(a);
21
Allocation dynamique
Attention au fait qu’il ne faut libérer la mémoire réservée pour le tableau
a qu’une seule fois.
En résumé
Déclaration d’un pointeur (avec son type)
Appel de la fonction malloc
Vérification que la mémoire est bien allouée
Libération de la mémoire à la fin du programme.
22
Allocation dynamique
Il est possible de définir des types plus élaborés et aussi de définir des
types à l’aide de l’instruction typedef.
23
Allocation dynamique
On peut aussi définir des types nouveaux à travers des structures. Par exemple
typedef struct {
long i;
double x,y;
} num_pos_points;
Une fois ainsi défini en en-tête du programme, on peut utiliser ce type comme un type
habituel.
Soit la déclaration de la structure cc
num_pos_points cc;
La syntaxe pour l’affectation des membres de la structure sera la suivante
cc.i=5;
cc.x=0.6;
cc.y=0.7; 24
Allocation dynamique
Pour le programme
for(int j=0;j<10;j++)
{ cc[j].i=5*j;
cc[j].x=0.6*j;
cc[j].y=0.7*j;
}
25
Allocation dynamique
#include<stdlib.h>
#include<stdio.h>
typedef struct {
int i;
double x,y; Pour un tableau créé dynamiquement, la
} num_pos_points; syntaxe du programme reste identique.
main(){
int j;
num_pos_points *cc=NULL;
cc= ( num_pos_points *) malloc(sizeof( num_pos_points)*10);
if (cc == NULL)
{
fprintf(stderr," l’allocation dynamique n’a pas fonctionnée \n");
return(EXIT_FAILURE);
}
for( j=0;j<10;j++)
{
cc[j].i=j;
cc[j].x=0.6*j;
cc[j].y=0.7*j;
}
free(cc);
exit(EXIT_SUCCESS);} 26
#include<stdlib.h>
#include<stdio.h>
typedef struct {
int i;
double x,y;
} num_pos_points;
main(){
int j;
num_pos_points *cc=NULL;
cc= ( num_pos_points *) malloc(sizeof( num_pos_points)*10);
if (cc == NULL)
{
fprintf(stderr," l’allocation dynamique n’a pas fonctionnée \n");
return(EXIT_FAILURE);
}
for( j=0;j<10;j++)
{
cc[j].i=j;
cc[j].x=0.6*j;
cc[j].y=0.7*j;
}
27
free(cc);
exit(EXIT_SUCCESS);}
Variables, des tableaux et des
fonctions
Par construction, quand on écrit un programme, on a des objets qui sont
généralement des nombres, des tableaux de nombres voire des tableaux de
structure de nombres. Les fonctions que l’on crée permettent soit de modifier
ces tableaux, soit de calculer des nouveaux nombres.
Pour ce qui est des variables d’entrée, la situation est simple car le langage C
passe les variables par valeur, ce qui signifie qu’il recopie les valeurs
transmises dans un espace de la mémoire propre à la fonction et que cet
emplacement sera a priori libéré une fois cette fonction exécutée.
Cette règle a l’avantage de ne pas modifier les variables d’entrée sans que
l’on s’en aperçoive.
Si l’on veut que la variable soit une variable de sortie, on a deux solutions :
soit on donne un type à la fonction ce qui fait que la valeur sera récupérée
dans le programme appelant, soit on transmet la valeur de l’adresse ce qui
permet de modifier l’état de la variable initialement déclarée dans le
programme appelant.
29
Variables, tableaux et
fonctions
Pour les tableaux, on transmet toujours l’adresse d’un
élément du tableau (généralement le premier) et le
sous-programme peut modifier les valeurs du tableau
transmis dans les arguments de la fonction.
30
Variables, tableaux et fonctions
#include <stdio.h>
#include <math.h>
#include <stdlib.h>
void inittab(double *,int );
int main(){
double x[10];
inittab(x,10);
En exécutant cet exemple, on peut vérifier que le
for(int i=0;i<10;i++)
tableau du programme principal a été modifié par
{
la fonction inittab
printf("%d %10.5e\n",i,x[i]);
}
exit(EXIT_SUCCESS);
}
void inittab(double x[],int n)
{
unsigned int i;
for (i=0;i<n;i++)
x[i] = cos( (double) i *M_PI / (double) n);
}
31
#include <stdio.h>
#include <math.h>
#include <stdlib.h>
void inittab(double *,int );
int main(){
double x[10];
inittab(x,10);
for(int i=0;i<10;i++)
{
printf("%d %10.5e\n",i,x[i]);
}
exit(EXIT_SUCCESS);
}
void inittab(double x[],int n)
{
unsigned int i;
for (i=0;i<n;i++)
x[i] = cos( (double) i *M_PI / (double) n);
}
32
La spécification de type :
const
Si la fonction ne doit pas modifier les valeurs du tableau
transmis dans les arguments de la fonction, une
disposition provenant du C++ a été introduite en C pour
sécuriser l’écriture des programmes.
33
#include <stdio.h>
#include <math.h>
#include <stdlib.h>
void inittab( double *,int );
void affiche (const double *,int);
int main(){
const int n=10;
double x[n];
inittab(x,n);
affiche(x,n);
exit(EXIT_SUCCESS);
}
void inittab( double x[], int n)
{
for (int i=0;i<n;i++) x[i]=cos((double) i *M_PI/(double) n);
}
void affiche (const double x[],int n)
{
34
for(int i=0;i<n;i++) printf("%d %10.5e\n",i ,x[i]);
}
La spécification de type :
const
En plaçant la directive const à chaque fois que le
tableau ne doit pas être modifié, on s’assure que la
fonction n’introduira d’effets indésirables.
35
Fonctions
Il reste le cas des fonctions qui sont passées comme argument. Appelons la
fonction appelante la fonction func_1 et la fonction appelée la func_2.
Ce pointeur est un peu particulier car il doit être déclaré avec le nombre et
le type de variables de la func_2.
36
/* protopypes */
double func_2(double);
double func_1(double,double,long, double (*)(double));
/*
double func_1(double a, double b,long n, double (*f)(double));
*/
}
/* dans le programme, l’appel sera */
func_1(a,b,n,func_2);
37
LISTES
CHAÎNÉES
Pr. Karima AISSAOUI
[Link]@[Link]
Introduction
Si j'ajoute un élément
(on parle d'empilage),
il sera placé au-dessus,
comme dans Tetris
Dépilage
1
Définition
2
Définition
3
Implémentation
La première structure permet de représenter un 'node' (élément) de la
liste chaînée.
4
Implémentation
5
Manipulation d'une
liste doublement
chainée
6
Allouer une nouvelle liste
7
Ajout en fin de liste
8
Ajout en fin de liste
1. Tout d'abord, il faut vérifier que la liste n'est pas NULL.
2. Si elle ne l'est pas, nous allons créer un nouvel élément (nouveau node).
Une fois celui-ci créé,
nous devons stocker la donnée dans le champ donnée (data)
puis faire pointer p_next vers NULL car ce sera le dernier élément de la liste.
3. A partir de là, deux possibilités s'offrent :
S'il n'existe pas de dernier élément (donc la liste est vide) Alors
Nous faisons pointer p_prev vers NULL
Nous faisons pointer la tête et la fin de liste vers notre nouvel élément
Sinon
Nous rattachons le dernier élément de notre liste à notre nouvel élément
Nous faisons pointer p_prev vers le dernier élément de notre liste
Nous faisons pointer notre fin de liste vers notre nouvel élément
4. Enfin, nous incrémentons notre champ length de notre liste puis nous
retournons la liste. Tout ceci constitue alors l'algorithme9 d'ajout en fin de
liste.
Ajout en fin de liste
10
Ajout en début de liste
11
Insérer un élément
Maintenant, si l'on désire ajouter un élément n’importe où dans notre liste ?
Tout d'abord, nous aurons besoin de parcourir notre liste.
Nous aurons aussi besoin d'un compteur (que l'on nommera) « i » afin de nous
arrêter à la position où nous souhaitons insérer notre nouvel élément.
Il faut alors réfléchir des différents cas de figure qui peuvent intervenir
lorsque nous aurons trouvé notre position:
Soit nous sommes en fin de liste
Soit nous sommes en début de liste
Soit nous sommes en milieu de liste
Cependant, les deux premiers cas sont très faciles à traiter. Enfin, nous
disposons de fonctions permettant d'ajouter un élément en début et en fin
de liste, il nous suffit donc de les réaliser(appeler).
Il faut alors gérer le cas où nous nous trouvons en milieu de liste.
12
Insérer un élément
13
Insérer un élément
Il faut tout d'abord relier les éléments suivants et précédents au nouvel
élément
puis, inversement, il faut relier le nouvel élément aux éléments suivant et
précédent.
A noter qu'il sera nécessaire d'avoir préalablement créer un nouvel élément sans quoi le
chaînage ne pourra pas avoir lieu.
Le chaînage va alors se dérouler en 4 étapes.
Pour parcourir la liste, il faut récupérer le pointeur vers le début de liste dans un
pointeur temporaire.
C'est ce pointeur temporaire qui servira à parcourir la liste.
Schématiquement, la liste sera parcourue de gauche à droite.
Le compteur sera bien évidemment incrémenté lors du parcours de chaque maillon
de la liste.
14
15
Insérer un élément
Pour parcourir la liste,
nous utilisons un pointeur nommé p_temp. Au tout début, celui-ci pointe vers le
premier élément de notre liste (p_list->p_head).
nous utilisons une structure de type while.
Tant que nous n'avons pas atteint la fin de liste (p_temp != NULL) et tant que
nous ne sommes pas à la position où nous voulons insérer notre élément
(position <= i), nous bouclons.
Dès lors que nous avons atteint notre position (position == i), nous devons
alors effectuer nos trois tests :
Si nous sommes en fin de liste (p_temp->p_next == NULL), nous utilisons la
fonction d’ajout en fin de liste
Sinon, si nous sommes en début de liste (p_temp->p_prev == NULL), nous
utilisons la fonction en début de liste
Sinon, il faut créer un nouvel élément et réaliser le chaînage sans oublier de
stocker la donnée dans le champ data
Enfin, si nous n'avons pas encore atteint notre position, nous passons à
l'élément suivant (p_temp = p_temp->p_next).
16
Supprimer un élément selon sa valeur
Supprimer un élément en fonction de sa valeur.
Voici un schéma pour mieux visualiser le procédé:
17
Supprimer un élément selon sa valeur
Ici, nous décidons de supprimer l'élément portant la valeur 15.
Tout d'abord, il faut parcourir la liste à la rechercher de l’élément à
supprimer. Dès que l'on aura trouvé la valeur correspondante, trois
possibilités s’offriront à nous:
l'élément se trouve en fin de liste
l'élément se trouve en début de liste
l'élément se trouve en milieu de liste
18
Supprimer un élément selon sa valeur
Si l'élément se trouve en fin de liste, Alors il faut faire pointer notre p_tail
vers l'avant dernier élément et faire pointer le pointeur vers l'élément
suivant de l'avant dernier élément vers NULL.
Sinon, si l'élément se trouve en début de liste, Alors il faudra faire pointer
notre p_head vers le second élément et faire pointer le pointeur vers
l'élément précédent du second élément vers NULL.
Sinon, il faudra relier l'élément précédent à l'élément que l'on veut
supprimer vers l'élément suivant à l'élément que l'on veut supprimer et il
faudra aussi relier l'élément suivant à l'élément su l'on veut supprimer vers
l'élément précédent à l'élément que l'on veut supprimer.
Une fois ceci fait, il ne nous restera plus qu'à supprimer notre élément
trouver et à décrémenter la taille de notre liste.
A noter cependant que nous utilisons une variable supplémentaire nommée
« found » pour nous arrêter au premier élément trouvé. Lorsque l'élément
est trouvé, cette variable change d'état et prend la valeur « 1 », marquant
ainsi l'arrêt de la boucle de parcours.
19
20
Supprimer un élément selon sa
position(Exercice)
Nous allons créer une fonction permettant de supprimer le nème élément
d'une liste doublement chaînée.
Il s'agit exactement du même procédé utilisé précédemment, mis à part le
fait que nous aurons besoin d'une variable supplémentaire nous permettant
de stocker la position à laquelle nous nous trouvons.
21
Recherche un élément selon sa valeur
Nous allons parcourir notre liste tant que nous n'aurons pas trouvé notre
élément (variable found).
Si nous trouvons notre élément, nous utilisons alors les fonctions déjà à
notre disposition (dlist_new et dlist_append) pour créer notre liste qui
sera retourné.
22
23
Recherche un ensemble d'éléments selon une
même valeur
Il faut créer une liste.
Si on trouve un élément, on l’initialise.
Puis ajouter les éléments trouvés dedans
24
25
Libérer une liste
26
Libérer une liste
Dans cette fonction, nous utilisons un double pointeur. En effet, notre
fonction delete doit directement effectuer les modifications sur la liste.
Autrement dit, celle-ci doit faire des modifications sur un objet de type
Dlist *.
En premier lieu, nous vérifions si la liste que nous avons récupérée n'est pas
NULL.
Il faut parcourir ensuite chaque élément de la liste (à noter la présence de
parenthèses afin de résoudre la priorité de l'opérateur -> sur l'opérateur *).
Seulement, nous prenons garde de sauvegarder l'élément courant dans un
pointeur p_del (qui sera l'élément que nous supprimerons).
En effet, si nous n'utilisons pas de pointeur intermédiaire, lorsque nous
supprimerons notre premier élément, p_temp->p_next n'existera plus
puisque notre élément aura été supprimé, nous aurons donc un plantage.
➔ C'est pour cela que nous sauvegardons d'abord notre élément courant, puis
nous passons à l'élément suivant et enfin nous supprimons notre élément
courant (p_del).
27
Quand tous les éléments sont supprimés, nous terminons par supprimer
notre liste puis nous la remettons à NULL.
Arbres
Pr. Karima AISSAOUI
[Link]@[Link]
1
Arbres généraux
Un arbre est une structure composée d’éléments appelés nœuds ; il est constitué d’un
nœud particulier appelé racine et d’une suite ordonnée éventuellement vide A1, A2, …,
Ap d’arbres disjoints appelés sous-arbres de la racine.
Un arbre contient donc au moins un nœud : sa racine.
2
Utilité des arbres
Les arbres sont des structures de données essentielles en C et en programmation en général,
car ils permettent de stocker, organiser et manipuler des données hiérarchiquement. Ils
permettent:
Recherche rapide et efficace
Organisation hiérarchique comme les fichiers dans un système de fichiers, les organisations
d'entreprise, ou les arbres de décision.
Représentation des expressions: es arbres binaires sont souvent utilisés pour représenter des
expressions arithmétiques, où les nœuds internes représentent des opérateurs (+, -, *, /) et les
feuilles représentent les opérandes.
Gestion des priorités
Parcours efficaces
Compression de données (algorithme de Huffman)
Réseaux et graphes
3
Exemple
la figure suivante représente l’arbre d’une descendance ; la racine de cet arbre contient
Yolande ; il est constitué de trois sous-arbres (p = 3) :
A1, de racine Claire, contient Claire et Hortense ;
A2, de racine Jean, contient Jean, Élisabeth, Jacques, Lou et Mandé ;
A3, de racine Colette, est réduit à Colette.
L’arbre est ici dessiné de haut en bas (la racine est en haut) et les sous-arbres de gauche à droite
;
4
Définitions
Plusieurs définitions sont à retenir ; nous illustrons les définitions avec
l’exemple précédent en identifiant un nœud avec la chaîne de caractères
qu’il contient :
les fils d’un nœud sont les racines de ses sous-arbres ; sur l’exemple, les fils de Jean
sont Élisabeth et Jacques ;
le père d’un nœud x autre que la racine est l’unique nœud dont x est un fils ; sur
l’exemple, Jacques est le père de Lou et de Mandé ;
la racine d’un arbre n’a pas de père ;
un nœud interne est un nœud qui a au moins un fils ; sur l’exemple, Claire est un
nœud interne ;
une feuille d’un arbre est un nœud sans fils ; sur l’exemple, Mandé est une feuille ;
les ancêtres d’un nœud « a » sont les nœuds qui sont sur le chemin entre la racine
(incluse) et « a » (inclus) ; les ancêtres de « a » différents de « a » sont les ancêtres
propres de « a » ; sur l’exemple, les ancêtres de Jacques sont Jacques, Jean et
Yolande ; 5
Définitions
les descendants d’un nœud « a » sont les nœuds qui appartiennent au sous-
arbre de racine « a » ; les descendants de « a » différents de « a » sont les
descendants propres de « a » ; sur l’exemple, les descendants de Jean sont
Jean, Élisabeth, Jacques, Lou et Mandé.
la profondeur d’un nœud est la longueur du chemin allant de la racine à ce
nœud ;
la profondeur de la racine est donc nulle et la profondeur d’un nœud autre
que la racine vaut un de plus que la profondeur de son père ;
la profondeur d’un nœud peut aussi être appelée niveau ou hauteur du nœud
; sur l’exemple, la profondeur d’Élisabeth vaut 2 ;
la hauteur d’un arbre est la profondeur maximum de ses nœuds ; la hauteur
de l’arbre donné en exemple vaut 3.
6
Pour connaître un arbre, on procède généralement en mémorisant la racine
(ou plus précisément son adresse) ; il suffit alors que chaque nœud connaisse
ses nœuds fils pour pouvoir retrouver tout l’arbre.
7
Parcours d’un arbre général
Parcourir un arbre, c’est examiner une fois et une seule chacun de ses nœuds, l’ordre des
examens dépendant d’une certaine règle.
On distingue les deux types suivants.
8
Parcours en préordre
Parcourir un arbre en préordre, c’est :
Examiner la racine ;
Parcourir en préordre le premier sous-arbre ;
…
parcourir en préordre le dernier sous-arbre.
Exemple : le parcours en préordre de l’arbre donné en exemple considère successivement les
nœuds Yolande, Claire, Hortense, Jean, Élisabeth, Jacques, Lou, Mandé, Colette.
9
Parcours en postordre
Parcourir un arbre en postordre, c’est :
parcourir en postordre le premier sous-arbre ;
…
parcourir en postordre le dernier sous-arbre ;
examiner la racine.
Exemple : le parcours en postordre de l’arbre donné en exemple considère successivement les nœuds
Hortense, Claire, Élisabeth, Lou, Mandé, Jacques, Jean, Colette, Yolande.
On part de la racine et on suit le tracé de l’arbre comme l’indique la figure ci-dessous. On s’aperçoit
que l’examen en préordre est obtenu en examinant les nœuds à la première rencontre alors que
l’examen en postordre est obtenu en examinant les nœuds à la dernière rencontre.
10
Arbres binaires
Un arbre binaire est soit vide, soit constitué d’un nœud particulier appelé racine et de deux
sous-arbres binaires disjoints appelés sous-arbre gauche et sous-arbre droit.
11
Exemple d’arbre binaire
La terminologie est la même que celle des arbres généraux ; néanmoins, si on considère
les fils d’un nœud, on parlera, s’ils existent, du fils gauche et du fils droit.
Sur l’exemple, on remarque que certains nœuds peuvent avoir seulement un fils droit ou
seulement un fils gauche ;
Par exemple, le nœud « e » (i.e. contenant la donnée e) admet uniquement un fils gauche,
le nœud « i », et pas de fils droit ;
12
Arbres binaires
Pour coder un arbre binaire, on fait souvent correspondre à chaque nœud une structure
contenant la donnée et deux adresses, une adresse pour chacun des deux nœuds fils.
Il suffit alors de mémoriser l’adresse de la racine pour pouvoir reconstituer tout l’arbre.
Une adresse nulle indique un arbre binaire vide.
13
Remarque
On code souvent un arbre général en utilisant un arbre binaire ;
On associe à chaque nœud de l’arbre initial un nœud de l’arbre binaire ;
Le fils gauche d’un nœud dans l’arbre binaire correspond au premier fils du nœud
correspondant de l’arbre initial (on met une adresse nulle s’il n’y a aucun fils)
Alors que le fils droit de ce nœud correspond au premier « frère cadet », c’est-à-
dire au nœud de l’arbre initial qui a même père que le nœud considéré et qui se
trouve immédiatement à sa droite (on met une adresse nulle s’il n’y a aucun frère
cadet).
14
Remarque: Arbres général vs Arbre binaire
Ainsi, l’arbre général donné en exemple précédemment et retracé ci-dessous à gauche peut
être codé avec l’arbre binaire représenté à droite, en changeant bien entendu l’interprétation
des liens.
15
Parcours(Le parcours en ordre symétrique)
Les deux parcours définis pour les arbres généraux sont aussi définis pour les arbres binaires.
Pour l’exemple ci-dessus, le parcours en préordre donne l’ordre : c, b, d, e, i, f, a, g, h et le parcours
en postordre donne l’ordre : d, i, e, b, a, h, g, f, c.
On considère pour les arbres binaires un troisième ordre : l’ordre symétrique. Parcourir un arbre
binaire en ordre symétrique, c’est : si l’arbre binaire est non vide, alors
parcourir en ordre symétrique le sous-arbre gauche ;
examiner la racine ;
parcourir en ordre symétrique le sous-arbre droit.
16
Parcours(Le parcours en ordre symétrique)
17
Exemple d’application
On peut utiliser un arbre binaire pour représenter une expression
arithmétique composée à partir des entiers et des quatre opérateurs binaires
: +, –, ×, /.
L’arbre ci-dessous représente l’expression : (14 + (8 / 2)) × (5 – 3)
18
Exemple d’application
Par construction, dans cet arbre, tout nœud interne a un fils gauche et un fils droit.
Les trois parcours de cet arbre donnent :
Parcours en préordre : × + 14 / 8 2 – 5 3 ; La forme polonaise de l’expression donne 14 + 8 / 2 × 5 – 3
;
parcours en ordre infixe : 14 + 8 / 2 × 5 – 3 ; il faudrait ajouter les parenthèses pour retrouver
l’expression initiale ;
parcours en postordre : 14 8 2 / + 5 3 – × ; La forme polonaise inverse de l’expression donne 14 + 8 /
2×5–3.
19
Quelques arbres binaires particuliers
Un arbre binaire est dit complet si tout nœud interne a exactement deux fils.
20
Quelques arbres binaires particuliers
Un arbre binaire est dit parfait si, en appelant h la hauteur de l’arbre, les niveaux de
profondeur 0, 1, …, h – 1 sont complètement remplis alors que le niveau de profondeur
h est rempli en partant de la gauche ;
Un arbre est parfait si tous ses niveaux sont remplis sauf éventuellement le dernier
niveau qui a toutes ses feuilles ramenées complètement à gauche.
21
Quelques arbres binaires particuliers
la structure d’un arbre binaire parfait est complètement déterminée par son nombre de
nœuds ; nous donnons ci-après les arbres binaires parfaits à 5 nœuds et à 12 nœuds.
22
Quelques arbres binaires particuliers
23
Arbres binaires de recherche
Un arbre binaire de recherche est un arbre binaire qui possède la propriété fondamentale
suivante:
Tous les nœuds du sous-arbre de gauche d'un nœud de l'arbre ont une valeur inférieure ou égale à la
sienne.
Tous les nœuds du sous-arbre de droite d'un nœud de l'arbre ont une valeur supérieure ou égale à la
sienne.
24
Recherche dans l'arbre
Un arbre binaire de recherche est fait pour faciliter la recherche
d'informations.
La recherche d'un nœud particulier de l'arbre peut être définie simplement
de manière récursive:
Soit un sous-arbre de racine « 𝑛𝑖 »,
si la valeur recherchée est celle de la racine « 𝑛𝑖 », alors la recherche est terminée. On a
trouvé le nœud recherché.
sinon,
si « 𝑛𝑖 » est une feuille (pas de fils) alors la recherche est infructueuse et l'algorithme se
termine.
si la valeur recherchée est plus grande que celle de la racine alors on explore le sous-arbre de
droite c'est à dire que l'on remplace « 𝑛𝑖 » par son nœud fils de droite et que l'on relance la
procédure de recherche à partir de cette nouvelle racine.
De la même manière, si la valeur recherchée est plus petite que la valeur
25 de « 𝑛𝑖 », on
remplace « 𝑛𝑖 » par son nœud fils de gauche avant de relancer la procédure.
Ajout d'un élément
Pour conserver les propriétés d'un arbre binaire de recherche nécessite
de, l'ajout d'un nouvel élément ne peut pas se faire n'importe
comment.
L'algorithme récursif d'ajout d'un élément peut s'exprimer ainsi:
Soit v la valeur de l’élément à insérer.
soit x la valeur du nœud racine « 𝑛𝑖 » d'un sous-arbre.
Si « 𝑛𝑖 » n'existe pas, le créer avec la valeur v. fin.
sinon
si v est plus grand que x,
remplacer « 𝑛𝑖 » par son fils droit.
recommencer l'algorithme à partir de la nouvelle racine.
sinon
remplacer « 𝑛𝑖 » par son fils gauche.
recommencer l'algorithme à partir de la nouvelle racine. 26
Ajout d'un élément
27
Implémentation
En langage C, un nœud d'un arbre binaire peut être représente par une structure
contenant un champ donnée et deux pointeurs vers les nœuds fils:
28
Implémentation
La fonction d'insertion qui permet d'ajouter un élément dans l'arbre et donc de le créer de manière à ce
qu'il respecte les propriétés d'un arbre binaire de recherche peut s‘écrire ainsi:
29
Suppression
L’algorithme permettant de supprimer un nœud d’un arbre est le
suivant:
Si le nœud à supprimer n’a pas de fils ➔ il suffit de le supprimer.
Si le nœud à supprimer a un seul fils ➔ On le remplace par son fils et on
supprime ce dernier.
Si le nœud à supprimer a deux fils ➔ On le remplace par l’extrémité du bord
gauche du sous-arbre droit.
30
Les opérations sur les arbres
31
LES TABLES DE HACHAGE
Pr. Karima AISSAOUI
[Link]@[Link]
1
Introduction
▪ Les listes chaînées ont un gros défaut lorsqu'on souhaite lire ce qu'elles contiennent :
il n'est pas possible d'accéder directement à un élément précis. Il faut parcourir la
liste en avançant d'élément en élément jusqu'à trouver celui qu'on recherche.
➔ Cela pose des problèmes de performance dès que la liste chaînée devient
volumineuse. « Imaginez une liste chaînée de 1 000 éléments où celui que l'on recherche
est tout à la fin ! »
▪ Les tables de hachage représentent une autre façon de stocker des données.
➔ Elles sont basées sur les tableaux du langage C.
➔ Leur gros avantage : Elles permettent de retrouver instantanément un élément précis,
que la table contient 100, 1 000, 10 000 cases ou plus encore !
2
Pourquoi utiliser une table de hachage ?
Les listes chaînées sont particulièrement souples, nous avons pu le constater : il est possible
d'ajouter ou de supprimer des cases à tout moment, alors qu'un tableau est « figé » une fois
qu'il a été créé.
Toutefois, les listes chaînées ont quand même un gros défaut : si on cherche à récupérer un
élément précis de la liste, il faut parcourir celle-ci en entier jusqu'à ce qu'on le retrouve !
3
Pourquoi utiliser une table de hachage ?
Prenons une liste chaînée qui contient des informations sur des élèves :
leur nom,
leur âge et
leur moyenne.
Chaque élève sera représenté par une structure « Eleve »
Si je veux retrouver les informations sur Luc Doncieux dans la fig. suivante, il
va falloir parcourir toute la liste pour se rendre compte qu'il était à la fin !
4
Pourquoi utiliser une table de hachage ?
Dans cet exemple, notre liste chaînée ne contient que quatre éléments. Mais imaginez
maintenant que celui-ci se trouve à la fin d'une liste chaînée contenant 10 000
éléments ! Ce n'est pas acceptable de devoir parcourir jusqu'à 10 000 éléments pour
retrouver une information. C'est là que les tables de hachage entrent en jeu.
5
Qu'est-ce qu'une table de hachage ?
Les tableaux ne connaissaient pas ce problème. Ainsi, pour accéder à l'élément d'indice 2
dans mon tableau, il me suffisait d'écrire ceci :
6
Qu'est-ce qu'une table de hachage ?
Donc, les listes chaînées sont plus flexibles. Les tableaux, eux, permettent un
accès plus rapide.
➔ Les tables de hachage constituent quelque part un compromis entre les deux.
Mais, Il y a un défaut important avec les tableaux : les cases sont identifiées
par des numéros qu'on appelle des indices.
➔ Il n'est pas possible de demander à l'ordinateur les données qui se trouvent à
la case "Luc Doncieux" .
➔ Pour retrouver l'âge et la moyenne de Luc Doncieux, on ne peut donc pas
écrire :
tableau["Luc Doncieux"];
7
Qu'est-ce qu'une table de hachage ?
Ce serait pourtant pratique de pouvoir accéder à une case du tableau rien qu'avec le nom
! Eh bien avec les tables de hachage, c'est possible.
➔ Les tables de hachage ne font pas « partie » du langage C. Il s'agit d'un concept.
➔ Puisque notre tableau doit forcément être numéroté par des indices, comment fait-on
pour retrouver le bon numéro de case si on connaît seulement le nom « Luc Doncieux » ?
8
Qu'est-ce qu'une table de hachage ?
En effet, un tableau reste un tableau et celui-ci ne fonctionne qu'avec des
indices numérotés. Imaginez un tableau correspondant à la fig. suivante : chaque
case a un indice et possède un pointeur vers une structure de type Eleve.
9
Qu'est-ce qu'une table de hachage ?
Si on veut retrouver la case correspondant à Luc Doncieux, il faut pouvoir
transformer son nom en indice du tableau. Ainsi, il faut pouvoir faire
l'association entre chaque nom et un numéro de case de tableau :
Julien Lefebvre = 0 ;
Aurélie Bassoli = 1 ;
Yann Martinez = 2 ;
Luc Doncieux = 3.
Mais On ne peut pas écrire tableau["Luc Doncieux"]. Ce n'est pas valide en C.
10
Qu'est-ce qu'une table de hachage ?
La question est : comment transformer une chaîne de caractères en numéro ?
Il faut écrire une fonction qui prend en entrée une chaîne de caractères, fait des
calculs avec, puis retourne en sortie un numéro correspondant à cette chaîne.
Ce numéro sera l'indice de la case dans notre tableau.
11
Écrire une fonction de hachage
Prenons donc un tableau de 100 cases dans lequel on va stocker des pointeurs
vers des structures Eleve.
Eleve* tableau[100];
12
Écrire une fonction de hachage
Nous devons écrire une fonction qui, à partir d'un nom, génère un nombre compris entre 0
et 99 (les indices du tableau).
Il existe des méthodes mathématiques pour « hacher » des données, c'est-à-dire les
transformer en nombres.
Les algorithmes MD5 et SHA1 sont des fonctions de hachage célèbres.
Il est possible d’inventer une fonction de hachage. Ici, pour faire simple, on propose
d'additionner les valeurs ASCII de chaque lettre du nom, c'est-à-dire pour Luc Doncieux
faire la somme suivante :
'L' + 'u' + 'c' + ' ' + 'D' + 'o' + 'n' + 'c' + 'i' + 'e' + 'u' + 'x'
13
Écrire une fonction de hachage
On va toutefois avoir un problème : cette somme dépasse 100 ! Comme notre
tableau ne fait que 100 cases, si on s'en tient à ça, on risque de sortir des limites
du tableau.
Chaque lettre dans la table ASCII peut être numérotée jusqu'à 255. On a donc
vite fait de dépasser 100.
sommeLettres % 100
14
Écrire une fonction de hachage
Voici à quoi pourrait ressembler cette fonction :
int hachage(char *chaine)
{
int i = 0, nombreHache = 0;
for (i = 0 ; chaine[i] != '\0' ; i++)
{
nombreHache += chaine[i];
}
nombreHache %= 100;
return nombreHache;
}
15
Écrire une fonction de hachage
Si on lui envoie hachage("Luc Doncieux"), elle renvoie 55.
Avec hachage("Yann Martinez"), on obtient 80.
➔ Grâce à cette fonction de hachage, vous savez donc dans quelle case de votre
tableau vous devez placer vos données !
➔ Lorsque vous voudrez y accéder plus tard pour en récupérer les données, il
suffira de hacher à nouveau le nom de la personne pour retrouver l'indice de
la case du tableau où sont stockées les informations !
16
Gérer les collisions
Quand la fonction de hachage renvoie le même nombre pour deux clés différentes, on dit qu'il
y a collision.
Par exemple dans notre cas, si nous avions une anagramme de Luc Doncieux qui s'appelle
LucDoncueix, la somme des lettres est la même, donc le résultat de la fonction de hachage
sera le même !
Deux raisons peuvent expliquer une collision.
La fonction de hachage n'est pas très performante. C'est notre cas. Nous avons écrit une fonction très
simple (mais néanmoins suffisante) pour nos exemples.
➔ Les fonctions MD5 et SHA1 mentionnées plus tôt sont de bonne qualité car elles produisent très peu de
collisions. Notez que SHA1 est aujourd'hui préférée à MD5 car c'est celle des deux qui en produit le moins.
Le tableau dans lequel on stocke nos données est trop petit. Si on crée un tableau de 4 cases et qu'on
souhaite stocker 5 personnes, on aura à coup sûr une collision, c'est-à-dire que notre fonction de
hachage donnera le même indice pour deux noms différents.
Si une collision survient, pas de panique ! Deux solutions s'offrent à vous au choix : l'adressage
ouvert et le chaînage. 17
L'adressage ouvert
S'il reste de la place dans votre tableau, vous pouvez utiliser la technique dite
du hachage linéaire. Le principe est simple. La case est occupée ? Pas de problème, allez
à la case suivante. Ah, elle est occupée aussi ? Allez à la suivante !
Ainsi de suite, continuez jusqu'à trouver la prochaine case libre dans le tableau. Si vous
arrivez à la fin du tableau, retournez à la première case et continuez.
Cette méthode est très simple à mettre en place, mais si vous avez beaucoup de
collisions, vous allez passer beaucoup de temps à chercher la prochaine case libre.
Il existe des variantes (hachage double, hachage quadratique…) qui consistent à hacher à
nouveau selon une autre fonction en cas de collision. Elles sont plus efficaces mais plus
complexes à mettre en place.
18
Le chaînage
Une autre solution consiste à créer une liste chaînée à l'emplacement de la
collision. Vous avez deux données (ou plus) à stocker dans la même case ?
Utilisez une liste chaînée et créez un pointeur vers cette liste depuis le tableau.
19
Le chaînage
20
En résumé
Les listes chaînées sont flexibles, mais il peut être long de retrouver un élément précis à
l'intérieur car il faut les parcourir case par case.
Les tables de hachage sont des tableaux. On y stocke des données à un emplacement
déterminé par une fonction de hachage.
La fonction de hachage prend en entrée une clé (ex. : une chaîne de caractères) et
retourne en sortie un nombre.
Ce nombre est utilisé pour déterminer à quel indice du tableau sont stockées les données.
Une bonne fonction de hachage doit produire peu de collisions, c'est-à-dire qu'elle doit
éviter de renvoyer le même nombre pour deux clés différentes.
En cas de collision, on peut utiliser l'adressage ouvert (recherche d'une autre case libre
dans le tableau) ou bien le chaînage (combinaison avec une liste chaînée).
21