Algorithmique des arbres, Ocaml L2, 2ème semestre

publicité
Algorithmique des arbres, Ocaml
L2, 2ème semestre
Frédérique Carrère
Irène Durand
Florian Golemo
Stefka Gueorguieva
Nathan Lothe
Mathieu Raffinot
Marc Zeitoun
Version du 18 mars 2017
http ://dept-info.labri.fr/ENSEIGNEMENT/asda/
[email protected]
Table des matières
1 Introduction
1
Les arbres comme structures de données . . . . . . .
1.1
Utilisation des arbres et premières définitions
1.2
Variantes d’arbres . . . . . . . . . . . . . . .
2
Données attachées aux nœuds d’un arbre . . . . . .
3
Quelques tâches importantes . . . . . . . . . . . . .
4
Références . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
2 Révisions : tableaux et listes
1
Rappels sur les tableaux . . . . . . . . . .
1.1
Quelques exercices sur les tableaux
2
La structure de donnée de liste . . . . . .
2.1
Les listes en OCaml . . . . . . . .
2.2
Quelques exercices sur les listes . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
7
. 7
. 9
. 11
. 12
. 13
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
2
2
2
5
5
5
6
3 Arbres et arbres de recherche
17
1
De la liste à l’arbre . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2
Arbres binaires de recherche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
4 Arbres 2-3-4 et arbres bicolores
29
5 Algorithme de Huffman
38
1
Arbre Patricia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
2
Codage de Huffman . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
1
Chapitre 1
Introduction
Ce cours se situe dans l’apprentissage des structures de données. Nous allons aborder et approfondir
la notion d’arbre, en passant par des rappels sur les tableaux et les listes, pour bien comprendre
pourquoi et dans quels cas la notion ou la structure d’arbre permet d’améliorer nos algorithmes, que
ce soit en conceptualisation, en simplicité ou en efficacité.
1
Les arbres comme structures de données
Les arbres sont une structure de donnée très utilisée en informatique. Le mot « arbre > > est en
fait un terme générique, qui désigne plusieurs sortes de structures de données différentes. Nous verrons
quelques unes de ces variantes en Section 1.2. Les arbres permettent d’organiser l’information de façon
plus élaborée que dans le cas de structures de données linéaires (telles les tableaux ou les listes). Cette
plus grande complexité a un coût et un gain :
• Le coût à payer une plus grande difficulté de manipulation des arbres par rapport à celle des
structures linéaires.
• Le gain apporté peut être une facilité de modélisation, lorsqu’on veut représenter des structures
hiérarchiques pour lesquelles les arbres sont très bien adaptés, ainsi qu’une meilleure efficacité
algorithmique.
Ce court chapitre introductif présente quelques utilisations des arbres en informatique et quelques
variations sur la structure de donnée d’arbre. La Section 2 explique la notion de multi-ensemble, que
nous voulons représenter grâce à cette structure de données. Enfin, la Section 3 présente des tâches
fréquemment effectuées sur les arbres, qui seront rencontrées dans le cours.
1.1
Utilisation des arbres et premières définitions
La notion d’arbre est introduite dans cette section avec des applications concrètes. Nous en profitons
pour introduire des termes importants, à retenir. Ils sont écrits avec cette police de caractères.
Vocabulaire et premier exemple : système de fichiers
Les arbres sont particulièrement adaptés à la représentation et à la manipulation d’information
déjà hiérarchique. Par exemple, un système de fichiers a une structure hiérarchique : un répertoire
peut contenir des sous-répertoires ou fichiers, qui eux-mêmes peuvent contenir des sous-répertoires ou
fichiers, etc. On peut représenter des répertoires et fichiers par un arbre, comme indiqué en Figure 1.1.
L’usage en informatique est de dessiner cette structure avec la racine / du système de fichiers en
haut, et en indiquant les répertoires au-dessous de celui qui les contient. On obtient la représentation
de la Figure 1.2. Chaque répertoire correspond à un nœud de l’arbre. Distinguer le nœud racine (ici
/) permet de parler de relations de parenté entre nœuds :
• Chaque nœud, sauf la racine, a un unique père. Dans l’exemple concret du système de fichiers,
le père d’un répertoire (autre que la racine) est le répertoire qui le contient. Ainsi, le père du
seul nœud dont le nom est include est le nœud usr.
2
/
bin
home
nafnaf
idees
plans
nifnif
noufnouf
usr
bin
include
lib
Fig. 1.1 : Vue hiérarchique de répertoires
• Inversement, chaque nœud peut avoir (ou non) des fils. Par exemple, le nœud usr a 3 fils (nommés
bin, include et lib). En revanche, le nœud include n’a aucun fils. Un nœud qui n’a pas de fils
est appelé une feuille.
• Un nœud qui n’est pas une feuille est appelé nœud interne.
• L’arité d’un nœud est son nombre de fils. Les feuilles sont donc les nœuds d’arité 0.
• Enfin, deux nœuds qui ont même père sont appelés frères.
/
bin
nafnaf
idees
home
nifnif
usr
noufnouf
bin
include
lib
plans
Fig. 1.2 : Représentation arborescente du même système de fichiers
Dans la Figure 1.2, on a indiqué chaque lien de parenté par une flèche du père vers le fils. En
général, on omet les flèches, qui sont redondantes avec le choix de placer la racine en haut, et de « faire
croître > > les arbres vers le bas.
Remarquez également que les noms des répertoires sont de l’information attachée aux nœuds, ils ne
désignent pas les nœuds eux-mêmes. En particulier, le même nom peut être utilisé pour 2 nœuds différents. Sur cet exemple, c’est le cas : deux des répertoires s’appellent bin (pour un arbre qui représente
un système de fichiers, deux frères ne peuvent bien sûr pas avoir le même nom).
Étant donné un nœud d’un arbre, il y a un unique chemin reliant ce sommet à la racine et ne
passant pas 2 fois par le même sommet. Une branche est un chemin qui va de la racine à une feuille.
Par exemple, / −
→ home −
→ nafnaf −
→ plans est une branche. Il y a donc autant de branches que de
feuilles. Enfin, un sous-arbre enraciné à un nœud particulier est l’arbre obtenu en ne conservant que
le nœud et ses descendants. Par exemple, le sous-arbre enraciné au seul nœud marqué usr est l’arbre
de la Figure 1.3 suivante.
3
usr
bin
include
lib
Fig. 1.3 : Sous-arbre enraciné au nœud marqué usr
Notez enfin que certains détails sont pour l’instant ignorés. Par exemple, doit-on considérer que
l’ordre des fils a une importance, ou au contraire, que permuter deux fils sur la figure représente le
même arbre ?
Dans ce cours, on utilisera des arbres binaires dans lesquels l’ordre des fils est important. Un arbre
est dit binaire lorsque tout nœud a 0, 1 ou 2 fils. Comme on utilisera des arbres dans lesquels l’ordre
des fils importe, on distingue le fils gauche et le fils droit de tout nœud interne. De la même façon,
on parle de sous-arbre gauche d’un nœud interne le sous-arbre enraciné en son fils gauche (idem
pour le sous-arbre droit).
Des utilisations très diverses
Les arbres ont de multiples utilisations en informatique. Nous en mentionnons quelques-unes dans
cette partie. Une utilisation très courante des arbres est fournie par le format XML (eXtended Markup
Language). Il s’agit d’un format standard de documents, utilisé dans de multiples contextes pour
stocker ou transmettre de l’information. Le code suivant est un exemple de fichier XML. Le format
est utilisé dans de nombreuses applications, comme les bases de données, les données GPS, le dessin
vectoriel, etc. Vous pouvez en visualiser d’autres, par exemple en regardant des traces fournies par un
GPS, ou les fichiers générés par un logiciel de dessin vectoriel (comme Inkscape).
<?xml version=” 1 . 0 ” e n c o d i n g=”UTF−8” ?>
<b o o k s t o r e>
<book i s b n=” 978 −0201896831 ”>
< t i t l e>The Art o f Computer Programming , Fundamental A l g o r i t h m s</ t i t l e>
<a u t h o r>Donald E . Knuth</ a u t h o r>
<y e a r>1997</ y e a r>
<volume>1</ volume>
<p r i c e>65</ p r i c e>
</ book>
<book i s b n=” 978 −2258034310 ”>
< t i t l e>C a l v i n e t Hobbes , tome 1 : Adieu , monde c r u e l !</ t i t l e>
<a u t h o r> B i l l Watterson</ a u t h o r>
<y e a r>1999</ y e a r>
<volume>1</ volume>
<p r i c e>10</ p r i c e>
</ book>
</ b o o k s t o r e>
Ce code se représente naturellement par un arbre, donné en Figure 1.4, où les valeurs concrètes ont
été omises.
bookstore
book
title
author
year
book
volume
price
title
author
year
volume
price
Fig. 1.4 : Une représentation du document XML
De façon générale, les arbres sont utilisés pour représenter et manipuler de l’information naturellement hiérarchique, mais aussi pour rendre plus rapide l’accès à l’information, tout en conservant
un temps d’accès raisonnable aux données. Les domaines d’applications des arbres en informatique
4
ne se limitent pas aux deux exemples déjà rencontrés. Ils sont utilisés dans des domaines aussi variés
que les algorithmes de routage, la compression de données, des algorithmes de fouille de données et
d’apprentissage, des algorithmes de résolution de jeux (comme les échecs), ou des algorithmes de rendu
de jeux 3D, voir ici et là. Ils interviennent aussi dans la représentation de grandes bases de données,
la représentation de certains formats de document (nous avons vu XML, mais le format PDF utilise
aussi les arbres, par exemple). Enfin, ils interviennent pour approcher des problèmes difficiles sur les
graphes (et cette liste d’applications n’est pas exhaustive). La structure d’arbre est donc réellement
fondamentale en informatique.
1.2
Variantes d’arbres
On distingue plusieurs sortes d’arbres : dans ce cours l’ordre des fils aura une importance. Ainsi,
les deux arbres de la Figure 1.5 ne sont pas les mêmes.
Fig. 1.5 : Deux arbres symétriques mais différents
Les arbres les plus simples sont les arbres binaires, dans lesquels chaque nœud interne a 2 fils. On
considèrera aussi des arbres à arité variable d’un nœud à un autre (on rappelle que l’arité d’un nœud
est son nombre de fils). Parfois, on peut considérer l’arbre vide, qui ne contient aucune feuille. On peut
aussi considérer, au contraire, que tout arbre a au moins un nœud. Cette distinction impacte en fait
tous les arbres que l’on considère, car les définitions d’arbres sont récursives.
2
Données attachées aux nœuds d’un arbre
Ensembles et multi-ensembles
Tout au long de ce cours, nous allons manipuler des données mémorisées dans un arbre. La première
idée est de partir d’un ensemble d’éléments. Par exemple, si les données à manipuler sont les entiers
entre 0 et 10, l’ensemble correspondant serait {0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10}. Malheureusement, les
ensembles ne suffisent pas, car un ensemble contient un élément ou pas, mais ne peut pas contenir
deux fois le même élément. On veut donc manipuler des objets mathématiques dans lesquels un élément
peut apparaître plusieurs fois, mais où l’ordre n’importe pas. L’objet mathématique adapté est en fait
une fonction d’un ensemble E dans N : pour un élément x ∈ E, l’entier f (x) qui compte le nombre
d’occurrences de x. Cet objet s’appelle un multi-ensemble. En pratique, on écrira un multi-ensemble
fini simplement en donnant ses éléments, chaque élément pouvant être présent plusieurs fois. Au lieu
d’utiliser la notation {. . .}, on utilisera {{. . .}}.
On utilisera souvent des valeurs entières, parce que c’est un type simple. Par exemple, on pourra
considérer le multi-ensemble S = {{12 , 2 , 2 , 3 , 5 , 6 , 7 , 6}}. Comme l’ordre des éléments dans un multiensemble n’est pas importante, on peut aussi noter S = {{7 , 2 , 12 , 3 , 5 , 2 , 6 , 6}}, ou bien encore S =
{{2 , 2 , 3 , 5 , 6 , 6 , 7 , 12}}. Ce multi-ensemble peut être représenté par un tableau, par une liste, par des
arbres, etc. Qu’est-ce qui change ? Nous allons voir cela en détail.
3
Quelques tâches importantes
Après un rappel sur les structures de données linéaires (tableau et listes), les chapitres suivants
présentent des problèmes et algorithmes sur les arbres. Ces algorithmes sont guidés par l’utilisation
qu’on souhaite faire des arbres. Cependant, certaines opérations sont génériques et importantes. Elles
reviennent donc fréquemment. Parmi celles-ci, il est conseillé de réfléchir à des algorithmes pour exécuter les tâches suivantes. Vous pouvez retrouver les algorithmes correspondants, écrits en OCaml, sur la
5
page du cours de Programmation fonctionnelle, https ://www.labri.fr/~zeitoun/enseignement/
16-17/PF/.
Pour simplifier, nous nous limitons volontairement ici aux arbres binaires. À partir d’un arbre
donné en entrée, il s’agit de calculer :
• son nombre de feuilles,
• son nombre de nœuds internes,
• sa taille, c’est-à-dire son nombre de nœuds, en comptant les feuilles comme les nœuds internes,
• sa hauteur, c’est-à-dire la longueur maximale d’une de ses branches,
• la liste de ses nœuds en parcours infixe,
• la liste de ses nœuds en parcours préfixe,
• la liste de ses nœuds en parcours postfixe.
Par ailleurs, comme les arbres seront utilisés pour mémoriser de information, une tâche élémentaire
est, étant donné un arbre t dont les nœuds sont étiquetés par des éléments d’un ensemble E, ainsi
qu’un élément x de E, de calculer
• un booléen, vrai si l’élément x apparaît dans l’arbre, et faux sinon.
• de façon plus précise, on peut demander que la fonction renvoie, en plus de ce booléen, un chemin
de la racine à un sommet qui contient l’élément x. Ce chemin peut être représenté par une liste
de directions (gauche ou droite).
• enfin, les arbres sont fréquemment utilisés comme structure de données pour représenter des
dictionnaires. Un dictionnaire est une liste de couples, chaque couple étant de la forme (k , v),
où k est appelée clé et v est appelée valeur. Chaque clé apparaît au maximum une fois dans
l’arbre. Par contre, une valeur peut apparaître plusieurs fois (associée à des clés différentes). Par
exemple, dans un dictionnaire au sens habituel, chaque clé est un mot et la valeur associée à un
mot contient ses différentes définitions.
Dans ce cadre, plutôt que de retourner simplement un booléen disant si x apparaît dans l’arbre,
il est intéressant d’avoir un algorithme recherchant x comme clé dans l’arbre, et si x est effectivement présent comme clé, retournant l’unique valeur associée à x.
Commentaire. Irene : moi ça me pose un problème ces fonctions de recherche qui retournent
un boolean. En général on recherche un élément à partir d’une clé et on retourne l’élément entier.
Savoir que l’élément appartient sans pouvoir y accéder n’est pas très intormatif. La même remarque
s’applique pour la recherche dans les listes ou les tableaux.
Marc : Je suis d’accord, j’ai ajouté une suite dans ce sens (si ça te va, on peut effacer cette note,
sinon tu peux modifier directement le texte).
4
Références
Le livre [1] est librement disponible en ligne. Il contient plusieurs algorithmes classiques, en particulier sur les arbres.
Bibliographie
[1]
D. Beauquier, J. Berstel et Ph. Chrétienne. Éléments d’Algorithmique. Masson, 1992. url :
http ://www-igm.univ-mlv.fr/~berstel/Elements/Elements.html.
6
Chapitre 2
Révisions : tableaux et listes
Ce chapitre présente quelques rappels sur les structures linéaires : tableaux et listes. On compare
notamment l’intérêt algorithmique de chacune de ses structures.
1
Rappels sur les tableaux
Un tableau est une suite d’éléments consécutifs en mémoire, tous de même type. Un tableau est
déterminé par :
• sa taille,
• le type des éléments qu’il peut contenir,
• l’adresse de sa première case.
La figure 2.1 représente un tableau contenant les éléments du multi-ensemble S = {{15 , 17 , 42 , 42 , 45 , 84 , 99}}
(dans l’ordre où ils sont écrits).
Premier indice
0
1
2
3
4
5
6
15 17 42 42 45 84 99
Indices
Éléments
Longueur du tableau: 7
Fig. 2.1 : Un tableau contenant 7 entiers
Remarque. 2.1 Même si on veut seulement représenter un multi-ensemble, un tableau contient en
fait plus d’information, car ses éléments sont naturellement ordonnés par leur indice dans le tableau.
Un tableau représente donc en fait, mathématiquement, un vecteur.
Cela n’empêche pas d’utiliser cette structure pour représenter un multi-ensemble, en ignorant
simplement l’ordre des éléments. Autrement dit, si on représente un multi-ensemble par un tableau,
permuter les éléments de ce tableau ne change pas le multi-ensemble représenté. Il peut alors être
avantageux de maintenir une représentation particulière de ce multi-ensemble (par exemple triée),
pour pouvoir effectuer des tâches plus efficacement (par exemple, en utilisant la dichotomie).
Un tableau est implanté en rangeant ses éléments de façon consécutive en mémoire, à partir de
l’adresse du premier élément du tableau. Ce choix a un avantage et deux inconvénients :
1. Accès direct. L’avantage des tableaux est qu’ils permettent l’accès à un élément dont l’indice
est connu en temps O(1) (cette propriété est appelée accès direct). En effet, pour accéder à
l’élément se trouvant en ième case d’un tableau, il suffit d’ajouter à l’adresse de son premier
élément i fois la taille de chaque élément. Le calcul de l’adresse de l’élément p[ i ] nécessite donc
seulement une addition et une multiplication. L’accès à la valeur p[ i ], ou *(p+i), nécessite en
7
plus de dé-référencer l’adresse. On a donc un nombre constant d’opérations « élémentaires » à
effectuer (une addition, une multiplication et un dé-référencement), ce qui nécessite un temps
O(1), c’est-à-dire constant, indépendant de l’indice i et de la taille du tableau.
p
p+3
0
1
2
3
4
5
6
15 17 42 42 45 84 99
Fig. 2.2 : Accès direct
Remarque. 2.2 En langage C, l’arithmétique des pointeurs effectue la multiplication par la
taille des éléments du tableau sans que l’on ait à l’écrire : si on écrit
int p [ 1 0 ] ;
float q [ 2 0 ] ;
alors p + 5 est l’adresse de la 6ème case de p et q + 15 est l’adresse de la 16ème case de q. On
n’a pas besoin de (et il ne faut pas, en fait) multiplier par sizeof(int) dans le cas de p, ou
par sizeof(float) dans le cas de q.
2. Redimensionnement coûteux. Dans certains langages, le nombre d’éléments d’un tableau est
fixe et décidé au moment de l’allocation mémoire. Dans ce cas, il n’est pas possible d’étendre un
tableau dont toutes les cases sont occupées pour y ranger un élément supplémentaire, sans faire
soi-même une recopie à la main.
Il existe cependant des langages permettant d’étendre un tableau. En C, vous avez vu la fonction
de la bibliothèque standard de prototype void * realloc (void *ptr, size_t size ); . Cette fonction
permet de redimensionner un tableau déjà alloué par une des fonctions malloc ou calloc. Par
exemple, l’instruction C :
r e a l l o c ( p , 10* s i z e o f ( int ) ) ;
fait passer de la situation de la Figure 2.3 (a) à celle de la Figure 2.3 (b) (si elle réussit, et en
supposant que p est un tableau contenant des éléments de type int).
p
0
p
(a)
1
2
3
4
5
6
0
(b)
1
2
3
4
5
6
7 8 9
15 17 42 42 45 84 99
15 17 42 42 45 84 99 ? ? ?
Longueur du tableau: 7
Longueur du tableau: 10
Fig. 2.3 : Redimensionnement d’un tableau
Cependant, même s’il y a assez de mémoire disponible, redimensionner un tableau peut conduire
à recopier tout le tableau. C’est le cas s’il n’y a pas assez d’espace contigu en mémoire, par
exemple si d’autres données ont déjà été allouées « de chaque côté du tableau > >, comme dans
la Figure 2.4.
p1
0
p2
1
2
3
4
5
6
0
p3
1
2
3
4
5
6
0
1
2
3
4
5
6
50 12 11 85 42 77 20 15 17 42 42 45 84 99 10 70 99 18 53 79 80
Fig. 2.4 : Une situation où redimensionner p2 est coûteux
8
Remarque. 2.3 Dans l’exemple de la figure 2.4, redimensionner p2 serait coûteux même s’il y
avait de la place disponible « à sa gauche > >, puisque l’extension doit se faire sur la droite.
Remarque. 2.4 Au niveau de la programmation, lorsqu’un gestionnaire de mémoire comme
celui de la bibliothèque standard C a besoin d’étendre un tableau de taille n, dire que le coût
de l’extension est O(n) dans le cas le pire n’est pas tout à fait exact. En effet, pour allouer
le nombre voulu de cases, l’algorithme de gestion mémoire doit trouver l’espace consécutivement en mémoire (car dans un tableau, on range les éléments de façon consécutive). Il est
possible qu’il y ait suffisamment d’espace en mémoire, mais pas consécutivement. Si l’algorithme décale certaines zones occupées mémoire pour créer un espace contigu assez grand, le
coût du redimensionnement peut être très important (et dépend de la taille de la mémoire
disponible, et non plus de celle du tableau).
3. Insertion et suppression coûteuses. Enfin, si on veut insérer un élément dans un tableau à
un indice i (en supprimant son dernier élément), il faut décaler tous les éléments à un indice j > i,
ce qui nécessite à nouveau, dans le cas le pire, O(n) affectations. De même, pour supprimer un
élément d’un tableau, il faut décaler d’une case vers la gauche tous les éléments d’indice supérieur
à l’indice de l’élément à supprimer, ce qui nécessite O(n) affectations dans le cas le pire.
Tableaux : résumé
• L’avantage des tableaux est l’accès en temps constant à n’importe quel élément dont on
connaît la position : le nombre d’opérations pour accéder à un élément ne dépend pas de
la taille du tableau.
• Un inconvénient des tableaux est qu’on ne peut pas toujours les redimensionner, et même si
on le peut, un redimensionnement peut induire une recopie complète de tout le tableau.
Le nombre d’affectations nécessaires est, dans ce cas, proportionnel au nombre d’éléments
du tableau.
• Un second inconvénient est que l’insertion dans un tableau nécessite de décaler tous les
éléments à la droite de celui qu’on veut insérer, ce qui demande à nouveau une recopie.
Dans le cas le pire, si on insère un élément en première place, ce décalage demande un
nombre d’affectations proportionnel au nombre d’éléments du tableau.
1.1
Quelques exercices sur les tableaux
1. Écrire une fonction int_array_imperative_print, qui affiche un tableau de manière itérative.
Exercice 2.1
Exemple : int_array_imperative_print [|5;3;2;17;1;2;12|] doit afficher la séquence suivante :
5 3 2 17 1 2 12 − : unit = ().
2. Quelle est la complexité de la fonction précédente ?
Solution.
1
2
3
4
5
6
let int_array_imperative_print tab =
let taille = Array. length tab in
for i = 0 to pred taille do
print_int tab .(i);
print_string " ";
done
Exercice 2.2
récursive.
1. Écrire une fonction int_array_recursive_print, qui affiche un tableau de manière
Exemple : int_array_recursive_print [|5;3;2;17;1;2;12|] doit afficher la séquence suivante :
9
5 3 2 17 1 2 12 − : unit = ().
2. Quelle est la complexité de la fonction précédente ?
Solution.
1
2
3
4
5
6
7
8
9
10
let int_array_recursive_print tab =
let taille = Array. length tab in
let rec aux i =
if i < taille then
begin
print_int tab .(i);
print_string " ";
aux (i + 1);
end;
in aux 0
Exercice 2.3
1. Écrire une fonction array_simple_sort, qui trie un tableau par sélection.
Exemple : L’évaluation au toplevel de l’expression array_simple_sort [|5;3;2;17;1;2;12|] donne
− : int array = [|1; 2; 2; 3; 5; 12; 17|] .
2. Quelle est la complexité de la fonction précédente ?
Solution.
1
2
3
4
5
6
7
8
9
10
11
12
13
let array_simple_sort t =
let m = Array. length t in begin
for i=0 to (m -2) do
for j = i+1 to (m -1) do
if (t.(i)>t.(j)) then
let v = t.(j) in begin
t.(j) <- t.(i);
t.(i) <- v;
end;
done
done;
t;
end
14
15
16
17
18
let swap_tab tab i j = (* O(1) *)
let tmp = tab .(i) in
tab .(i) <- tab .(j);
tab .(j) <- tmp
19
20
21
22
23
24
25
26
27
28
29
30
31
let selection_sort_rec tab =
let len = Array. length tab -1 in
let indice_min i = (* O(len(tab) - i) *)
let rec aux j im =
if j <= len then
aux (j + 1) (if tab .(j) < tab .(im) then j else im)
else
im
in aux (i + 1) i in
let rec aux i =
if i < len then
begin
10
swap_tab tab i ( indice_min i);
aux (i + 1)
end
32
33
34
in
begin
aux 0;
tab
end
35
36
37
38
39
1. Écrire une fonction dichotomic_search, qui recherche un élément x dans un tableau trié par une approche dichotomique et renvoie vrai ou faux suivant si l’élément est là
ou non.
Exercice 2.4
Exemple :
• L’évaluation au toplevel de dichotomic_search [|1;2;7;7;7;12;17;23;42|] 23 donne − : bool = true.
• L’évaluation au toplevel de dichotomic_search [|1;2;7;7;7;12;17;23;42|] 24 donne − : bool = false.
2. Quelle est la complexité de la fonction précédente ?
Solution.
1
2
3
4
5
6
7
8
9
10
11
12
2
let dichotomic_search tab x =
let rec dichotomic_search_aux tab x left right =
if left > right then
false
else
let middle = (left + right )/2 in
let v = tab .( middle ) in
if x = v then true else
if x < v then dichotomic_search_aux tab x left ( middle - 1)
else (* x > v *)
dichotomic_search_aux tab x ( middle + 1) right
in dichotomic_search_aux tab x 0 (Array. length tab - 1)
La structure de donnée de liste
Une autre structure de données dite « linéaire » qui permet de représenter en mémoire des multiensembles est la liste. Au lieu de représenter un multi-ensemble sous la forme d’un tableau, c’est-à-dire
par une suite de cases consécutives en mémoire, nous utilisons l’idée suivante : chaque élément est
placé dans une structure de deux champs, le premier contenant l’élément à mémoriser, et le second
contenant l’adresse de la structure suivante. Le dernier élément de la liste sera associé à une valeur
spéciale, notée traditionnellement NULL. La liste elle-même peut être représentée par un pointeur sur
le premier élément de la liste, appelé tête de la liste, ou head (si elle est vide, la liste sera représentée
par la valeur NULL). Du point de vue de la programmation, en langage C par exemple, la valeur
NULL est une adresse invalide, ce qui permet de la différencier des pointeurs de la liste, et ainsi
de détecter la fin de la liste. La Figure 2.5 représente le même multi-ensemble que précédemment,
S = {{15 , 17 , 42 , 42 , 45 , 84 , 99}}, les éléments apparaissant dans cet ordre.
15
17
42
42
45
head
Fig. 2.5 : Une liste simplement chaînée d’entiers
11
84
99
NULL
Remarque. 2.5 Comme dans le cas des tableaux, une liste fournit implicitement un ordre sur les
éléments (voir Remarque 1).
À nouveau, ce choix d’implantation a des avantages et des inconvénients.
1. Un inconvénient est que contrairement au cas des tableaux, l’accès à un élément n’est pas direct :
accéder à un élément nécessite de « suivre » la chaîne de pointeurs, ce qui, à partir du début de
la liste, a un coût O(n) dans le cas le pire (où n est la taille de la liste).
2. Au contraire des tableaux, les opérations d’insertion en tête et de suppression peuvent être
implantées en O(1). En particulier, le coût de l’ajout de k éléments en tête d’une liste de taille
n a un coût O(k), qui ne dépend pas de n.
La Figure 2.6 montre les O(1) étapes de suppression d’un élément de la liste, en supposant qu’il a déjà
été recherché (rappel : la recherche elle-même a une complexité O(n), où n est la taille de la liste). On
suppose donc l’élément à supprimer pointé par x, et on appelle next le champ de la structure pointant
sur la cellule suivante de la liste.
x
15
17
42
42
45
84
99
NULL
42
45
84
99
NULL
42
45
84
99
NULL
45
84
99
NULL
head
y
x
y = x−>next;
15
17
42
head
y
x
x−>next = y−>next;
15
17
42
head
y
x
15
free (y);
17
42
head
Fig. 2.6 : Suppression dans une liste en temps constant
Listes : résumé
• L’inconvénient principal des listes est l’accès d’un élément en temps O(n) dans le cas le
pire, si la liste est de taille n, même si on sait que l’élément est le ième dans la liste.
• Un avantage est qu’on peut ajouter/supprimer un élément en tête, en temps O(1).
• De façon plus générale, si on dispose d’un pointeur sur un élément de la liste, on peut insérer ou supprimer un élément en temps O(1), sans sans recopie des éléments présents.
2.1
Les listes en OCaml
Tout comme dans la plupart des langages, on pourrait définir un type liste en langage OCaml. La
définition peut se faire de la façon suivante, si on veut définir les listes d’entiers.
1
2
3
type i n t _ l i s t =
| Empty
| C e l l of i n t * i n t _ l i s t
C’est une définition de type récursive. Elle exprime qu’une liste est :
12
• soit la liste vide, qu’on choisit de représenter par la valeur Empty.
• soit composée d’un couple (entier, liste).
Par exemple, la liste de la figure 2.5 s’écrirait
1
2
3
4
5
6
7
Cell (15 ,
Cell (17 ,
Cell (42 ,
Cell (42 ,
Cell (45 ,
( Cell (84 ,
C e l l ( 9 9 , Empty ) ) ) ) ) ) ) )
Remarque. 2.6 Les identificateurs permettant de construire les types, ici Empty et Cell, doivent
commencer par une majuscule (au contraire du nom du type, int_list , qui doit commencer par une
minuscule).
Une remarque importante est que définir une liste de flottants, ou une liste de chaînes de caractères,
se ferait de façon analogue. Comme de nombreux algorithmes sur les listes ne dépendent pas du type
des éléments de la liste, il serait intéressant d’avoir une notion de liste dont le type des éléments est
un paramètre. Il est possible d’écrire une telle définition en OCaml. Dans ce cas, il faut utiliser un
paramètre de type, commençant par une apostrophe, par exemple ’a, ou ’foo. Par exemple :
type ’ f o o l i s t =
| Empty
| C e l l of ’ f o o * ’ f o o l i s t
1
2
3
OCaml déterminera le type d’une liste en fonction de celui de ces éléments. Par exemple :
#
−
#
−
#
−
1
2
3
4
5
6
Empty ; ;
: ’ a l i s t = Empty
C e l l ( 1 , Empty ) ; ;
: i n t l i s t = C e l l ( 1 , Empty )
C e l l ( 1 . 5 , Empty ) ; ;
: f l o a t l i s t = C e l l ( 1 . 5 , Empty )
Enfin, il est en fait inutile d’écrire notre propre type, car Ocaml fournit déjà un type ’a list , ainsi
que des fonctions et opérateurs. De ce fait,
• Plutôt que la notation lourde ci-dessus pour écrire la liste de la Figure 2.5, on écrit simplement
[15;17;42;42;45;84;99] . Attention, les éléments sont séparés par des « ; > > et non des « , > >.
• La liste vide se note donc [] .
• On peut ajouter un élément en tête de liste avec l’opérateur :: . Par exemple, 1::[2;3] est la liste
[1;2;3] , qu’on peut aussi obtenir par 1::2::3::[] .
• On peut concaténer deux listes grâce à l’opérateur @. Par exemple, [1;2;3] @ [4;5] est la liste
[1;2;3;4;5] .
• Plusieurs fonctions utiles sont disponibles dans la bibliothèque : voir https ://caml.inria.fr/
pub/docs/manual-ocaml/libref/List.html. Il est possible de visualiser leur code à l’aide de
l’utilitaire ocamlbrowser.
2.2
Quelques exercices sur les listes
Exercice 2.5 Écrire une fonction conc qui concatène deux listes sans utiliser l’opérateur de conca-
ténation @ de Caml.
Exemple : conc [1;2;3] [4,5] s’évalue en − : int list = [1; 2; 3; 4; 5].
Note. Cette fonction existe dans la bibliothèque standard OCaml, elle peut être utilisée sous le
nom List .append. Voir https ://caml.inria.fr/pub/docs/manual-ocaml/libref/List.html.
13
Solution.
1
2
3
4
let rec conc l1 l2 = match l1 with
| [] -> l2
| q::t -> let r = conc t l2 in q::r
Le code de la fonction List .append de la bibliothèque standard OCaml est disponible grâce à
l’utilitaire ocamlbrowser.
Exercice 2.6 Écrire une fonction mir qui renverse une liste, sans utiliser d’opérateur d’itération sur
les listes. Exemple : mir [1;2;3;4;5] doit donner − : int list = [5; 4; 3; 2; 1].
Solution.
1
2
3
let rec mir l1 = match l1 with
| [] -> []
| q::t -> let l2 = mir t in l2 @ q
Cette fonction existe dans la bibliothèque standard sous le nom List .rev.
Exercice 2.7 Écrire une fonction pref qui renvoie la liste des n premiers éléments d’une liste, et une
erreur si la liste est trop courte (c’est-à-dire a strictement moins de n éléments).
Exemple : pref [1; 2; 3; 4] 2 doit donner − : int list = [1; 2].
Solution.
1
2
3
4
let rec pref l1 n = match l1 ,n with
| _,0 -> []
| [],_ -> failwith "pas assez d' element (s)"
| q::t,_ -> let r = pref t (n -1) in q::r
Exercice 2.8 De la même façon, écrire une fonction suff qui renvoie la liste des n derniers élé-
ments d’une liste l1 , et une erreur si ce n’est pas possible. Exemple : suff [1;2;3;4] 2 doit donner
− : int list = [3; 4].
Solution.
1
2
3
4
let suff l1 n =
let l2 = mir l1 in
let l3 = pref l2 n in
mir l3
Listes triées
Exercice 2.9 Écrire une fonction insert qui insère dans une liste triée un élément, à sa place dans
la liste.
Exemple : insert [1;3;4;7;8] 7 doit donner − : int list = [1; 3; 4; 7; 7; 8].
Solution.
1
2
3
4
let rec insert l1 x = match l1 with
| [] -> [x]
| q::t -> if q < x then q :: ( insert t x )
else x::l1
Exercice 2.10 Écrire une fonction tri_insertion , qui trie une liste en utilisant la fonction insert .
Quelle est sa complexité ?
14
Solution.
1
2
3
4
5
6
let tri_insertion l1 =
let rec tri_insertion_rec l1 l2 = match l1 with
| [] -> l2
| q::t -> let l3 = insert l2 q in tri_insertion_rec t l3
in
tri_insertion_rec l1 []
Exercice 2.11 Écrire une fonction split qui coupe une liste l1 en deux deux listes d’éléments de l1
de tailles égales (à un élément près).
Exemple : split [1;2;3;4;5] doit donner − : int list * int list = [1; 3; 5], [2; 4].
Solution.
1
2
3
4
let
|
|
|
rec split = function
[] -> [] ,[]
[q] -> [q],[]
q::s::r -> let l1 ,l2 = split r
in q::l1 , s::l2
Exercice 2.12 Écrire une fonction merge qui fusionne deux listes triées en une nouvelle liste triée.
Exemple : merge([1; 4; 4; 7],[1;2;6]) doit donner − : int list = [1; 1; 2; 4; 4; 6; 7].
Solution.
1
2
3
4
let
|
|
|
5
6
rec merge = function
l1 ,[] -> l1
[],l2 -> l2
q::r,s::t -> if (q <= s)
then q :: merge(r, s::t)
else s :: merge( q::r, t)
Exercice 2.13 Écrire une fonction merge_sort qui utilise récursivement les deux fonctions split et
merge pour trier une liste. Quelle est sa complexité ? Exemple : tri_fusion
donner − : int list = [2; 4; 4; 5; 5; 6; 8; 19].
[8;2;4;4;5;6;19;5] doit
Solution.
1
2
3
4
5
let
|
|
|
rec merge_sort = function
[] -> []
[x] -> [x]
l -> let l1 ,l2 = split(l) in
merge ( merge_sort (l1), merge_sort (l2))
Exercice 2.14 — (Plus difficile). On suppose avoir une variable du type suivant, écrit en langage C :
struct c e l l {
struct c e l l * next ;
}
Sur la figure suivante, deux exemples de telles structures sont pointées chacune par une variable,
x1 pour la première et x2 pour la seconde :
15
NULL
x1
x2
On dit que la variable x1 représente une liste, alors que la variable x2 représente un lasso, car
aucune cellule dont l’unique champ est NULL n’est accessible depuis x2. Enfin, on appelle longueur
associée à la variable de type struct cell * le nombre de cellules accessibles depuis cette variable.
Ainsi, la longueur associée à x1 est 7, et celle associée à x2 est 10.
1. Écrivez un algorithme de complexité O(n2 ) dans le cas le pire, pour déterminer si une variable
dont la longueur associée est n représente une liste ou un lasso.
2. Même question avec une complexité O(n). Proposer au moins 4 algorithmes différents.
16
Chapitre 3
Arbres et arbres de recherche
Les tableaux et les listes permettent de représenter des multi-ensembles de deux manières différentes, et chacune, du fait de son organisation en mémoire, possède ses propres propriétés algorithmiques. La table suivante suppose que les éléments des multi-ensembles peuvent être ordonnés. Les
éléments d’un multi-ensemble peuvent donc être triés par ordre croissant (d’où les colonnes Tableau
trié et Liste triée). Dans cette table, n désigne le nombre d’éléments du tableau ou de la liste représentant le multi-ensemble. La table récapitule les propriétés de ces deux structures vues dans le cours
d’algorithmique du 1er semestre de L2 et dans le chapitre précédent.
Recherche d’un élément
Recherche du minimum
Insertion d’un élément
Suppression d’un élément
Tableau
Tableau trié
Liste
Liste triée
O(n)
O(n)
O(n)
O(n)
O(log n)
O(1)
O(n)
O(n)
O(n)
O(n)
O(n)
O(n)
O(n)
O(1)
O(n)
O(n)
Tab. 3.1 : Résumé des complexités de certaines tâches sur tableaux et listes
Un avantage du tableau trié est qu’il permet la recherche d’un élément en temps O(log n), en utilisant un algorithme dichotomique, alors que celui des listes est l’insertion ou la suppression d’un élément
en temps constant (voir le Chapitre 2). L’insertion d’un élément dans un tableau non nécessairement
trié peut se faire en O(1) en maintenant l’indice du premier emplacement libre, et en supposant que
les emplacements libres sont maintenus en fin de tableau. En contrepartie, la suppression d’un élément
nécessite un décalage, pour maintenir les emplacements libres en fin de tableau. Enfin, la complexité
O(n) de l’insertion ou de la suppression d’un élément dans une liste provient de la recherche de cet
élément. Une fois cette recherche effectuée, l’insertion et la suppression ne nécessitent plus que O(1)
opérations élémentaires, comme on l’a vu au Chapitre 2.
Remarque. 3.1 En pratique, la différence entre des complexités en O(n) et en O(log n) est significa-
tive. Par exemple, si n = 1015 (un peta-octet, c’est-à-dire un million de giga-octets), alors log(n) vaut
environ seulement 35. Cela signifie qu’un algorithme de complexité O(log n) sera utilisable même
pour de très grandes valeurs de n (en supposant raisonnable la constante multiplicative cachée dans
la notation O(·)).
1
De la liste à l’arbre
On aimerait combiner l’efficacité de la recherche dans les tableaux triés avec la facilité d’insertion/suppression d’un élément déjà trouvé fournie par les listes. La remarque 1 montre qu’à défaut de
parvenir à des complexités en O(1), obtenir des complexités en O(log n) est très intéressant.
Par ailleurs, garder les éléments à stocker contigus en mémoire pose des problèmes lorsqu’on veut
insérer ou supprimer un élément, car cette structure de données force à faire des décalages. On part
donc de l’idée suivante : la complexité O(log n) de recherche d’un élément dans un tableau trié provient
17
de l’algorithme de dichotomie (voir l’exercice 4 du Chapitre 2). Le problème dans une liste est que
l’on n’a pas accès à l’élément central (supposons pour simplifier que cet élément existe, c’est-à-dire
qu’on travaille sur une liste de taille impaire). Pour résoudre ce problème d’accès à l’élément central,
on pourrait représenter une liste triée par :
• sa partie gauche, constituée des éléments à gauche de l’élément central,
• son élément central,
• sa partie droite, constituée des éléments à droite de l’élément central.
Par exemple, on pourrait représenter la liste de la Figure 2.5 de la façon suivante :
15
17
head
42
NULL
45
42
84
head
99
NULL
Fig. 3.1 : Découpage d’une liste triée en partie gauche, élément central, partie droite
Par ailleurs, pour implanter ce triplet, on peut utiliser la représentation suivante (notez que la
nouvelle structure nécessaire, celle qui est pointée par x, utilise deux pointeurs) :
x
42
15
17
45
42
NULL
84
99
NULL
Fig. 3.2 : Structure de données pour découper une liste triée élément central, parties gauche et droite
Cette idée permet bien d’accélérer la recherche dans une liste triée. En effet, on a maintenant accès
à l’élément central, et on peut donc commencer à appliquer la méthode dichotomique : si l’élément
cherché est l’élément central, il est trouvé. Sinon, s’il est inférieur à l’élément central, on le cherche
dans la partie gauche, et sinon, dans la partie droite. On a donc divisé l’espace de recherche par 2.
Il est naturel de continuer cette idée et de subdiviser à nouveau les sous-listes gauche et droite. Sur
le même exemple, on obtiendrait :
x
42
84
17
15
NULL
42
NULL
45
NULL
99
NULL
Fig. 3.3 : Arbre découpant récursivement une liste
18
On obtient un arbre binaire : chaque nœud de l’arbre a au maximum 2 fils. De plus, comme on est
parti d’une liste triée, on peut vérifier que l’arbre a la propriété suivante : pour tout nœud z de l’arbre,
• les étiquettes des nœuds présentes dans le sous-arbre gauche de z sont inférieures ou égales à
l’étiquette de z.
• les étiquettes des nœuds présentes dans le sous-arbre droit de z sont supérieures ou égales à
l’étiquette de z.
Ces arbres binaires, dont les nœuds sont étiquetés par des valeurs ayant cette propriété, s’appellent
arbres binaires de recherche. Cette propriété est importante, car elle guide la navigation vers
un élément que l’on recherche, de la même façon que l’on guide la navigation dans l’algorithme de
recherche dichotomique. La longueur de la navigation est donnée par la hauteur, qui est la longueur
de la branche la plus longue de l’arbre. Par convention, la hauteur de l’arbre vide est −1, et
la longueur d’une branche est son nombre d’arêtes reliant les nœuds de la branche. Par exemple, la
hauteur de l’arbre de la Figure 3.3 est 2.
Si on part d’une liste triée de longueur n = 2k −1 et que l’on construit l’arbre comme dans l’exemple
ci-dessus, on obtiendrait un arbre dont toutes les branches auraient la même hauteur, k − 1 (l’exemple
est le cas où k = 3, n = 7). Autrement dit, le nombre de choix à effectuer serait k − 1 = log2 (n + 1) − 1.
2
Arbres binaires de recherche
Rappelons que nos objectifs sont les suivants. On veut :
• une structure de donnée permettant de représenter des multi-ensembles, et
• qui permet d’effectuer les opérations suivantes de façon efficace :
– Rechercher un élément dans le multi-ensemble,
– Ajouter un élément,
– Supprimer un élément.
On va organiser les éléments d’un multi-ensemble à représenter selon l’idée expliquée en Section 1.
Sur l’exemple présenté dans cette section, nous avons ignoré deux détails :
1. Une valeur peut être présente en plusieurs exemplaires dans le multi-ensemble à représenter. Cela
pose un problème potentiel, puisqu’on voudrait qu’en tout nœud de l’arbre, les valeurs présentes
dans le sous-arbre gauche soient inférieures à la valeur du nœud, et celles présentes dans le sousarbre droit soient supérieures. Pour gérer le cas de valeurs en plusieurs exemplaires et diminuer
le nombre de nœuds des arbres construits, on procède ainsi : au lieu de stocker individuellement
chaque valeur, on stocke des couples (x , n) où x est une valeur présente dans le multi-ensemble,
et n > 1 désigne le nombre de fois où x apparaît dans l’ensemble. Par exemple, on représentera le
multi-ensemble {{15 , 17 , 42, 42 , 45 , 84 , 99}} utilisé dans l’exemple de la Section 1 par l’ensemble
{(15 , 1) , (17 , 1) , (42, 2) , (45 , 1) , (84 , 1) , (99 , 1)}, parce que 42 apparaît 2 fois. Ainsi, il n’y aura
plus de duplication de valeurs. Cet ensemble pourrait être représenté par l’arbre de la Figure 3.4.
(42, 2)
(17, 1)
(15, 1)
(84, 1)
(45, 1)
(99, 1)
Fig. 3.4 : Un arbre représentant un multi-ensemble
On peut noter sur cet arbre que les 1ères coordonnées suivent la règle des arbres binaires de
recherche : la 1ère coordonnée d’un nœud est (strictement) supérieure aux 1ère coordonnées des
nœuds de son sous-arbre gauche et (strictement) inférieure aux 1ère coordonnées des nœuds de
son sous-arbre droit.
19
2
42
1
1
17
84
1
1
1
15
45
99
Fig. 3.5 : Une autre représentation de l’arbre de la Figure 3.4
Pour alléger les notations, on indiquera les secondes coordonnées, c’est-à-dire le nombre d’occurrences de l’élément, au dessus du nœud plutôt que dans un couple. L’arbre de la Figure 3.4 peut
donc se dessiner comme en Figure 3.5.
2. Le second détail ignoré en Section 1 est le suivant : dans l’exemple de la Section 1, tout nœud a 0
ou 2 fils. En général, ce ne sera pas vrai : par exemple le nœud 17 de la Figure 3.5 a un seul fils.
Si on préfère travailler avec des arbres dans lesquels tout nœud a 0 ou 2 fils, qu’on appelle arbres
binaires entiers (au sens de « plein > > : full binary tree en anglais), il suffit d’ajouter
à tout nœud qui n’a pas 2 fils une ou deux feuilles fictives, qui ne portent pas de valeur. On
pourrait voir ces feuilles comme un arbre vide. L’arbre ainsi obtenu est présenté en Figure 3.6.
2
42
1
1
17
84
1
1
1
15
45
99
Fig. 3.6 : Un arbre binaire entier
En résumé, l’arbre de la Figure 3.6 représente le multi-ensemble {{15 , 17 , 42 , 42 , 45 , 84 , 99}}. Les
feuilles, représentées par un carré, peuvent aussi être vues comme l’arbre vide. Chaque nœud
interne contient une clé et un entier (noté au dessus) qui représente le nombre de fois que la clé
apparaît dans le multi-ensemble.
On remarque que :
• cet arbre est entier (au sens ci-dessus),
• aucune de ses feuilles ne porte de valeur.
Dans la suite de ce chapitre, nous travaillerons avec de tels arbres. Nous travaillerons avec des
arbres binaires qui ne sont pas entiers plus tard dans d’autres chapitres. Le lien entre les deux
versions sera abordé à la fin de ce chapitre.
Remarque. 3.2 Le point 1 montre en fait que l’on peut sans plus de travail représenter des dic-
tionnaires en utilisant les arbres binaires de recherche. En effet, on s’apprête à mémoriser dans
les nœuds des couples (x , n) où x n’apparaît pas plus d’une fois. De façon plus générale, on peut
mémoriser des couples (k , v), celui où k est une clé et v la valeur associée. La représentation d’un
multi-ensemble est simplement un cas particulier, où les clés sont les éléments du multi-ensemble
et la valeur associée à une clé est le nombre de fois où l’élément apparaît.
On va maintenant définir formellement le type arbre binaire de recherche. Ce type permet, comme
expliqué en Remarque 2, de représenter un dictionnaire. La définition 1 utilise donc deux ensembles : le
premier, K, correspond aux clés, et le second, V , aux valeurs associées aux clés. Suivant la Remarque 2,
20
la représentation des multi-ensembles est un simple cas particulier : pour représenter un multi-ensemble
d’éléments de E, on prendra K = E (par exemple les entiers dans l’exemple de la Section 1), et V = N,
pour représenter le nombre d’occurrences de chaque valeur.
Definition 3.1 — Arbre binaire de recherche. Soit K un ensemble muni d’une relation d’ordre total
6, et V un ensemble. On écrit k1 < k2 si k1 6 k2 et k1 6= k2 . Un arbre binaire de recherche sur
K , V est un arbre dont chaque nœud est :
• ou bien une feuille. Dans ce cas, le nœud n’est pas étiqueté.
• ou bien un nœud ayant exactement deux fils. Dans ce cas, le nœud est étiqueté par un couple
(k , v), où e ∈ K et v ∈ V . De plus, pour tout nœud de son sous-arbre gauche étiqueté par
(k` , v` ), on a k` < k. Symétriquement, pour tout nœud de son sous-arbre droit est étiqueté
par (kr , vr ), alors k < kr .
Remarque. 3.3 — Important. On pourrait être tenté de simplifier la définition, en disant seulement
qu’en chaque nœud binaire, si on appelle x, x` , et xr les clés de ce nœud, de son fils gauche et de
son fils droit, on a x` < x < xr . Cette condition est nécessaire dans un arbre binaire, mais elle n’est
pas suffisante. Par exemple, elle est vraie pour l’arbre suivant, mais ce n’est pas un arbre binaire
de recherche.
2
42
1
1
17
84
1
1
0
99
Le problème est le nœud qui porte la valeur 0. En effet, comme il se trouve dans le sous-arbre droit
de la racine qui porte 42, la valeur qu’il porte devrait être au moins 43, donc pas 0.
À nouveau, on peut penser aux feuilles comme représentant l’arbre vide. Elles seront notées Empty
dans le code OCaml, mais ce nom est juste une convention : on pourrait appeler aussi cette constante
Leaf, si on voit une feuille non pas comme l’arbre vide mais comme un arbre avec un seul sommet
(qui serait à la fois une feuille et sa racine).
Dans le cas de la représentation d’un multi-ensemble, rappelons à nouveau que V = N permet
de compter le nombre d’apparitions d’un élément dans le multi-ensemble : pour chaque nœud interne
étiqueté par (k , v), l’entier v représente le nombre de fois que k apparaît dans l’ensemble représenté
par l’arbre binaire. Il est appelé multiplicité de k. Par exemple, dans l’arbre de la Figure 3.6, la
multiplicité de 42 est 2 et la multiplicité de 17 est 1.
1. Écrire un type OCaml (’ k ,’ v) bst pour définir un type arbre binaire de recherche,
où ’k représentera le type des clés et ’v le type des valeurs.
Exercice 3.1
2. Écrire un type OCaml ’k bst_multiset pour définir un type arbre binaire de recherche représentant un multi-ensemble, où ’k est le type des éléments du multi-ensemble.
Solution.
1. type ('k,'v) bst =
Empty
3
| Node of ('k,'v) bst * ('k * 'v) * ('k,'v) bst
1
2
2. On peut reprendre la définition précédente en remplaçant ’v par int, ou plus directement :
type ’k multiset = (’k, int ) bst.
21
Exercice 3.2 En utilisant l’un des types définis dans l’exercice 1, écrire l’arbre de recherche de la
Figure 3.6 en OCaml.
Solution.
1
let bst_uniq x = Node (Empty , (x,1), Empty)
2
3
4
5
let a = Node ( bst_uniq 15, (17 ,1) , Empty)
in let b = Node ( bst_uniq 45, (84, 1), bst_uniq 99)
in Node (a, (42 ,2) , b)
1. Écrire une fonction bst_search qui recherche un élément x dans un arbre binaire
de recherche représentant un multi-ensemble, et qui renvoie 0 s’il est absent, et sa multiplicité
s’il est présent.
Exercice 3.3
2. Quel est le type de cette fonction ?
Solution.
1. let rec bst_search x t =
match t with
3
| Empty -> 0
4
| Node(left , (y,n), right) ->
5
if x = y then n
6
else if x < y then bst_search x left
7
else bst_search x right
1
2
2. La fonction bst_search est une fonction de type (’ a, int ) bst −> ’a −> int. Remarquez que
OCaml déduit automatiquement le type int pour les valeurs, parce que la fonction renvoie 0
si l’élément n’est pas présent dans l’arbre.
1. Écrire une fonction list_of_bst qui prend en argument un arbre binaire de recherche qui représente un multi-ensemble, et qui construit une liste triée en ordre croissant
des éléments de l’arbre, en tenant compte de leurs multiplicités. Par exemple, pour l’arbre de
la Figure 3.6, la fonction devra renvoyer la liste [15; 17; 42; 42; 45; 84; 99].
Exercice 3.4
2. Quel est le type de cette fonction ?
Solution.
1. let rec list_of_bst t =
let rec copycat x n l =
(* Ajoute x n fois en tête de l *)
3
if n = 0 then l else copycat x (n -1) (x::l)
4
in
5
match t with
6
Empty -> []
7
| Node(left , (k,n), right) ->
8
( list_of_bst left) @ ( copycat k n ( list_of_bst right ))
1
2
2. (’ a, int ) bst −> ’a list.
Exercice 3.5 — Complexité (plus difficile). Remarquez qu’on ne peut pas écrire une version de la
fonction list_of_bst de l’exerice 4 qui travaillerait en temps linéaire par rapport à la taille de
l’arbre binaire de recherche passé en argument. En effet, si l’arbre contient un seul nœud binaire
dont l’étiquette est de multiplicité n, alors la taille de la liste résultat est n. On va donc s’intéresser
à sa complexité en fonction de la taille de la liste retournée.
22
1. Votre fonction écrite dans l’exerice 4 est-elle de complexité linéaire par rapport à la taille de
la liste retournée ? Pourquoi ?
2. Écrire une version list_of_bst_efficient de cette fonction de complexité linéaire par rapport
à la taille de la liste résultat. Cette question est plus difficile. Pour la résoudre, vous pouvez :
(a) Dans un premier temps, calculer une fonction qui prend en argument une liste d’arbres
binaires de recherche, et qui renvoie la liste triée des nœuds de la suite de ces arbres,
avec leur multiplicité (la valeur retournée est donc une liste d’arbres ayant chacun un
seul nœud interne).
(b) Dans un second temps, faire une fonction qui calcule le résultat voulu à partir de cette
liste.
Solution.
1. Non : la fonction écrite dans la solution de l’exercice 4 est au moins en O(n2 ) à cause de la
concaténation. A détailler.
2. (* Crée la liste [x;x;...;x]:: acc où x est répété n fois *)
let rec repeat x n acc =
3
if n <= 0 then acc else repeat x (n -1) (x:: acc)
1
2
4
5
(*
6
7
8
9
10
11
12
13
14
15
16
Explicite une liste représentant un multi - ensemble :
Chaque (x,n) dans la liste est remplacé par x;x;...;x (n fois ).
La liste renvoyée est inversée .
Par exemple , unzip_rev [('a ' ,5); ('b ' ,2)] vaut ['b'; 'b'; 'a'; 'a'; 'a'; 'a'
*)
let rec unzip_rev l =
let rec helper l acc =
match l with
| [] -> acc
| (k,v)::r -> helper r ( repeat k v acc)
in helper l []
17
18
19
20
(*
Fournit la liste " décompressée " d'un arbre binaire de recherche t.
On partira de la liste [t], avec un accumulateur vide.
21
22
23
Cette liste est traitée suivant la valeur de son premier élément :
a) si c'est l'arbre vide , on le supprime .
24
25
26
27
b) si c'est un arbre avec un seul noeud interne (k,v), on mémorise ce couple
dans l' accumulateur et on supprime ce premier élément de la liste dans
l'appel récursif .
28
29
30
31
32
33
34
35
c) si c'est un arbre avec >= 2 noeuds internes , on calcule la liste obtenue
remplaçant , dans l'appel récursif , ce 1er élément par 3 arbres :
- son sous -arbre gauche ,
- l'arbre ayant juste un noeud: sa racine ,
- son sous -arbre droit.
Ce faisant , la liste d' arbres obtenue a, inductivement , des clés triées e
ordre croissant (les clés du k-ème arbre sont inférieures à celles du (k+
36
37
38
On continue récursivement jusqu 'à ce que la liste soit vide. A ce moment , on
a inséré dans l' accumulateur la liste des couples (k,v) par clés croissantes
23
Comme on a inséré en tête , cette liste est inversée dans l' accumulateur .
L'appel unzip_rev permet de les remettre dans l'ordre.
39
40
41
Il est simple de voir que la fonction effectue un nombre linéaire de :: (un
nombre constant par noeud de l'arbre ).
42
43
44
45
46
47
48
49
50
51
52
53
54
*)
let list_of_bst_efficient t =
let rec helper l acc =
match l with
[] -> acc
| Empty ::a -> helper a acc
| (Node(a, (k,v), b))::r ->
if a = Empty && b = Empty
then helper r ((k, v):: acc)
else helper (a::( Node(Empty , (k, v), Empty ))::b::r) acc
in unzip_rev ( helper [t] [])
Exercice 3.6 Écrire une fonction bst_insert qui prend en argument un arbre binaire de recherche
représentant un multi-ensemble d’éléments d’un ensemble E, ainsi qu’un élément e de E qui renvoie
l’arbre binaire de recherche représentant le multi-ensemble d’origine auquel on a ajouté E.
Solution.
let rec bst_insert x t =
match t with
Empty -> Node (Empty , (x,1), Empty)
| Node(left , (y, n), right) ->
if x = y then Node (left , (y, n+1), right)
else if x < y then Node ( bst_insert x left , (y, n), right)
else Node (left , (y, n), bst_insert x right)
1
2
3
4
5
6
7
1. En utilisant la fonction écrite en exercice 6, écrire une fonction bst_of_list qui
prend en argument une liste d’éléments d’un ensemble E, et qui retourne un arbre binaire de
recherche représentant le même multi-ensemble que la liste.
Exercice 3.7
2. En utilisant la fonction écrite dans l’exercice 4, écrire une fonction qui prend une liste en
argument et calcule la liste d’éléments triée dans l’ordre croissant. Tester cette fonction sur
des listes d’entiers ou de chaînes de caractères.
3. Quelle est la complexité de la fonction écrite en question 2.
Solution.
1. let bst_of_list l =
let rec aux l acc =
3
match l with
4
[] -> acc
5
| head :: tail -> aux tail ( bst_insert head acc)
6
in aux l Empty
1
2
1
let list_sort l = list_of_bst ( bst_of_list l)
3.
2.
Exercice 3.8 Écrire une fonction bst_min qui identifie l’élément minimum d’un arbre binaire de
24
recherche, et renvoie un couple :
• la première composante du couple est soit Empty si l’arbre est vide, soit l’arbre ayant un
unique nœud interne dont la valeur est le minimum, avec sa multiplicité,
• la seconde composante du couple est l’arbre obtenu en supprimant le nœud où se trouvait
l’élément minimum.
Solution.
1
2
3
4
5
6
7
Le minimum se retrouve en suivant les fils gauches.
let rec bst_min t =
match t with
| Empty -> Empty , Empty
| Node(Empty , (k,v), right) -> ( Node(Empty , (k,v), Empty), right )
| Node(left , (k,v), right) ->
let ( min_node , left_without_min ) = bst_min left in
( min_node , Node( left_without_min , (k, v), right) )
Exercice 3.9 Symétriquement, écrire une fonction bst_max qui identifie l’élément maximal d’un
arbre binaire de recherche, le renvoie, et le supprime de l’arbre (quelque soit sa multiplicité).
L’insertion dans un arbre binaire de recherche est simple, parce qu’elle intervient au niveau des
feuilles. La suppression est plus difficile. Pour supprimer une occurrence d’un élément x de multiplicité n dans un arbre binaire de recherche qui représente un multi-ensemble, on peut procéder ainsi.
L’algorithme est décrit de façon itérative, mais il faudra le programmer de façon récursive.
• On recherche le nœud de clé x (qui doit exister si on veut supprimer une occurrence de x). Puis,
– Si la multiplicité de x est strictement supérieure à 1, il suffit de la décrémenter.
– Sinon, la multiplicité de x est 1. C’est plus difficile dans ce cas, où on doit produire un
arbre ayant un nœud de moins que l’arbre d’origine. La difficulté est aussi que l’on
veut garder la propriété que l’arbre obtenu reste un arbre binaire de recherche. Comme la
suppression va affecter uniquement le sous-arbre enraciné au nœud à supprimer, on peut
décrire l’algorithme en supposant que ce nœud, étiqueté (x , 1), se trouve à la racine. On
distingue plusieurs cas :
*
*
*
*
Si les deux fils du nœud racine (x , 1) sont vides, le résultat est simplement l’arbre vide.
Si le nœud fils droit du nœud racine (x , 1) est vide, le résultat est le sous-arbre gauche.
Si le nœud fils gauche du nœud racine (x , 1) est vide, le résultat est le sous-arbre droit.
Sinon, le nœud racine (x , 1) a deux fils non vides. On calcule l’élément dont la clé
est juste avant celle de x. C’est la clé maximale dans le sous-arbre gauche (elle se
trouve en suivant les fils droits dans le sous-arbre gauche). Appelons y cette clé et m
sa multiplicité. Le résultat est l’arbre obtenu de l’arbre original en supprimant la clé
(y , m) et en étiquetant la racine par (y , m) au lieu de (x , 1).
Exercice 3.10 Écrire une fonction bst_remove qui prend comme arguments un arbre binaire de
recherche et un élément e, et renvoie l’arbre binaire de recherche correspondant à l’arbre d’entrée
dans lequel on a supprimé une occurrence de e.
On pourra utiliser une fonction auxiliaire bst_pop_max bst qui prend en paramètre un abr
bst et qui retourne l’abr dans lequel le noeud contenant l’élément maximum a été supprimé et en
seconde valeur le couple (e, m) où e est l’élément maximum supprimé et m sa multiplicité.
La fonction bst_remove e bst parcourt récursivement l’arbre en le reconstruisant jusqu’à rencontrer éventuellement l’élément e à rechercher ; si l’élément a une multiplicité strictement plus
grande que 1, le noeud est conservé dans le nouvel arbre avec une multiplicité décrémentée de 1
sinon s’il n’y a pas de sous-arbre gauche, il suffit de retourner le sous-arbre droit, sinon grâce à la
fonction auxiliaire appliquée au sous-arbre gauche, on récupère l’élément maximum et le sous-arbre
gauche privé de ce maximum qui permettent de reformer un abr ayant ce maximum à la racine.
25
Solution.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
let rec bst_remove e bst = (* O(h) with h = heigth (bst) *)
(* bst_pop_max returns bst ', (emacs , m) where
with emax max element of bst , m its multiplicity and bst ',
bst without (emax , m) *)
let rec bst_pop_max bst = (* O(h) with h heigth of max *)
match bst with
Empty -> failwith "no max in empty bst"
| Node(left , em , Empty) -> left , em
| Node(left , em , right) ->
let bst1 , em1 = bst_pop_max right in
Node(left , em , bst1), em1
in
match bst with
Empty -> bst
| Node(left , (x, m), right) ->
if e = x then
if m > 1 then
Node(left , (e, m - 1), right)
else (* m = 1, node must be removed *)
if left = Empty then
right
else
let b, em = bst_pop_max left in
Node(b, em , right)
else
if e < x then
Node( bst_remove e left , (x, m), right)
else
Node(left , (x, m), bst_remove e right)
Exercice 3.11 Écrire une fonction qui prend en argument un arbre binaire de recherche, et
retourne une chaîne de caractères qui le représente suivant ce codage.
La lecture d’un arbre affiché en mode texte (sans graphisme) est souvent compliquée : dès que
l’arbre est un peu large, il devient très difficile de lire l’affichage produit. Il existe cependant une
astuce qui permet de visualiser les arbres simplement. L’idée est d’afficher les arbres horizontalement,
en utilisant le retour à la ligne du mode texte. La figure 3.7 montre un exemple de cet affichage.
1
12
1
1
8
23
3
2
4
9
2
11
(a) Arbre binaire de recherche
(b) Affichage en mode texte
Fig. 3.7 : Exemple d’affichage simple d’un arbre en mode texte.
26
1. Écrire une fonction tree_display qui affiche l’arbre en mode texte, comme sur
la Figure 3.7b.
Exercice 3.12
2. Écrire une fonction bst_dot qui affiche le code Graphviz permettant d’afficher un arbre binaire
de recherche graphiquement, en mettant le couple (clé, valeur) dans chaque nœud. La syntaxe
Graphviz est très simple, voir http ://graphviz.org/Documentation/dotguide.pdf et pour
les puristes, http ://www.graphviz.org/doc/info/attrs.html.
Solution.
1
2
3
let print_level_string string level =
print_string ( String .make (4 * level) ' ');
print_string string
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let rec tree_display tree =
let rec aux tree level =
match tree with
Empty -> print_level_string "-> Vide\n" level
| Node(l, (e, m), r) ->
begin
aux r (level + 1);
print_level_string
("->" ^ ( string_of_int e) ^ ":" ^ ( string_of_int m))
level;
print_newline ();
aux l (level + 1);
end
in begin
aux tree 0;
print_newline ()
end
Arbres binaires de recherche (ABR) : résumé
Qu’avons nous gagné par rapport aux listes et aux tableaux pour représenter un multi-ensemble ?
On a en fait introduit deux points allant dans des directions orthogonales.
• D’une part, si un élément est présent en plusieurs exemplaires dans le multi-ensemble, on ne le
représente qu’une fois, associé à sa multiplicité. Cela aurait très bien pu être fait de la même
façon au niveau des listes ou des tableaux, en mémorisant des couples (x , m) où x est un élément
et m > 1 est sa multiplicité.
• L’autre point important est une modification plus profonde de la structure de donnée : arbres
binaires de recherche au lieu de listes ou tableaux. Les opérations importantes
– de recherche d’un élément,
– d’insertion d’un élément,
– de suppression d’un élément,
– de recherche du minimum ou du maximum,
se font dans un temps proportionnel à la hauteur de l’arbre qui représente le multi-ensemble,
dans le cas le pire. Rappelons que la hauteur est la longueur de la plus longue branche. On compte
cette longueur en nombre de d’arêtes entre les nœuds de la branche, avec comme convention
que la hauteur de l’arbre vide est −1. Voir les 2 exemples de la Figure 3.8.
Plus précisément, appelons n le nombre de nœuds internes d’un arbre binaire de recherche permettant de représenter un multi-ensemble (c’est-à-dire que n est le nombre d’éléments différents
du multi-ensemble à représenter).
27
Appelons h(t) la hauteur minimale d’un tel arbre de recherche, et n le nombre de clés différentes
dans l’arbre, c’est-à-dire le nombre de nœuds de t. On a
– h(t) 6 n − 1 : cette hauteur maximale est atteinte dans le cas d’un arbre « filiforme > >,
– h(t) > log2 (n + 1) − 1 : cette hauteur minimale est atteinte dans le cas d’un arbre parfait
(c’est-à-dire, dont toutes les feuilles sont à la même profondeur, et tous les nœuds internes
ont exactement 2 fils).
1
7
1
6
2
5
7
4
1
1
4
3
1
2
1
1
2
6
1
3
2
1
7
1
1
3
5
7
(a) Arbre filaire, n = 7, h = 6 = n − 1
(b) Arbre parfait, n = 7, h = 2 = log2 (n + 1) − 1
Fig. 3.8 : Deux arbres à 7 clés
Nous obtenons alors le tableau de complexité suivant.
Recherche d’un élément
Recherche du minimum
Insertion d’un élément
Suppression d’un élément
Tableau
Tableau trié
Liste
Liste triée
ABR
O(n)
O(n)
O(1)
O(n)
O(log n)
O(1)
O(n)
O(n)
O(n)
O(n)
O(n)
O(n)
O(n)
O(1)
O(n)
O(n)
Θ(h(t))
Θ(h(t))
Θ(h(t))
Θ(h(t))
Pour les ABR, la notation Θ(h(t)) exprime que la complexité est comprise entre C1 h(t) et C2 h(t),
où C1 et C2 sont deux constantes. Avec la remarque précédente reliant h(t) à n, la complexité de tous
les algorithmes (recherche, insertion, suppression) est donc :
• Ω(log n), c’est-à-dire au moins log2 (n), à une constante multiplicative près, et
• O(n), c’est-à-dire au plus n, à une constante multiplicative près, et
Ce serait mieux si ces complexités étaient en O(log n), c’est-à-dire si les arbres pouvaient être maintenus
de hauteur O(log n). C’est l’objet du chapitre suivant.
Commentaire. Irène : je ne suis pas d’accord qu’on écrive de O(log n) à O(n) cela n’a aucun sens
mathématique. À la rigueur, on peut écrire O(hauteur(bst)) . D’ailleurs c’est quoi n, le nombre de
noeuds, le nombre d’éléments du multi-ensemble. Tout cela n’est pas très précis.
Marc : OK, merci, j’ai fait une rédaction plus précise (ou un peu moins fausse).
28
Chapitre 4
Arbres 2-3-4 et arbres bicolores
Ce chapitre présente les arbres 2-3-4 et les arbres bicolores, qui sont deux structures de données
d’arbres de recherche permettant de maintenir une hauteur logarithmique de la hauteur en fonction
du nombre de nœuds. D’après l’analyse en fin de chapitre précédent, cela implique que toutes les
opérations importantes (recherche, insertion et suppression) auront une complexité O(log(n)), ce qui
est un gain significatif par rapport à la situation avec les listes ou les tableaux.
Le matériel de ce chapitre est présenté dans la deuxième partie de ce document, ou dans le livre [3],
pages 238 à 255, disponible en ligne. à l’adresse https ://www.lri.fr/~mcg/PDF/FroidevauxGaudelSoria.
pdf.
Arbres 2-3-4
1. Un arbre de recherche est un arbre général étiqueté dont chaque nœud contient un k-uplet
d’éléments distincts et ordonnés , k = 1 , 2 , 3 , . . ..
2. Un nœud contenant les éléments x1 < x2 < . . . < xk a k + 1 sous-arbres, tels que :
• tous les éléments du premier sous-arbre sont inférieurs à x1 ,
• tous les éléments du i-ème sous-arbre, i = 2 , . . . , k, sont supérieurs à xi−1 et inférieurs à xi ,
• tous les éléments du (k + 1)ieme sous arbre sont supérieurs à xk .
3. Un arbre 2-3-4 est un arbre de recherche dont les nœuds sont de trois types, 2-nœud, 3-nœud ou
4-nœud, et dont toutes les feuilles sont situées à la même hauteur.
Exercice 4.1 Hauteur d’un arbre 2-3-4
Soit t un arbre 2-3-4 contenant n nœuds et soit h(t) sa hauteur.
Montrer que
log4 (3n + 1) − 1 6 h(t) 6 log2 (n + 1) − 1
2. En déduire que si m est le nombre d’éléments de l’arbre, on a
1.
log4 (m + 1) − 1 6 h(t) 6 log2 (m + 1) − 1
Exercice 4.2 Insertion dans un arbre 2-3-4 avec éclatements en remontée
Soit la suite des entiers 4 , 35 , 10 , 13 , 3 , 30 , 15 , 12 , 7 , 40 , 20 , 11 , 6.
Donner la construction de l’arbre 2-3-4 par insertions successives aux feuilles, à partir de l’arbre
vide.
L’insertion dans un arbre vide entraîne la création d’un 2-nœud.
L’insertion dans un i-nœud, i = 2 , 3, entraîne la création d’un i + 1-nœud.
L’insertion dans un 4-nœud, provoque l’éclatement de la feuille en deux 2-nœuds contenant respectivement le plus petit et le plus grand élément de la feuille, et l’élément médian doit être ajouté au
nœud père de la feuille.
Les éclatements peuvent remonter en cascade, éventuellement sur toute la hauteur de l’arbre si le
29
chemin suivi pour déterminer l’endroit de l’insertion n’est formé que de 4-nœuds.
Exercice 4.3 Insertion dans un arbre 2-3-4 avec éclatements à la descente
Pour éviter la propagation des éclatements de bas en haut, il suffit de travailler sur les arbres 2-3-4
qui ne contiennent jamais deux 4-nœuds à la suite : dans ce cas toute insertion provoque au plus
un éclatement. Ceci peut être réalisé en éclatant les 4-nœuds à la descente, lors de la recherche de
la place de l’élément à ajouter.
Reprendre l’exemple de l’éxercice précédent en utilisant la méthode d’éclatement à la descente.
Exercice 4.4 Éclatements des 4-nœuds.
Isoler sous forme de schémas d’arbres 2-3-4 les modifications locales possibles lors d’éclatements
des 4-nœuds.
Exercice 4.5 Représentation d’arbre 2-3-4
Donner des exemples de représentation des différents types de nœuds. Quels sont les inconvénients
des ces représentations ?
Exercice 4.6 Un arbre 2-3-4
Proposer un type OCaml type 'a tree234 pour définir le type arbre 2-3-4.
Exercice 4.7 Recherche dans un arbre 2-3-4
Écrire une fonction tree234_search qui recherche un élément dans un arbre 2-3-4, et renvoie 0 s’il
n’est pas présent, et sa multiplicité dans le cas contraire.
Quelle est sa complexité ?
Exercice 4.8 Visualization d’un arbre 2-3-4
Écrire une fonction tree234_to_dot qui produit la représentation de l’arbre 2-3-4 au format dot,
syntaxe Graphviz consultable à l’adresse :
http ://www.graphviz.org/pdf/dotguide.pdf
Arbres 2-3-4 : représentation par des arbres bicolores
Lectures utiles : [3, 4, 5, 2]
La représentation des arbres 2-3-4 par des arbres bicolores est adaptée d’après [3].
Les arbres bicolores sont une implémentation efficace des arbres 2-3-4 car ils supportent un traitement uniforme des différents types de nœuds des arbres 2-3-4. Un type de nœud d’un arbre 2-3-4
correspond à une hiérarchie des éléments contenus dans ce nœud. Ainsi par exemple, le 4-nœud illustré
sur la Fig. 4.2(a) correspond à l’hiérarchie définie par l’arbre binaire représenté sur la Fig. 4.2(b). Les
couleurs, noir et rouge, sont utilisées pour simuler cette hiérarchie. A chaque k-nœud, k = 2 , 3 , 4, d’un
arbre 2-3-4 on associe un arbre bicolore. La racine de cet arbre est coloriée en noir et contient un des
éléments du k-nœud. Les autres éléments du k-nœud sont placés dans les fils et sont coloriés en rouge.
Pour k = 2, la transformation est à l’identique à la coloration près.
Pour k = 3, la racine contient au choix, l’élément de gauche, la Fig. 4.1(b), ou de droite, la Fig. 4.1(c).
Pour k = 4, la racine contient l’élément central comme illustré sur la Fig. 4.2(b).
Un arbre bicolore est un arbre binaire de recherche qui satisfait les propriétés suivantes :
• Chaque noeud est soit rouge, soit noir.
• La racine est noire.
• Il n’existe pas de branche qui contient deux nœuds rouges consécutifs.
• Chaque branche contient le même nombre de nœuds noirs.
Exercice 4.9 Représentation d’un arbre 2-3-4 par un arbre bicolore
Soit l’arbre 2-3-4 donné sur la Fig. 4.3. Dessiner sa représentation sous la forme d’un arbre bicolore.
On utilisera les transformations des k-nœuds, k = 2 , 3 , 4 comme illustré sur la Fig. 4.1 et la Fig. 4.2
30
a
a, b
P0
P0
P1
b
P2
(a) 3-nœud
P1
P2
(b) arbre bicolore ”penché à droite”
b
a
P0
P2
P1
(c) arbre bicolore ”penché à gauche”
Fig. 4.1 : Transformation d’un 3-nœud, Fig. 4.1(a), en arbres bicolores, Fig. 4.1(b)(c)
b
a, b, c
a
P0
P1
P2
c
P3
(a) 4-nœud
P0
P1
P2
P3
(b) arbre bicolore
Fig. 4.2 : Transformation d’un 4-nœud, Fig. 4.2(a), en arbre bicolore, Fig. 4.2(b)
12, 45
20, 25, 40
8
3, 5
10
15
22, 24
30, 35, 38
(a)
Fig. 4.3 : Arbre 2-3-4
31
50, 60
43
48
55, 59
63, 65
a
a
β
α, β, γ
(a)
γ
α
(b)
a
β, a
β
α
α
γ
(d)
γ
(c)
Fig. 4.4 : Eclatement d’un 4-nœud, fils gauche d’un 2-nœud, Fig. 4.4(a), en un 3-nœud et deux 2nœuds, Fig. 4.4(d)
Exercice 4.10 Un arbre bicolore
Proposer un type OCaml type 'a bct pour définir le type arbre bicolore.
Exercice 4.11 Recherche dans un arbre bicolore
Écrire une fonction bct_search qui recherche un élément dans un arbre bicolore.
Exercice 4.12 Simulation des éclatements des nœuds d’un arbre 2-3-4
Pour chaque type d’éclatement d’un 4-nœud, intervenant dans l’insertion à la descente dans un
arbre 2-3-4, montrer comment il peut être simulé sur un arbre bicolore.
Des exemples sont donnés sur les figures Fig. 4.4, Fig. 4.5, Fig. 4.6, Fig. 4.7 et Fig. 4.8
Exercice 4.13 Rotations
Pour maintenir les propriétés des arbres bicolores on rééquilibre l’arbre par des transformations
locales appelées rotations. Il existe quatre types de rotation : les rotations simples, à droite (Fig. 4.9)
et à gauche (Fig. 4.10), et les rotations doubles, gauche-droite (Fig. 4.11) et droite-gauche (Fig. 4.12).
En utilisant des rotations écrire une fonction bct_combine_left e3 l r qui transforme l’arbre
bicolore de la Fig. 4.13(a) en l’arbre de la Fig. 4.13(b).
Par analogie, écrire une fonction bct_combine_right e1 l r qui transforme l’arbre bicolore de la
Fig. 4.14(a) en l’arbre de la Fig. 4.14(b).
Exercice 4.14 Implémentation d’arbres bicolores
Écrire une fonction bct_insert e t pour l’insertion de l’ élément e dans un arbre bicolore t.
Exercice 4.15 Construction d’arbres bicolores
Écrire une fonction list_to_tree l insert_fun qui à partir de la liste l et la fonction d’ insertion
insert_fun construit l’arbre en insérant successivement les éléments de la liste.
Tester cette fonction pour construire l’ arbre bicolore à partir de la liste 4 , 10 , 35 , 13 , 15 , 7 , 12 , 40 , 20
et 6.
[2]
Thomas H. Cormen et al. Introduction to Algorithms (3. ed.) MIT Press, 2009. isbn : 9780-262-03384-8. url : http ://mitpress.mit.edu/books/introduction-algorithms.
[3]
Christine Froidevaux, Marie-Claude Gaudel et Michèle Soria. Types de données et algorithmes. Collection Informatique. McGraw-Hill, 1990. url : https ://www.lri.fr/~mcg/PDF/
FroidevauxGaudelSoria.pdf.
32
a
a
β
α, β, γ
(a)
γ
α
(b)
a
a, β
β
γ
α
(d)
γ
α
(c)
Fig. 4.5 : Eclatement d’un 4-nœud, fils droit d’un 2-nœud, Fig. 4.5(a), en un 3-nœud et deux 2-nœuds,
Fig. 4.5(d)
a
a, b
β
b
α, β, γ
(a)
γ
α
(b)
a
β, a, b
β
b
α
α
γ
(d)
γ
(c)
Fig. 4.6 : Eclatement d’un 4-nœud, fils gauche d’un 3-nœud, Fig. 4.6(a), en un 4-nœud et deux 2nœuds, Fig. 4.6(d)
33
a
b
a, b
α, β, γ
β
(a)
γ
α
(b)
a
β
b
a
b
β
α
α
γ
γ
(d)
(c)
a, β, b
α
γ
(e)
Fig. 4.7 : Eclatement d’un 4-nœud, fils médian d’un 3-nœud, Fig. 4.7(a), en un 4-nœud et deux 2nœuds, Fig. 4.7(d)
34
a
b
a, b
α, β, γ
β
(a)
γ
α
(b)
a
b
b
a
β
β
α
γ
γ
α
(d)
(c)
a, b, β
α
γ
(e)
Fig. 4.8 : Eclatement d’un 4-nœud, fils droit d’un 3-nœud, Fig. 4.8(a), en un 4-nœud et deux 2-nœuds,
Fig. 4.8(d)
q
p
U
p
W
q
U
V
V
W
(a)
(b)
Fig. 4.9 : Rotation droite
p
U
V
q
q
p
W
U
W
V
(a)
(b)
Fig. 4.10 : Rotation gauche
35
r
r
p
q
W
q
T
p
U
V
T
W
V
U
(a)
(b)
q
p
T
r
U
V
W
(c)
Fig. 4.11 : Rotation gauche-droite
r
r
p
T
q
U
q
T
W
U
V
p
V
W
(a)
(b)
q
p
r
T
U
V
W
(c)
Fig. 4.12 : Rotation droite-gauche
e3
e2
r
e2
e1
e1
a3
a1
a1
e3
a2
a3
r
(b)
a2
(a)
Fig. 4.13 : Rééquilibrage à droite
36
e1
e2
e3
l
e1
e2
a3
l
a1
e3
a1
a2
a3
(b)
a2
(a)
Fig. 4.14 : Rééquilibrage à gauche
[4]
Leonidas J. Guibas et Robert Sedgewick. « A Dichromatic Framework for Balanced Trees ».
In : 19th Annual Symposium on Foundations of Computer Science, Ann Arbor, Michigan, USA,
16-18 October 1978. 1978, p. 8–21. doi : 10.1109/SFCS.1978.3. url : http ://dx.doi.org/10.
1109/SFCS.1978.3.
[5]
Robert Sedgewick. Left-leaning Red-Black Trees. 2008. url : http ://www.cs.princeton.edu/
~rs/talks/LLRB/LLRB.pdf.
37
Chapitre 5
Algorithme de Huffman
Lectures utiles :
[5], Chap. 17, p266
Un arbre nommé Patricia, ”Practical Algorithm To Retrieve Information Coded In Alphanumeric”,
Algorithme Pratique pour Extraire des Informations Codées en Alphanumérique
https ://www.normalesup.org/~simonet/teaching/caml-prepa/index.html
V. Simonet, Caml programming
https ://www.normalesup.org/~simonet/teaching/caml-prepa/index.html
M. Zeitoun, Compression de fichiers par l’algorithme de Huffman
http ://www.labri.fr/ zeitoun/enseignement/13-14/ISN/ISN-5-5-2014.pdf
Huffman Coding : A CS2 Assignment
https ://www.cs.duke.edu/csed/poop/huff/info/
0
1
0
t
1
0
n
a
1
e
Fig. 5.1 : Un arbre de Patricia
1
Arbre Patricia
Exercice 5.1 Arbre Patricia
Proposer un type OCaml type 'a pat pour définir le type arbre Patricia.
Exercice 5.2 Extraction de sous-arbre
Écrire une fonction sub_tree qui prend en paramètres un arbre Patricia a et un mot binaire w,
et retourne le sous-arbre de a désigné par w. Si le mot binaire w ne désigne pas un sous-arbre de a,
le programme stoppe et affiche un message d’erreur.
Exercice 5.3 Recherche
Écrire une fonction read qui prend en paramètres un arbre Patricia a et un mot binaire w. Cette
fonction parcourera l’arbre a en lisant le mot w depuis la racine jusqu’à arriver sur une feuille. La
fonction retournera alors l étiquette de la feuille atteinte et la partie du mot w qui n’a pas été lue. Si
le mot w ne permet pas d’atteindre une feuille, le programme stoppe et affiche un message d’erreur.
2
Codage de Huffman
38
Exercice 5.4 Calcul des fréquences d’occurrences de caractères dans un texte
Écrire une fonction frequency qui à partir d’un texte t passé en paramètre produit une liste
triée de paires (frec,c) où frec correspond à la fréquence d’apparition du caractère c dans le
texte t. La liste L sera triée dans l’ordre croissant des fréquences.
Ainsi par exemple, soit t="tentant", la fonction produira la liste
L= [(14.28%, 'e') ; (14.28%,'a') ; (28.58%, 'n') ; (42.86%,'t')]
Exercice 5.5 Représentation d’ un code de Huffman
On considère un arbre binaire dont les feuilles contiennent des caractères et leurs fréquences
respectives, et les noeuds internes contiennent la somme des fréquences respectivement de leurs fils
gauche et de leur fils droit.
Proposer un type OCaml type 'a huff pour définir un code de Huffman.
Exercice 5.6 Construction d’un arbre binaire correspondant au codage de Huffman
On se propose de construire l’arbre binaire de codage de Huffman de la manière suivante :
1. Construire Lhuf f , la liste triée de noeuds contenant les éléments de L, dans l’ordre croissant
des fréquences respectives.
2. Répéter tant que Lhuf f a plus d’un élément .
(a) Construire un nœud n12 en fusionnant les deux nœuds, n1 et n2 de fréquences les plus
petites et en tant que fils gauche et fils droit de n12 , et de fréquence la somme des
fréquences de n1 et n2 .
(b) Supprimer n1 et n2 de la liste Lhuf f .
(c) Insérer n12 dans la liste triée Lhuf f .
3. Retourner le noeud, unique élément de Lhuf f , qui définit l’arbre binaire du codage de Huffman.
Exercice 5.7 Écrire une fonction huffman qui prend en paramètre un texte t et construit l’arbre de
codage de Huffman
Exercice 5.8 Écrire une fonction encode qui prend en paramètre l’arbre binaire a, représentant un
codage de Huffman, et un mot w, une chaîne de caractères, et renvoie wbin, une chaîne binaire
correspondant à l’encodage de w selon a.
Exercice 5.9 Écrire une fonction decode qui prend en paramètre l’arbre binaire a, représentant un
codage de Huffman, et un mot binaire wbin, et renvoie w, la chaîne correspondant au mot w codé
selon a.
Bibliographie
[1]
D. Beauquier, J. Berstel et Ph. Chrétienne. Éléments d’Algorithmique. Masson, 1992. url :
http ://www-igm.univ-mlv.fr/~berstel/Elements/Elements.html.
39
Téléchargement