Vous êtes sur la page 1sur 14

Les arbres en C.

1 - Implmentation d'un arbre simple.


Introduction :

Je ne vais pas vous faire un cours complet sur les arbres, mais vous montrer comment les implmenter en
langage C.
Voyons tout de mme quelques notions lmentaires pour qu'il n'y ait pas ambiguts :
Qu'est-ce qu'un arbre ?
Tout comme les listes chanes, les arbres servent mmoriser des donnes. Ils sont constitus d'lments
que l'on appelle souvent des nuds (node). Ils sont semblables aux listes chanes par le fait que les
lments sont chans les uns avec les autres, mais avec la possibilit que plusieurs branches partent d'un
nud, d'ou leur nom. (On pourrait trs bien voir une liste chaine comme un arbre une seule branche). Il
est courant d'appeler le premier lment d'un arbre la racine. La racine est un nud qui n'a pas de parent. On
peut aussi entendre parler de feuilles, ce sont les nuds qui sont au bout des branches et qui n'ont donc pas
d'enfants.
Cet article tant destin aborder les arbres, nous allons donc en crer un qui sera le plus simple possible, ce
sera un arbre binaire.
Un arbre binaire est un arbre ou chaque nud peut avoir au maximum deux branches. Pour diffrencier les
branches, on les nomme souvent droite ou gauche (right, left).
On peut se faire un petit schma pour se le reprsenter visuellement.

La racine en haut et les branches vers le bas, dsol mais c'est la reprsentation la plus courante pour les
arbres (informatique).
Pour qu'un arbre soit efficace, il ne faut pas le remplir anarchiquement mais de faon ordonne, ceci afin de
retrouver nos donnes rapidement et sans avoir parcourir l'arbre complet. C'est l son gros avantage par
rapport aux listes chanes. Il est souvent bien plus rapide de parcourir l'arbre de la racine jusqu' une feuille
qu'une longue liste chane parfois entirement.

Un arbre binaire de recherche :

C'est un des arbres les plus simples, et nous allons le simplifier au maximum. Ses lments (ou nuds) ne
contiendront qu'une valeur de type entier. (On aurait pu y embarquer des structures de donnes plus
complexe, mais a ne nous est pas utile et a surchargerait l'exemple). C'est cette valeur qui nous servira

ordonner les lments. Nous l'appellerons donc la cl (key).


Tout comme les listes chanes, les arbres sont bass sur une structure du langage C. La diffrence sera
quelle contiendra deux pointeurs pour lier les lments, un pointeur pour accder la branche de gauche et
l'autre pour accder la branche de droite. Nous avons maintenant suffisamment d'lments pour constituer
la structure d'un nud.
typedef struct node
{
unsigned int key;
struct node *left;
struct node *right;
} node ;

La deuxime tape est de trier les lments leur insertion dans l'arbre. Le tri sera effectu sur la valeur de
la cl (key). Le premier lment est insr la racine de l'arbre, l'lment suivant est insr gauche si la
valeur de sa cl est inferieure celle de la racine et droite si la valeur de sa cl est suprieure celle de la
racine. (On aurait pu faire l'inverse). Pour les lments qui suivent, c'est le mme principe jusqu' trouver un
emplacement libre au bout d'une branche.
Par exemple si on avait insr des lments ayant comme cl ; 10, 20, 4, 8, 5, 15, 3 dans cet ordre, on aurait
un arbre quivalent au schma suivant :

Comme vous le voyez, il va tre facile de retrouver un lment, il suffira de suivre le mme cheminement
que pour l'insertion.
Pour notre exemple, le point d'entre de l'arbre sera un pointeur initialis NULL qui devra pointer sur la
racine ds l'insertion du premier lment.
node *Arbre = NULL;

La premire chose que l'on peut faire, c'est donc d'insrer des lments. Pour cela, nous allons crer la
fonction addNode.
void addNode(node **tree, unsigned int key)
{
node *tmpNode;
node *tmpTree = *tree;
node *elem = malloc(sizeof(node));
elem->key = key;
elem->left = NULL;
elem->right = NULL;

if(tmpTree)
do
{
tmpNode = tmpTree;
if(key > tmpTree->key )
{
tmpTree = tmpTree->right;
if(!tmpTree) tmpNode->right = elem;
}
else
{
tmpTree = tmpTree->left;
if(!tmpTree) tmpNode->left = elem;
}
}
while(tmpTree);
else *tree = elem;

Sa fonction est de crer l'lment l'aide de la fonction malloc, d'initialiser ses champs : la cl avec la valeur
dsir (pass en paramtre la fonction), d'initialiser les deux pointeurs NULL (il sera positionn au bout
d'une branche et n'a donc pas d'enfants) et de l'insrer dans l'arbre en tenant compte des critres de tri. Donc
si l'lment n'est pas le premier, on boucle (boucle do while) afin d'avancer de nud en nud jusqu'a
atteindre un emplacement libre (pointeur NULL) et chaque nud on part droite si la cl est suprieure
celle du nud courant ou gauche si elle est infrieure ou gale celle du nud courant.
Le cas du premier lment (racine de l'arbre) impose d'affecter l'adresse de cet lment au pointeur sur
l'arbre que l'on a pass en paramtre la fonction. Il s'impose donc que ce paramtre soit un pointeur de
pointeur afin de passer l'adresse du pointeur sur l'arbre la fonction. Fonction que l'on appellera donc de la
faon suivante :
node *Arbre = NULL;
//...
addNode(&Arbre, 30);

Comme on peut le remarquer, ce n'est gure plus compliqu que pour une liste chane.
Remarque: Je n'ai pas test les retours de la fonction malloc pour ne pas surcharger le code.
Nous avons dit que notre arbre est un arbre de recherche. C'est donc la deuxime fonction que nous allons
crer. Elle devra nous indiquer si un lment avec une cl de valeur x est prsent dans l'arbre.
int searchNode(node *tree, unsigned int key)
{
while(tree)
{
if(key == tree->key) return 1;
if(key > tree->key ) tree = tree->right;
else tree = tree->left;

}
return 0;

Le principe est identique la fonction d'insertion. On suit le cheminement en partant droite ou gauche
selon la valeur de la cl. A chaque nud on vrifie si on est en prsence de l'lment recherch, si oui on
retourne la valeur 1. Quand on arrive au bout de la branche si on ne l'a pas trouv on retourne 0. On est
certain qu'il ne se trouve pas dans une autre branche. Il n'y a donc pas besoin de tester.
Si la structure tait plus complexe, nous aurions pu faire retourner un contenu par la fonction. Par exemple
une valeur si on avait eu un couple cl valeur.
L'appel de cette fonction est des plus simples :

if(searchNode(Arbre, Key)) //...

A ce stade nous avons dj un arbre oprationnel.


Mais nous n'allons pas nous arrter l, nous allons lui ajouter une fonction qui permettra d'afficher l'arbre et
tri en plus.
Pour cela il va falloir parcourir l'arbre complet, et l nous avons gure le choix si l'on veut faire cela de
faon simple, nous devons utiliser des fonctions rcursives. (Une fonction rcursive est une fonction qui
s'appelle-elle mme).
void printTree(node *tree)
{
if(!tree) return;
if(tree->left)

printTree(tree->left);

printf("Cle = %d\n", tree->key);


if(tree->right) printTree(tree->right);
}

Dans la fonction, on remarque deux appels rcursifs de la fonction, un sur le pointeur de gauche et l'autre sur
le pointeur de droite de l'lment en cours de traitement. Ce qui va permettre de passer d'lment en lment.
Ce qui ferra arrter la rcursivit dans notre fonction, c'est le test du pointeur NULL des feuilles.
Pour afficher les valeurs des cls dans l'ordre, il faut partir gauche toute avant d'afficher la cl au retour de
la fonction, par contre si l'on part droite, on affiche la cl en cours avant l'appel de la fonction. (Vous
pouvez essayer de suivre le cheminement sur le schma).
Allez ! Une autre fonction d'affichage trs semblable la prcdente, sauf que l'on part droite toute en
premier.
void printReverseTree(node *tree)
{
if(!tree) return;
if(tree->right) printReverseTree(tree->right);
printf("Cle = %d\n", tree->key);
if(tree->left)

printReverseTree(tree->left);

Vous avez test ! a affiche l'arbre tri en sens inverse tout simplement.
Une dernire fonction ncessaire dans notre exemple, nous avons allou de la mmoire avec malloc, il faut
donc la librer. L aussi il faut parcourir l'arbre complet, nous utiliserons donc le mme principe que pour
les fonctions d'affichage.
void clearTree(node **tree)
{
node *tmpTree = *tree;
if(!tree) return;
if(tmpTree->left)

clearTree(&tmpTree->left);

if(tmpTree->right) clearTree(&tmpTree->right);
free(tmpTree);
*tree = NULL;
}

L'utilisation est des plus simples. Je mets un code d'utilisation en bas de page (main.c) qui vaut mieux qu'un
grand discours.
Codes sources de l'exemple :

Pour plus de lisibilit et de possibilit de rutilisation de cet arbre, nous sparerons son code de son
utilisation.
rbtree.h :
#ifndef CGI_RBTREE_H
#define CGI_RBTREE_H
typedef struct node
{
unsigned int key;
struct node *left;
struct node *right;
} node ;
#ifdef __cplusplus
extern "C" {
#endif
void addNode(node **tree, unsigned int key);
int searchNode(node *tree, unsigned int key);
void printTree(node *tree);
void printReverseTree(node *tree);
void clearTree(node **tree);
#ifdef __cplusplus
}
#endif
#endif //CGI_RBTREE_H

rbtree.c :
#include <stdio.h>
#include <stdlib.h>
#include "rbtree.h"
void addNode(node **tree, unsigned int key)
{
node *tmpNode;
node *tmpTree = *tree;
node *elem = malloc(sizeof(node));
elem->key = key;
elem->left = NULL;
elem->right = NULL;
if(tmpTree)
do
{
tmpNode = tmpTree;
if(key > tmpTree->key )
{

tmpTree = tmpTree->right;
if(!tmpTree) tmpNode->right = elem;
}
else
{

tmpTree = tmpTree->left;
if(!tmpTree) tmpNode->left = elem;

}
}
while(tmpTree);
else *tree = elem;

/***************************************************************************/
int searchNode(node *tree, unsigned int key)
{
while(tree)
{
if(key == tree->key) return 1;
if(key > tree->key ) tree = tree->right;
else tree = tree->left;

}
return 0;

/***************************************************************************/
void printTree(node *tree)
{
if(!tree) return;
if(tree->left)

printTree(tree->left);

printf("Cle = %d\n", tree->key);


if(tree->right) printTree(tree->right);
}
/***************************************************************************/
void printReverseTree(node *tree)
{
if(!tree) return;
if(tree->right) printReverseTree(tree->right);
printf("Cle = %d\n", tree->key);
}

if(tree->left)

printReverseTree(tree->left);

/***************************************************************************/
void clearTree(node **tree)
{
node *tmpTree = *tree;
if(!tree) return;
if(tmpTree->left)

clearTree(&tmpTree->left);

if(tmpTree->right) clearTree(&tmpTree->right);
free(tmpTree);

*tree = NULL;

Voici un exemple d'utilisation de l'arbre que nous venons de construire.


main.c :
#include <stdio.h>
#include <stdlib.h>
#include "rbtree.h"
int main()
{
unsigned int Key;
node *Arbre = NULL;
addNode(&Arbre,
addNode(&Arbre,
addNode(&Arbre,
addNode(&Arbre,
addNode(&Arbre,
addNode(&Arbre,
addNode(&Arbre,
addNode(&Arbre,
addNode(&Arbre,
addNode(&Arbre,
addNode(&Arbre,

30);
20);
50);
45);
25);
80);
40);
70);
25);
10);
60);

puts("-------------------------------");
printTree(Arbre);
puts("-------------------------------");
printReverseTree(Arbre);
puts("-------------------------------");
Key = 30;
if(searchNode(Arbre, Key)) printf("La cle %d existe.\n", Key);
else printf("La cle %d n'existe pas.\n", Key);
Key = 32;
if(searchNode(Arbre, Key)) printf("La cle %d existe.\n", Key);
else printf("La cle %d n'existe pas.\n", Key);
puts("-------------------------------");
clearTree(&Arbre);
return 0;
}

Cet arbre est bien adapt pour la recherche d'lment, l'insertion et la recherche ne ncessite pas de parcourir
l'arbre en entier. Il peut aussi faire du tri, vous l'avez vu avec les fonctions d'affichage. Il est mme assez
performant si les valeurs des cls des lments insrs sont alatoires.
Par contre il devient trs mauvais dans le cas d'insertion d'lments dj tris ou partiellement tris. Dans ce

cas au lieu de faire plusieurs branches il pourrait en faire une seule, trs grande dans le pire des cas. (Ce qui
reviendrait une liste chaine).

2 - Implmentation d'un tas (heap).


Introduction :

Aprs les arbres binaires de recherche, nous allons voir un nouveau type d'arbres : les arbres tasss, appel
aussi tas (heap en anglais). Ce sont aussi des arbres binaires.
Sur les arbres binaires de recherches nous avions mentionn un dfaut, qui est le fait que certaines branches
peuvent tre beaucoup plus longues que d'autres dans certaines circonstances. Ce qui a un effet nfaste sur
les performances. L'arbre que nous allons aborder pallie ce problme. La hauteur de sa plus grande
branche ne pourra tre suprieure que d'une unit par rapport la plus petite. C'est dire que si sa plus
grande branche passe par 15 nuds sa plus petite passera par 14 nuds au minimum (15 au cas o l'arbre
soit complet). Bien sr comme tous arbres, les donnes seront insres selon un certain ordre, afin de les
retrouver facilement. Ce type d'arbre est trs bien adapter pour servir de file de priorit. C'est ce que nous
allons construire dans l'exemple suivant.

Le tas :

Comme les structures des articles prcdents nous la ferons le plus simple possible, la valeur sauvegarde
sera donc encore une fois un entier.
Mais cette fois nous allons faire une implmentation compltement diffrente de ce que l'on a vu jusqu'
maintenant. Nous n'allons plus utiliser d'lments chans par des pointeurs, mais un tableau dynamique.
Cette faon de procder peut paratre trange au premier abord, mais est trs bien adapte ce type d'arbre.
Pour comprendre le principe nous allons suivre le schma suivant, ou les lments de l'arbre reprsentent les
indices du tableau et non pas leur valeur.

Premire remarque, l'indice du premier lment est 1, il est positionn la racine de l'arbre (ou au sommet
du tas). La case d'indice 0 du tableau ne sera pas utilise. Cela nous permettra de mieux visualiser le
fonctionnement de cet arbre. (A savoir que l'on aurait pu tout fait commencer avec l'indice 0, ce n'est qu'un
choix). Nous remarquons donc sur le schma qu'il est assez simple de calculer l'indice des enfants d'un
nud, puisque l'indice du fils gauche est l'indice du parent multipli par 2 et l'indice du fils droit est l'indice

du fils gauche plus 1. Nous pouvons donc parcourir une branche sans parcourir tout le tableau. Vous devez
commencer percevoir les avantages d'un tel systme.
Le point d'entre de notre tas sera une structure qui contiendra un pointeur sur le tableau de donnes, tableau
d'entier pour notre exemple, mais aussi deux informations, la taille du tableau, c'est dire le nombre
d'lment valide qu'il contient et la capacit du tableau, c'est dire sa taille relle, comprenant les cases non
utilises. Ceci dans le but d'viter des rallocations chaque ajout d'lment. Le tableau sera agrandi
seulement quand sa taille aura atteint la capacit du tableau.
typedef
{
int
int
int
} Heap;

struct
capacity;
size;
*tab;

Premire chose faire, crer et initialiser la structure. Pour cela nous allons crer une fonction nomme :
CreateHeap, elle nous retournera un pointeur initialis sur une structure Heap.
Heap* createHeap(void)
{
Heap *heap = malloc(sizeof(Heap));
if(!heap) return NULL;
heap->tab = malloc(32*sizeof(int));
heap->capacity = 32;
heap->size = 0;
return heap;
}

/* Prvoir l'chec de malloc */

On cre une variable du type Heap avec malloc, puis on cr le tableau nomm tab de taille 32. (Je n'ai pas
test le retour de malloc pour ne pas surcharger l'exemple). Nous mettons donc le champ capacity 32 et
size 0 (le tableau est vide).
Nous avons dit prcdemment que notre arbre se comportera comme une file de priorit. Nous ferons donc
en sorte que le premier lment qui sera extrait du tas sera celui qui la plus grande valeur (on aurait pu
faire l'inverse). Pour le trouver facilement, il sera donc positionns la racine de l'arbre (case d'indice 1 du
tableau). Les lments serons donc positionn de tel sorte que la valeur de leurs parents leurs soit suprieure
et celle de leurs enfants leur soit inferieures ou gal. Mais alors comment insrer les lments, et bien ils
seront insr la premire position vide du tableau (celle qui correspond au nud marqu suivant sur le
schma de l'arbre si dessus). Ensuite il faut faire remonter l'lment par change avec son parent, tant que sa
valeur est suprieure celle de son parent. Par exemple si on insert des lments ayant comme valeur ; 12,
11, 14, 8, 13, 7, 18, 6, 1, 10, 3 dans cet ordre, on aurait un arbre quivalent au schma suivant :

Aprs avoir cr le tas,


Heap *tas = CreateHeap();

Nous allons voir la fonction utilise pour inserer les lments :


void pushNode(Heap *heap, int value)
{
if(heap->size >= heap->capacity) HeapRealloc(heap);
heap->size++;
heap->tab[heap->size] = value;
reorganizeHeap(heap);
}

Si la capacit maximum du tableau est atteinte, on l'agrandira en appelant la fonction HeapRealloc (Code en
bas de page). Comme on a ajout un lment, il faut donc incrmenter la taille (champ size). Puis, on met
l'lment dans la dernire case valide du tableau, c'est celle qui a l'indice heap->size. Ensuite comme
mentionn prcdemment il faut dplacer l'lment la bonne place. Ce sera le rle de la fonction
reorganizeHeap :
static void reorganizeHeap(Heap *heap)
{
int tmp;
int size = heap->size;
int index = size/2;
while(heap->tab[index] < heap->tab[size] && size>1)
{
tmp = heap->tab[size];
heap->tab[size] = heap->tab[index];
heap->tab[index] = tmp;

index/=2;
size/=2;

Pour atteindre les fils d'un parent on avait dit qu'il fallait multiplier l'indice du parent par 2. Mais l nous
nous trouvons dans la situation inverse, donc pour retrouver le parent d'un fils, il suffit de le diviser par 2
tout simplement. C'est ce que nous ferons dans la fonction reorganizeHeap pour remonter l'lment par
change avec son parent tant qu'il est suprieur son parent.
Maintenant voyons la fonction inverse, c'est dire le retrait d'un lment. Le retrait de l'lment est simple
puis qu'il est la racine de l'arbre. Ou a ce complique c'est pour rorganiser les lments restant. La
methode est de mettre le dernier lment la racine. On n'oublie pas de diminuer la taille de 1 (il y a un
lment en moins). Mais cet lment n'est plus sa place, il faut donc le descendre par change avec le plus
grand de ses fils tant qu'il est suprieur l'lment ou qu'il n'ai plus de fils.
int popNode(Heap *heap)
{
int tmp;
int indexUp = 1;
int indexDn = 2;
if(heap->size==0) return -1;
int value = heap->tab[1];
heap->tab[1] = heap->tab[heap->size];
heap->size--;
while(indexDn<=heap->size)
{

if(indexDn+1 <= heap->size && heap->tab[indexDn] < heap->tab[indexDn+1])


{
indexDn++;
}
if(heap->tab[indexDn] > heap->tab[indexUp])
{
tmp = heap->tab[indexDn];
heap->tab[indexDn] = heap->tab[indexUp];
heap->tab[indexUp] = tmp;
}
indexUp = indexDn;
indexDn *= 2;
}
return value;
}

Bien videmment, une fonction pour librer la mmoire alloue quand on en n'a plus besoin.
void freeHeap(Heap *heap)
{
free(heap->tab);
free(heap);
}

L'utilisation est des plus simples. Je mets un code d'utilisation en bas de page (main.c) qui vaut mieux qu'un
grand discours.
Codes sources de l'exemple :

Pour plus de lisibilit et de possibilit de rutilisation de cet arbre, nous sparerons son code de son
utilisation.
heap.h :
#ifndef CGI_HEAP_H
#define CGI_HEAP_H
typedef
{
int
int
int
}Heap;

struct
capacity;
size;
*tab;

#ifdef __cplusplus
extern "C" {
#endif
Heap* createHeap(void);
void pushNode(Heap *heap, int value);
int popNode(Heap *heap);
void freeHeap(Heap *heap);
#ifdef __cplusplus
}
#endif
#endif //CGI_HEAP_H

heap.c :

#include <stdlib.h>
#include "heap.h"
static void HeapRealloc(Heap *heap);
static void reorganizeHeap(Heap *heap);
Heap* createHeap(void)
{
Heap *heap = malloc(sizeof(Heap));
if(!heap) return NULL;
heap->tab = malloc(32*sizeof(int));
heap->capacity = 32;
heap->size = 0;
return heap;
}

/* Prvoir l'chec de malloc */

/***************************************************************************/
static void HeapRealloc(Heap *heap)
{
int new_size = 2*heap->capacity;
heap->tab = realloc(heap->tab, new_size*sizeof(int));
/* Prvoir l'chec de realloc */
heap->capacity = new_size;
}
/***************************************************************************/
static void reorganizeHeap(Heap *heap)
{
int tmp;
int size = heap->size;
int index = size/2;
while(heap->tab[index] < heap->tab[size] && size>1)
{
tmp = heap->tab[size];
heap->tab[size] = heap->tab[index];
heap->tab[index] = tmp;

index/=2;
size/=2;

}
/***************************************************************************/
void pushNode(Heap *heap, int value)
{
if(heap->size >= heap->capacity) HeapRealloc(heap);
heap->size++;
heap->tab[heap->size] = value;
reorganizeHeap(heap);
}
/***************************************************************************/
int popNode(Heap *heap)
{
int tmp;
int indexUp = 1;
int indexDn = 2;
if(heap->size==0) return -1;
int value = heap->tab[1];
heap->tab[1] = heap->tab[heap->size];
heap->size--;
while(indexDn<=heap->size)
{
if(indexDn+1 <= heap->size && heap->tab[indexDn] < heap->tab[indexDn+1])

indexDn++;
}
if(heap->tab[indexDn] > heap->tab[indexUp])
{
tmp = heap->tab[indexDn];
heap->tab[indexDn] = heap->tab[indexUp];
heap->tab[indexUp] = tmp;
}
indexUp = indexDn;
indexDn *= 2;

}
return value;

/***************************************************************************/
void freeHeap(Heap *heap)
{
free(heap->tab);
free(heap);
}

Voici un exemple d'utilisation de l'arbre que nous venons de construire.


main.c :
#include <stdio.h>
#include "heap.h"
int main()
{
Heap *tas = createHeap();
pushNode(tas,
pushNode(tas,
pushNode(tas,
pushNode(tas,
pushNode(tas,
pushNode(tas,
pushNode(tas,
pushNode(tas,
pushNode(tas,
pushNode(tas,
pushNode(tas,
printf("Valeur
printf("Valeur
printf("Valeur
printf("Valeur
printf("Valeur
printf("Valeur
printf("Valeur
printf("Valeur
printf("Valeur
printf("Valeur
printf("Valeur

12);
11);
14);
8);
13);
7);
18);
6);
1);
10);
3);

freeHeap(tas);
return 0;
}

retiree
retiree
retiree
retiree
retiree
retiree
retiree
retiree
retiree
retiree
retiree

:
:
:
:
:
:
:
:
:
:
:

%d\n",
%d\n",
%d\n",
%d\n",
%d\n",
%d\n",
%d\n",
%d\n",
%d\n",
%d\n",
%d\n",

popNode(tas));
popNode(tas));
popNode(tas));
popNode(tas));
popNode(tas));
popNode(tas));
popNode(tas));
popNode(tas));
popNode(tas));
popNode(tas));
popNode(tas));

Si l'on compare une liste chane trie. Le retrait de l'lment de tte est comparable car dans les deux cas
il n'y a pas besoin de parcourir la liste. Mais pour l'insertion d'un lment c'est tout autre chose. Pour la liste
dans le pire des cas on parcourira la liste complte, alors que pour le tas a sera la hauteur d'une branche au
maximum. Ce qui pour des structures de donnes comportant un trs grand nombre d'lments n'est plus
ngligeable.