Sie sind auf Seite 1von 225

Programmation et Algorithmique

INF 421, Ecole Polytechnique

Philippe Baptiste et Olivier Bournez


22 juillet 2009

Avant-propos
Ce polycopi est utilis pour le cours INF 421 intitul Les bases de la programmation et de e e e lalgorithmique. Ce cours fait suite au cours INF 311 Introduction a linformatique et prc`de le ` e e cours INF 431 intitul Fondements de linformatique. Lobjectif du cours est triple : (1) programe mer en java, (2) ma triser les bases de lalgorithmique sur des structures dynamiques (listes, arbres) et (3) introduire quelques notions fondamentales dinformatique comme les expressions rguli`res, les automates et lanalyse syntaxique. e e Ce polycopi correspond ` quelques corrections, modications et complments au polycopi e a e e de Philippe Baptiste et Luc Maranget, qui lui mme fait suite au polycopi rdig par Jean e e e e Berstel et Jean-Eric Pin. Le polycopi de Philippe Baptiste et Luc Maranget a t rdig pour e ee e e prendre en compte les changements dans lorganisation des cours dinformatique de lEcole, en conservant de nombreux passages de J. Berstel et J.-E. Pin (en particulier pour les chapitres relatifs aux arbres). Nous remercions nos coll`gues de lEcole Polytechnique, et plus particuli`rement ceux qui ont e e dune mani`re ou dune autre contribu au succ`s du cours INF 421 : Mathieu Boespug, Philippe e e e Chassignet, Mathieu Cluzeau, Thomas Heide Clausen, Robert Cori, Xavier Dahan, Olivier Devillers, Germain Faure, Jean-Christophe Fillitre, Thomas Houtmann, Philippe Jacquet, Fabien a Laguillaumie, Fabrice Le Fessant, Luc Maranget, Laurent Mauborgne, David Monniaux, Syl vain Pradalier, Alejandro Ribes, Dominique Rossin, Eric Schost, Nicolas Sendrier, Jean-Jacques Lvy, Franois Morain, Laurent Viennot et Axelle Ziegler. Merci aussi ` Christoph Durr du LIX, e c a qui a magniquement illustr la page de couverture 1 de ce polycopi. e e Les auteurs peuvent tre contacts par courrier lectronique aux adresses suivantes : e e e Philippe.Baptiste@polytechnique.fr Olivier.Bournez@polytechnique.fr On peut aussi consulter la version html de ce polycopi ainsi que les pages des travaux dirigs e e sur le site http ://www.enseignement.polytechnique.fr/informatique/inf421

Le 421 est un jeu de bar qui se joue au comptoir avec des ds. e

Table des mati`res e


I Listes 1 Structure dynamique, structure squentielle . . . . . . . . . . . . . . . . . . . . . e 2 Listes cha ees, rvisions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . n e 3 Complment : programmation objet . . . . . . . . . . . . . . . . . . . . . . . . . . e 7 7 10 23 29 29 31 36 45 53 54 59 65 71 73 73 76 85 89 89 91 95 100 104 110

II Complexit des algorithmes. Applications. e 1 Complexit dalgorithmes et complexit de probl`mes e e e 2 Complexits asymptotiques . . . . . . . . . . . . . . . e 3 Tri des listes . . . . . . . . . . . . . . . . . . . . . . . 4 Listes boucles, Calcul de lenveloppe convexe . . . . e III Piles et les ` 1 A quoi ca sert ? . . . . . . . . . . . . . . 2 Implmentation des piles . . . . . . . . . e 3 Implmentation des les . . . . . . . . . e 4 Type abstrait, choix de limplmentation e

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

IV Associations Tables de hachage 1 Statistique des mots . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Table de hachage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 Choix des fonctions de hachage . . . . . . . . . . . . . . . . . . . . . . . . . . . . V Arbres 1 Dnitions . . . . . . . . . . . . . . . e 2 Union-Find, ou gestion des partitions 3 Arbres binaires . . . . . . . . . . . . 4 Arbres de syntaxe abstraite . . . . . 5 Files de priorit . . . . . . . . . . . . e 6 Codage de Human . . . . . . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

VI Arbres binaires 121 1 Implantation des arbres binaires . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121 2 Arbres binaires de recherche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125 3 Arbres quilibrs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132 e e VII Expressions rguli`res e e 1 Langages rguliers . . . . . . . . . . . . . . . . e 2 Notations supplmentaires . . . . . . . . . . . e 3 Programmation avec les expressions rguli`res e e 4 Implmentation des expressions rguli`res . . e e e 5 143 143 146 148 151

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

` TABLE DES MATIERES 5 Une autre approche du ltrage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157

VIII Les 1 2 3 4 5 6

automates Pourquoi tudier les automates . . . . . . . . . e Rappel : alphabets, mots, langages et probl`mes e Automates nis dterministes . . . . . . . . . . e Automates nis non-dterministes . . . . . . . . e Automates nis et expressions rguli`res . . . . e e Un peu de Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

163 163 163 164 167 172 175 179 179 183 186 201 205 213 219 221 221

A Morceaux de Java 1 Un langage plutt classe . . . . o 2 Obscur objet . . . . . . . . . . . 3 Constructions de base . . . . . . 4 Exceptions . . . . . . . . . . . . 5 Entres-sorties . . . . . . . . . . e 6 Quelques classes de biblioth`que e 7 Pi`ges et astuces . . . . . . . . . e Rfrences ee Index

Chapitre I

Listes
1 Structure dynamique, structure squentielle e

Les structures de donnes en algorithmique sont souvent complexes et de taille variable. e Elles sont souvent aussi dynamiques, au sens o` elles voluent, en forme et en taille, en cours u e dexcution dun programme. e Lobjet de ce chapitre est dintroduire la structure de liste. Les listes sont lexemple le plus simple dune telle structure dynamique. Le qualitatif de dynamique renvoie ` la faon dont la mmoire ncessaire est alloue, et a c e e e soppose ` la notion de structure statique. La mmoire peut en eet tre alloue dynamiquement a e e e lors de lexcution du programme, ou statiquement avant lexcution du programme. e e Lallocation statique suppose que la taille de mmoire ` allouer (cest-a-dire ` demander e a a au syst`me dexploitation) puisse tre dtermine au moment de la compilation. Cette taille est e e e e range dans le chier excutable produit par le compilateur et cest le syst`me dexploitation qui e e e alloue la mmoire correspondante au moment de lancer le programme. En Pascal par exemple, la e taille des tableaux globaux est donne par des constantes, de sorte les tableaux globaux peuvent e tre et sont allous statiquement. e e program ; var t : array [1.100] of integer ; begin . . . end. Lallocation dynamique de la mmoire existe aussi en Pascal (il existe une fonction new), mais e elle est plus dlicate quen Java, car en Pascal, comme dans beaucoup de langages, le proe grammeur doit rendre explicitement les cellules de mmoire dont il na plus besoin au syst`me e e dexploitation. En revanche, le syst`me dexcution de Java est assez malin pour raliser cette opration e e e e tout seul, ce qui permet de privilgier lallocation dynamique de la mmoire, bien plus souple e e que lallocation statique. En Java toutes les structures de donnes sont alloues dynamiquement gnralement par e e e e new, mais les cha nes sont galement alloues dynamiquement. Il nen reste pas moins que les e e tableaux en Java manquent encore un peu de souplesse, puisque leur taille est xe au moment e de la cration. Ainsi, un tableau nest pas tr`s commode pour stocker une suite de points entrs e e e a ` la souris et termine par un double clic. Il faut dabord allouer un tableau de points ` une taille e a par dfaut N suppose susante et ensuite grer le cas o` lutilisateur entre plus de N points, e e e u par exemple en allouant un nouveau tableau plus grand dans lequel on recopie les points dj` ea 7

CHAPITRE I. LISTES

stocks. Il est bien plus commode dorganiser ces points comme une suite de petits blocs de e mmoire, un bloc contenant un point et un lien vers le bloc suivant. On alloue alors la mmoire e e proportionnellement au nombre de points stocks sans se poser de questions. Nous venons de e dcouvrir le principe de la liste (simplement cha ee). Une liste L E de E est : e n (1) la liste vide, (2) ou une paire (une cellule) qui contient un lment E et la liste des lments suivants. ee ee Il sagit dune dnition par induction, dans le cas 2 le mot liste appara ` nouveau. On e t a peut, sans vraiment tout expliquer, dire que lensemble L E est dcrit par lquation suivante. e e L E = { } (E L E ) O` est la liste vide. u On notera que la dnition thorique des listes ne fait plus usage du mot lien. Mais d`s que e e e lon implmente les listes, le lien revient ncessairement. En Java, une liste est une rfrence, e e ee qui est : (1) soit la rfrence null qui ne pointe nulle part et reprsente la liste vide, ee e (2) ou bien une rfrence qui pointe vers une cellule contenant un lment et une liste. ee ee Autrement dit voici une dnition possible des cellules de liste de points en Java. e class PointList { Point val ; PointList next ; // Llment e e // La suite

PointList (Point val, PointList next) { this.val = val ; this.next = next ; } } La cellule de liste est donc un objet de la classe PointList et une valeur de type PointList est bien une rfrence, puisque les valeurs des objets en Java sont des rfrences. On peut comprendre ee ee un aspect de lorganisation des listes ` laide de dessins qui dcrivent une abstraction de ltat de a e e la mmoire, cest-`-dire une simplication de cet tat, sans les dtails techniques. Par exemple, e a e e on suppose donns deux points p1 et p2 , et on produit les listes : e List ps = new PointList (p1 , null) ; List qs = new PointList (p2 , ps) ; Alors, ltat nal de la mmoire est schmatis ainsi : e e e e qs p2 ps p1

Etant entendu que, dans ce type de schma, les `ches reprsentent les rfrences, la barre e e e ee diagonale null , les bo tes nommes les variables, et les bo e tes anonymes les cellules mmoire e alloues par new (voir A.3.1). e Il semble que ce quest une liste dans la vie de tous les jours est clair : une liste est une squence dlments. Par exemple la liste des admis ` un concours quelconque, ou la liste de e ee a courses que lon emporte au supermarch pour ne rien oublier. Lintuition est juste, elle fait e en particulier appara (surtout dans le premier exemple) que lordre des lments dune liste tre ee importe. Mais mons nous un peu, car la liste de linformatique est tr`s contrainte. En eet, e e les oprations lmentaires possibles sur une liste sont peu nombreuses : e ee

1. STRUCTURE DYNAMIQUE, STRUCTURE SEQUENTIELLE (1) Tester si une liste est la liste vide ou pas ( (a) extraire le premier lment e (.val), ee (b) ou bien extraire la liste qui suit (.next). Pour construite les listes, il ny a que deux oprations, e (1) la liste vide existe ( null ), (2) on peut ajouter un nouvel lment e en tte dune liste (new PointList (e, )). ee e == null),

(2) et si est (une rfrence vers) une cellule (e, ), on peut ee

Et cest tout, toutes les autres oprations doivent tre programmes ` partir des oprations e e e a e lmentaires. ee Une opration frquemment programme est le parcours de tous les lments dune liste. e e e ee Cette opration se fait idalement selon un idiome, cest-`-dire selon une tournure frquemment e e a e employe qui signale lintention du programmeur, ici une boucle for . e // ps est une PointList for (PointList qs = ps ; qs != null ; qs = qs.next ) { // Traiter llment qs.val e e } Les programmeurs Java emploient normalement une telle boucle pour signaler que leur code ralise un simple parcours de liste. D`s lors, la lecture de leur code en est facilite. Notons que e e e rien, ne nous empche demployer un autre type de boucle, par exemple e PointList qs = ps ; while (qs != null) { // Traiter llment qs.val e e qs = qs.next ; } La liste simplement cha ee est une structure squentielle. Une structure squentielle ren e e groupe des lments en squence et lacc`s ` un lment donn ne peut se faire quen parcourant ee e e a ee e la structure, jusqu` trouver llment cherch. Cette technique dacc`s squentiel soppose ` a ee e e e a lacc`s direct. Un exemple simple de structure de donnes qui ore lacc`s direct est le tableau. e e e La distinction entre squentiel et direct se retrouve dans des dispositifs matriels : une bande e e magntique nore quun acc`s squentiel puisquil faut drouler toute la bande pour en lire la e e e e n ; en revanche la mmoire de lordinateur ore lacc`s direct. En eet, on peut voir la mmoire e e e comme un grand tableau (doctets) dont les indices sont appels adresses. Lacc`s au contenu e e dune case se fait simplement en donnant son adresse. En fait le tableau est une abstraction de la mmoire de lordinateur. e Exercice 1 Donner deux autres exemples de dispositifs matriels orant acc`s squentiel et e e e acc`s direct. e Solution. Lutilisateur qui entre un texte au clavier nore quun acc`s squentiel ` un proe e a gramme qui lit ce quil frappe. Un disque dur peut tre considr comme orant lacc`s direct, e ee e le dispositif dadressage des donnes est juste un peu plus compliqu que celui de la mmoire. e e e Les donnes sont rparties en cylindres qui correspondent ` des anneaux sur le disque, lacc`s ` e e a e a un cylindre donn se fait par un dplacement radial de la tte de lecture. Vu du programmeur e e e il existe donc des chiers ` acc`s uniquement squentiel (lentre standard si elle correspond au a e e e clavier) et des chiers ` acc`s direct (lentre standard si elle correspond ` un chier, par une a e e a redirection <chier ).

10

CHAPITRE I. LISTES

En rsum la liste est une structure dynamique, parce que la mmoire est alloue petit ` e e e e a petit directement en fonction des besoins. La liste peut tre dnie inductivement, ce qui rend la e e programmation rcursive assez naturelle (inductif et rcursif sont pour nous informaticiens des e e synonymes). Enn, par nature, la liste est une structure de donnes squentielle, et le parcours e e de la structure est privilgi par rapport ` lacc`s ` un lment arbitraire. e e a e a ee

Listes cha ees, rvisions n e

Les listes cha ees ont dj` t vues dans le cours prcdent. Nous commenons donc par n ea ee e e c quelques rvisions. Attention, nous protons de ce que les listes ont dj` t vues en cours e eaee pour systmatiser les techniques de programmation sur les listes. Il y a sans doute un vrai prot e a ` lire cette section. Si vous en doutez, essayez tout de suite les exercices de la section 2.4.

2.1

Oprations lmentaires sur les listes e ee

Comme rvision nous montrons comment raliser les ensembles (dentiers) ` partir des listes e e a (dentiers) : dans cet exercice, un ensemble sera reprsent par une liste dont tous les lments e e ee sont deux ` deux distincts. Nous ne spcions aucune autre contrainte, en particulier aucun a e ordre des lments dune liste nest impos. ee e Voici la dnition des cellules de liste dentiers. e class List { int val ; List next ; // Llment e e // La suite

List (int val, List next) { this.val = val ; this.next = next ; } } Pour la programmation, un premier point tr`s important ` prendre en compte est que null e a est un ensemble valable. Il est alors naturel dcrire des mthodes statiques, car null qui ne e e poss`de aucune mthode est une valeur lgitime pour une variable de type List . e e e Commenons par calculer le cardinal den ensemble. Compte tenu de ce que les lments des c ee listes sont deux ` deux distincts, il sut de calculer la longueur dune liste. Employons donc la a boucle idiomatique static int card( List xs) { int r = 0 ; for ( ; xs != null ; xs = xs.next) r++ ; // pour r = r+1 ; return r ; } Ce premier code fait appara quil nest pas toujours utile de rserver une variable locale pour tre e la boucle idiomatique. Ecrire xs = xs.next ne fait que changer la valeur de la variable locale xs (qui appartient en propre ` lappel de mthode) et na pas dimpact sur la liste elle-mme. a e e Ainsi crire le code suivant ne pose pas de probl`me particulier. e e List xs = new List (1, new List (2, new List (3, new List (4,null)))) l int size = card(xs) ; ` A la n du code, la variable xs existe toujours et rfrence toujours la premi`re cellule de la ee e liste {1, 2, 3, 4}. En fait, cette variable xs na rien ` voir avec le param`tre homonyme de la a e mthode card et heureusement. e

2. LISTES CHA EES, REVISIONS IN

11

Poursuivons en achant les ensembles, ou, plus exactement, par fabriquer une reprsentation e achable des ensembles sous forme de cha ne. Voici un premier essai dcriture dune mthode e e statique toString dans la classe List avec la boucle idiomatique. static String toString( List xs) { String r = "{" ; for ( ; xs != null ; xs = xs.next ) r = r + xs.val + ", " ; r = r + "}" ; return r ; } Le code est critiquable : Lachage est laid, il y a une virgule en trop ` la n. Par exemple, pour la liste des entiers a 1 et 3, on obtient "{1, 3, }". Le cot de production de la cha est quadratique en la longueur de la liste. En eet on u ne admettra que la concatnation de deux cha e nes (par +) cote en proportion de la somme des u longueurs des cha nes concatnes, car il faut allouer une nouvelle cha pour y recopier e e ne les caract`res des cha e nes concatnes. Pour une liste xs de longueur n on proc`de donc e e e a ` n concatnations de cha e nes, de tailles supposes rguli`rement croissantes ce qui m`ne e e e e au nal ` un cot quadratique. a u k + 2 k + n k = n(n + 1) k 2

Pour remdier au premier probl`me on peut distinguer le cas du premier lment de la liste. Pour e e ee remdier au second probl`me on peut utiliser un objet StringBuilder de la biblioth`que Java e e e (voir A.6.1.4). Les objets StringBuilder sont des cha nes dynamiques, dont on peut changer la taille. En particulier, ils poss`dent une mthode append(String str) qui permet de leur e e ajouter une cha str ` la n, pour un cot proportionnel ` la longueur de str. Bref on a : ne a u a static String toString( List xs) { a StringBuilder r = new StringBuilder () ; // Un StringBuilder ` nous r.append("{") ; i f (xs != null) { e e r.append(xs.val) ; // ajouter lcriture dcimale du premier entier xs = xs.next ; // et celles des suivants prfixes par ", " e e for ( ; xs != null ; xs = xs.next ) r.append(", " + xs.val) ; } r.append("}") ; // Renvoyer la cha^ne contenue dans le StringBuilder return r.toString() ; } Apr`s cet chauement, crivons ensuite la mthode mem qui teste lappartenance dun entier e e e e a ` un ensemble. static boolean mem(int x, List xs) { for ( ; xs != null ; xs = xs.next) { i f (x == xs.val) return true ; } return false ; }

12

CHAPITRE I. LISTES

On emploie la boucle idiomatique, an de parcourir la liste et de retourner de la mthode mem e d`s quun entier gal ` x est trouv. Comme on le sait certainement dj` une criture rcursive e e a e ea e e est galement possible. e static boolean mem(int x, List xs) { i f (xs == null) { return false ; } else { return (x == xs.val) || mem(x, xs.next) ; } } La version rcursive provient directement de la dnition inductive des listes, elle tient en deux e e quations : e M (x, ) = faux M (x, (x , X )) = (x = x ) M (x, X ) Ces deux quations sont le support dune preuve par induction structurelle vidente de la core e rection de mem. Alors, que choisir ? De faon gnrale le code itratif (le premier, avec une boucle) est plus c e e e ecace que le code rcursif (le second), ne serait-ce que parce que les appels de mthode sont e e assez coteux. Mais le code rcursif rsulte dune analyse systmatique de la structure de donnes u e e e e inductive, et il a bien plus de chances dtre juste du premier coup. Ici, dans un cas aussi simple, e le code itratif est prfrable (en Java qui favorise ce style). e ee

2.2

Programmation s re, style dit fonctionnel u

Nous envisageons maintenant des mthodes qui fabriquent de nouveaux ensembles. Come menons par la mthode add qui ajoute un lment ` un ensemble. Le point remarquable est c e ee a quun ensemble contient au plus une fois un lment donn. Une fois disponible la mthode mem, ee e e crire add est tr`s facile. e e static List add(int x, List xs) { i f (mem(x, xs)) { return xs ; } else { return new List (x, xs) ; } } Attaquons ensuite la mthode remove qui enl`ve un lment x dun ensemble X. Nous e e ee choisissons de suivre le principe des structures de donnes dites fonctionnelles ou non-mutables e ou parfois mme persistantes. Selon ce principe, on ne modie jamais le contenu des cellules de e liste. Cela revient ` considrer les listes comme dnies inductivement par L E = {} (E a e e L E ), et il est important dadmettre d`s maintenant que cette approche rend la programmation e bien plus sre. u Pour crire remove, raisonnons inductivement : dans un ensemble vide, il ny a rien ` enlever ; e a tandis que dans un ensemble (une liste) X = (x , X ) on distingue deux cas : Llment x ` enlever est gal ` x et alors X moins x est X , car X est une liste dlments ee a e a ee deux ` deux distincts et donc X ne contient pas x. a Sinon, il faut enlever x de X et ajouter x ` la liste obtenue, dont on sait quelle ne peut a pas contenir x .

2. LISTES CHA EES, REVISIONS IN Soit en quations : e R(x, ) = R(x, (x, X )) = X R(x, (x , X )) = (x , R(x, X )) Et en Java : static List remove(int x, List xs) { i f (xs == null) { return null ; } else i f (x == xs.val) { return xs.next ; } else { return new List (xs.val, remove(x, xs.next)) ; } } avec x = x

13

En ce qui concerne ltat mmoire, remove(x,xs) renvoie une nouvelle liste (il y a des new), e e dont le dbut est une copie du dbut de xs, jusqu` trouver llment x. Plus prcisment, si e e a ee e e nous crivons e List xs = new List (1, new List (2, new List (3, new List (4, null)))) ; List ys = remove(3, xs) ; Alors on a les structures suivantes en mmoire e xs ys 1 1 2 2 3 4

(Les cellules alloues par remove sont grises.) On constate que la partie de la liste xs qui prc`de e e e e llment supprim est recopie, et que la partie qui suit llment supprime est partage entre ee e e ee e e les deux listes. Dans le style fonctionnel on en vient toujours ` copier les parties des structures a qui sont changes. e Au passage, si on crit plutt e o List xs = new List (1, new List (2, new List (3, new List (4, null)))) ; xs = remove(3, xs) ; Alors on obtient 1 xs 1 2 2 3 4

Cest-`-dire exactement la mme chose, sauf que la variable xs pointe maintenant vers la nouvelle a e liste et quil ny a plus de rfrence vers la premi`re cellule de lancienne liste. Il en rsulte ee e e que la mmoire occupe par les trois premi`res cellules de lancienne liste ne peut plus tre e e e e accde par le programme. Ces cellules sont devenues les miettes et le gestionnaire de mmoire e e e de lenvironnement dexcution de Java peut les rcuprer. Le ramassage des miettes (garbage e e e collection) est automatique en Java, et cest un des points forts du langage. Ecrivons maintenant remove avec des boucles. Voici un premier essai : parcourir la liste xs en la recopiant en vitant de copier tout lment gal ` x. e ee e a static List remove(int x, List xs) { List r = null ;

14 Fig. 1 Les quatre itrations de remove e xs 1 1 2 r 3 4 xs

CHAPITRE I. LISTES

1 1

2 2

3 r

xs 1 1 2 2 3 4 4 r


1 1 2 2

xs 3 r 4

for ( ; xs != null ; xs = xs.next ) { i f (x != xs.val) r = new List (xs.val, r) ; } return r ; } Et ici appara une dirence, alors que la version rcursive de remove prserve lordre des t e e e lments de la liste, la version itrative inverse cet ordre. Pour sen convaincre, on peut remarquer ee e ee que lorsque quune nouvelle cellule new List(xs.val, r) est construite, llment xs.val est ajout au dbut de la liste r, tandis quil est extrait de la n de la liste des lments dj` e e ee ea parcourus. On peut aussi schmatiser ltat mmoire sur lexemple dj` utilis. Avant la boucle e e e ea e on a x 3 xs 1 r La gure 1 schmatise ltat de la mmoire ` la n de chacune des quatre itrations, la cellule e e e a e nouvelle tant grise. Et enn voici le bilan de ltat mmoire, pour le programme complet. e e e e List xs = new List (1, new List (2, new List (3, new List (4, null)))) ; List ys = remove(3, xs) ; xs 1 1 2 2 3 4 4 ys 2 3 4

2. LISTES CHA EES, REVISIONS IN

15

Linversion na pas dimportance ici, puisque les ensembles sont des listes dlments deux ` ee a deux distincts sans autre contrainte, le code itratif convient. Il nen y irait pas de mme si les e e listes taient ordonnes. e e Il est possible de procder itrativement et de conserver lordre des lments. Mais cest e e ee un peu plus dicile. On emploie une technique de programmation que faute de mieux nous appellerons initialisation dire. Jusquici nous avons toujours appel le constructeur des listes e e e a ` deux arguments, ce qui nous garantit une bonne initialisation des champs val et next des cellules de liste. Nous nous donnons un nouveau constructeur List ( int val) qui initialise seulement le champ val.1 Le champ next sera aect une seule fois plus tard, ce qui revient e moralement ` une initialisation. a List (int val) { this.val = val ; } Pour illustrer linitialisation dire, nous commenons par un exemple plus facile. ee c Exercice 2 Copier une liste en respectant lordre de ses lments, sans crire de mthode ee e e rcursive. e Solution. Lide revient ` parcourir la liste xs en copiant ses cellules et en dlgant linitialisae a ee tion du champ next ` litration suivante. Il y a un petit dcalage ` grer au dbut et ` la n a e e a e e a du parcours. static List copy( List xs) { i f (xs == null) return null ; // Ici la copie poss`de au moins une cellule, que voici e List r = new List (xs.val) ; // last pointe sur la derni`re cellule de la liste r e List last = r ; // Parcourir la suite de xs for ( xs = xs.next ; xs != null ; xs = xs.next ) { // initialiser last.next ` une nouvelle cellule a last.next = new List (xs.val) ; e e / qui devient donc la derni`re cellule du rsultat / last = last.next ; } // initialiser le next de la toute derni`re cellule ` une liste vide e a last.next = null ; // Inutile, mais cest plus propre return r ; } On observe que le code construit une liste sur laquelle il maintient deux rfrences, r pointe sur la ee premi`re cellule, tandis que last pointe sur la derni`re cellule sauf tr`s bri`vement au niveau e e e e du commentaire /**. . . **/, o` last pointe sur lavant-derni`re cellule. Plus prcisment, la u e e e situation en rgime permanent avant la ligne qui prc`de le commentaire /**. . . **/ est e e e la suivante : xs 1 1 r
1

2 2 ? last

En fait, un champ de type objet non-initialis explicitement contient la valeur par dfaut null, voir A.3.3. e e

16

CHAPITRE I. LISTES

Et apr`s ajout dune nouvelle cellule par last.next = new List(xs.val). e xs 1 1 r last 2 2 3 3 ? 4 5

(La cellule qui vient dtre alloue est grise). Et enn apr`s excution de laectation last == e e e e e last.next. xs 1 1 r 2 2 last 3 3 ? 4 5

On conoit quavec un tel dispositif il est facile dajouter de nouvelles cellules de liste ` la n du c a rsultat et possible de rendre le rsultat (avec r qui pointe sur la premi`re cellule). On observe e e e le cas particulier trait au dbut, il rsulte de la ncessit didentier le cas o` il ne faut pas e e e e e u construire la premi`re cellule du rsultat. e e Enn, la mthode itrative construit exactement la mme liste que la mthode rcursive, e e e e e mme si les cellules de listes sont alloues en ordre inverse. En eet la mthode rcursive ale e e e loue la cellule apr`s que la valeur du champ next est connue, tandis que la mthode itrative e e e lalloue avant. La gure 2 donne la nouvelle version itrative de remove, crite selon le principe de linie e tialisation dire. Par rapport ` la copie de liste, le principal changement est la possibilit ee a e darrt de la copie ` lintrieur de la boucle. En eet, lorsque llment ` supprimer est identi e a e ee a e (x == xs.val), nous connaissons la valeur ` ranger dans last.next, puisque par lhypoth`se a e xs est un ensemble , xs moins llment x est exactement xs.next, on peut donc arrter ee e la copie. On observe galement les deux cas particuliers traits au dbut, ils rsultent, comme e e e e dans la copie de liste, de la ncessit didentier les cas o` il ne faut pas construire la premi`re e e u e cellule du rsultat. Au nal le code itratif est quand mme plus complexe que le code rcursif. e e e e Par consquent, si lecacit importe peu, par exemple si les listes sont de petite taille, la e e programmation rcursive est prfrable. e ee Nous pouvons maintenant tendre les oprations lmentaires impliquant un lment et un e e ee ee ensemble et programmer les oprations ensemblistes du test dinclusion, de lunion et de la e dirence. Pour programmer le test dinclusion xs ys, il sut de tester lappartenance de e tous les lments de xs ` ys. ee a static boolean included( List xs, List ys) { for ( ; xs != null ; xs = xs.next ) i f (!mem(xs.val, ys)) return false ; return true ; }

2. LISTES CHA EES, REVISIONS IN Fig. 2 Enlever un lment selon la technique de linitialisation dire ee ee static List remove(int x, List xs) { /* Deux cas particuliers */ i f (xs == null) return null ; /* Ici xs != null, donc xs.val existe */ i f (x == xs.val) return xs.next ; /* Ici la premi`re cellule du rsultat existe et contient xs.val */ e e List r = new List (xs.val) ; List last = r ; /* Cas gnral : copier xs (dans r), jusqu` trouver x */ e e a for ( xs = xs.next ; xs != null ; xs = xs.next ) { i f (x == xs.val) { // initialiser last.next ` xs moins xs.val (c-a-d. xs.next) a last.next = xs.next ; return r ; } // initialiser last.next ` une nouvelle cellule a last.next = new List (xs.val) ; last = last.next ; } // initialiser last.next a une liste vide ` last.next = null ; // Inutile mais cest plus propre. return r ; }

17

Exercice 3 Programmer lunion et la dirence ensembliste. e Solution. Pour lunion, il sut dajouter tous les lment dun ensemble ` lautre. ee a static List union( List xs, List ys) { List r = ys ; for ( ; xs != null ; xs = xs.next ) r = add(xs.val, r) ; return r ; } On aurait mme pu se passer de la variable r pour crire ys = add(xs.val, ys) et puis e e return ys. Mais la clart en aurait souert. e Pour la dirence des ensembles, lg`rement plus dlicate en raison de la non-commutativit, e e e e e on enl`ve de xs tous les lments de ys. e ee // Renvoie xs \ ys static List diff( List xs, List ys) { List r = xs ; for ( ; ys != null ; ys = ys.next ) r = remove(ys.val, r) ; return r ; }

18

CHAPITRE I. LISTES

2.3

Programmation dangereuse, structures mutables

Les mthodes remove de la section prcdente copient tout ou partie des cellules de la liste e e e passe en argument. On peut, par exemple dans un souci (souvent mal venu, disons le tout de e suite) dconomie de la mmoire, chercher ` viter ces copies. Cest parfaitement possible, ` e e a e a condition de sautoriser les aectations des champs des cellules de liste. On parle alors de structure de donnes mutable ou imprative. Ainsi, dans le remove rcursif on remplace lallocation e e e dune nouvelle cellule par la modication de lancienne cellule, et on obtient la mthode nremove e destructive ou en place suivante static List nremove(int x, List xs) { i f (xs == null) { return null ; } else i f (x == xs.val) { return xs.next ; } else { xs.next = nremove(x, xs.next) ; return xs ; } } On peut adapter la seconde version itrative de remove (gure 2) selon le mme esprit, les e e rfrences r et last pointant maintenant, non plus sur une copie de la liste passe en argument, ee e mais sur cette liste elle-mme. e static List nremove(int x, List xs) { /* Deux cas particuliers */ i f (xs == null) return null ; i f (x == xs.val) return xs.next ; // Ici la premi`re cellule du rsultat existe et contient xs.val, e e List r = xs ; List last = r ; /* Cas gnral : chercher x dans xs pour enlever sa cellule */ e e for ( xs = xs.next ; xs != null ; xs = xs.next ) { / Ici, on a last.next == xs / i f (x == xs.val) { // Enlever la cellule pointe par xs e last.next = xs.next ; return r ; } last.next = xs ; // Affectation inutile last = last.next ; / Exceptionnellement, on a xs == last / } last.next = null ; // Affectation inutile return r ; } On note que la version itrative comporte des aectations de last.next inutiles. En eet, ` e a lintrieur de la boucle les rfrences xs et last.next sont toujours gales, car last pointe e ee e toujours sur la cellule qui prc`de xs dans la liste passe en argument, sauf tr`s bri`vement au e e e e e niveau du second commentaire /**. . . **/. En rgime permanent, la situation au dbut dune e e itration de boucle (avant le premier commentaire /**. . . **/) est la suivante e

2. LISTES CHA EES, REVISIONS IN

19

x 3 r

last 1

xs 2 3 4

Et en n ditration, apr`s le second commentaire /**. . . **/, on a e e x 3 r last 1 xs 2 3 4

Au dbut de litration suivante on a e e x 3 r 1 last 2 xs 3 4

La recherche de xs.val == x est termine, et le champ next de la cellule last qui prc`de la e e e cellule xs est aect. e x 3 r 1 last 2 xs 3 4

Puis la liste r est renvoye. e Au nal, leet de nremove( int x, List xs) sapparente ` un court-circuit de la cellule a qui contient x. Ainsi excuter e List xs = new List (1, new List (2, new List (3, new List (4, null)))) ; List ys = nremove(3, xs) ; produit ltat mmoire e e xs 1 ys Un probl`me majeur survient donc : lensemble xs est modi ! On comprend donc pourquoi e e nremove est dite destructive. On peut se dire que cela nest pas si grave et que lon saura en tenir compte. Mais cest assez vain, car si on supprime le premier lment de lensemble xs, le ee comportement est dirent e List xs = new List (1, new List (2, new List (3, new List (4, null)))) ; List ys = nremove(1, xs) ; Alors on obtient xs 1 ys 2 3 4 2 3 4

20

CHAPITRE I. LISTES

Et on observe qualors lensemble xs na pas chang. e En conclusion, la programmation des structures mutables est tellement dlicate quil est e souvent plus raisonnable de sabstenir de lemployer. Comme toujours en programmation, la maxime ci-dessus soure des exceptions, dans les quelques cas o` lon poss`de une ma u e trise compl`te des structures de donnes, et en particulier, quand on est absolument sr que les e e u structures modies ne sont connues daucune autre partie du programme. e Un bon exemple de ce type dexception est la technique dinitialisation dire employe dans ee e la seconde mthode remove itrative de la gure 2. En eet, le champ next est aect une seule e e e fois, et seulement sur des cellules fra ches, alloues par la mthode remove elle-mme, et qui ne e e e sont accessible qu` partir des variables r et last elles-mmes locales ` remove. Par consquent, a e a e les modications apportes ` certaines cellules sont invisibles de lextrieur de remove. e a e

2.4

Contrle de la rvision o e

Nous avons divis les techniques de programmation sur les listes selon deux fois deux e catgories. Les listes peuvent tre fonctionnelles (ou non-mutables) ou mutables (ou impratives), e e e et la programmation peut tre rcursive ou itrative. e e e An dtre bien sr davoir tout compris, programmons une opration classique sur les listes e u e des quatre faons possibles. Concatner deux listes signie, comme pour les cha c e nes, ajouter les lments dune liste ` la n de lautre. Nommons append la mthode qui concat`ne deux listes. ee a e e La programmation rcursive non-mutable rsulte dune analyse simple de la structure inductive e e de liste. static List append( List xs, List ys) { i f (xs == null) { return ys ; // A(, Y ) = Y } else { return new List (xs.val, append(xs.next, ys)) ; // A((x, X), Y ) = (x, A(X, Y )) } } Nommons nappend la version mutable de la concatnation. Lcriture rcursive de nappend ` e e e a partir de la version fonctionnelle est mcanique. Il sut de remplacer les allocations de cellules e par des modications. static List nappend( List xs, List ys) { i f (xs == null) { return ys ; } else { xs.next = nappend(xs.next, ys)) ; return xs ; } } Pour la programmation itrative non-mutable, on a recours a linitialisation dire. e ` ee static List append( List xs, List ys) { i f (xs == null) return ys ; List r = new List (x.val) ; List last = r ; for ( xs = xs.next ; xs != null ; xs = xs.next ) { last.next = new List (xs.val) ; last = last.next ; } last.next = ys ; return r ;

2. LISTES CHA EES, REVISIONS IN }

21

Pour la programmation mutable et itrative, on supprime les allocations de la version none mutable et on remplace les initialisations dires par des aectations des champs next de la ee liste passe comme premier argument. On nexplicite que la derni`re aectation de last.next e e qui est la seule qui change quelque chose. static List nappend( List xs, List ys) { i f (xs == null) return ys ; List r = xs ; List last = r ; for ( xs = xs.next ; xs != null ; xs = xs.next ) { last = last.next ; } last.next = ys ; return r ; } Voici un autre codage plus concis de la mme mthode, qui met en valeur la recherche de la e e derni`re cellule de la liste xs. e static List nappend( List xs, List ys) { i f (xs == null) return ys ; List last = xs ; for ( ; last.next != null ; last = last.next ) ; // idiome : boucle qui ne fait rien last.next = ys ; return xs ; } Enn on constate que le cot de la concatnation est de lordre de n oprations lmentaires, u e e ee o` n est la longueur de la premi`re liste. u e Exercice 4 Que fait le programme suivant ? List xs = new List (1, new List (2, new List (3, null))) ; List ys = nappend(xs, xs) ; Solution. Le programme aecte la rfrence xs au champ next de la derni`re cellule de la ee e liste xs. Il en rsulte une liste boucle . e e xs 1 ys 2 3

Et pour tre vraiment sr davoir compris, nous conseillons encore un exercice, dailleurs un e u peu pig. e e Exercice 5 Programmer les deux versions fonctionnelles, itrative et rcursive, de reverse la e e mthode qui inverse une liste. e Solution. Une fois nest pas coutume, la version itrative est de loin la plus naturelle. e

22 static List reverse( List xs) { List r = null ; for ( ; xs != null ; xs = xs.next ) r = new List (xs.val, r) ; return r ; }

CHAPITRE I. LISTES

En eet, en parcourant (lisant) une squence quelconque dentiers (par ex. en lisant des entiers e entrs au clavier) pour construire une liste, la liste est naturellement construite dans lordre e inverse de la lecture. De fait, les lments sont ajouts au dbut de la liste construite alors quil ee e e sont extraits de la n de la squence lue voir aussi le premier remove itratif de la section 2.2. e e Cest souvent embtant, mais ici cest utile. e Ecrire la version rcursive est un peu gratuit en Java. Mais la dmarche ne manque pas e e dintrt. Soit R la fonction qui inverse une liste et A la concatnation des listes, le premier ee e lment de la liste que lon souhaite inverser doit aller a la n de la liste inverse. Soit lquation : ee ` e e R((x, X)) = A(R(X), (x, )) On est donc tent dcrire : e e // Mauvais code, montre ce quil faut viter. e static List reverse( List xs) { i f (xs == null) { return null ; } else { return append(reverse(xs.next), new List (xs.val, null)) ; } } Ce nest pas une tr`s bonne ide, car pour inverser une liste de longueur n, il en cotera au e e u moins de lordre de n2 oprations lmentaires, correspondant aux n append eectus sur des e ee e listes de taille n 1, . . . 0. Un truc des plus classiques vient alors ` notre secours. Le truc revient ` considrer une a a e mthode auxiliaire rcursive qui prend deux arguments. Nous pourrions donner le truc directee e ment, mais prfrons faire semblant de le dduire de dmarches plus gnrales, lune thorique ee e e e e e et lautre pratique. Commenons par la thorie. Gnralisons lquation prcdente, pour toute liste Y on a c e e e e e e A(R((x, X)), Y ) = A(A(R(X), (x, )), Y ) Or, quelles que soient les listes X, Y et Z, on a A(A(X, Y ), Z) = A(X, A(Y, Z)) (la concatnation e est associative). Il vient : A(R((x, X)), Y ) = A(R(X), A((x, ), Y )) Autrement dit : A(R((x, X)), Y ) = A(R(X), (x, Y )) Posons R (X, Y ) = A(R(X), Y ), autrement dit R renvoie la concatnation de X inverse et de Y . e e Avec cette nouvelle notation, lquation prcdente scrit : e e e e R ((x, X)), Y ) = R (X, (x, Y )) Equation qui permet de calculer R rcursivement, puisque de toute vidence R (, Y ) = Y . e e

3. COMPLEMENT : PROGRAMMATION OBJET private static reverseAux( List xs, List ys) { i f (xs == null) { return ys ; } else { return reverseAux(xs.next, new List (xs.val, ys)) ; } } Par ailleurs on a R (X, ) = A(R(X), ) = R(X) et donc : static List reverse( List xs) { return reverseAux(xs, null) ; }

23

Pour lapproche pratique du truc, il faut savoir que lon peut toujours et de faon assez c simple remplacer une boucle par une rcursion. Commenons par ngliger les dtails du corps e c e e de la boucle de reverse itratif. Le corps de la boucle lit les variables xs et r puis modie r. e On rcrit donc la boucle ainsi ee for ( ; xs != null ; xs = xs.next ) r = body(xs, r) ; O` la mthode body est vidente. u e e Considrons maintenant une itration de la boucle comme une mthode loop. Cette mthode e e e e a besoin des param`tres xs et r. Si xs est null , alors il ny a pas ditration (pas dappel de e e body) et il faut passer le rsultat r courant ` la suite du code. Sinon, il faut appeler litration e a e suivante en lui passant r modi par lappel ` body. Dans ce cas, le rsultat ` passer ` la suite du e a e a a code est celui de litration suivante. On op`re ensuite un petit saut smantique en comprenant e e e passer ` la suite du code comme retourner en tant que rsultat de la mthode loop . a e e Soit : private static List loop( List xs, List r) { i f (xs == null) { return r ; } else { return loop(xs.next, body(xs, r)) ; } } La mthode loop est dite rcursive terminale car la derni`re opration eectue avant de ree e e e e tourner est un appel rcursif. Ou plus exactement lunique appel rcursif est en position dite e e terminale. Enn, dans le corps de reverse itratif, on remplace la boucle par un appel ` loop. e a static List reverse( List xs) { List r = null ; r = loop(xs, r) ; return r ; } On retrouve, apr`s nettoyage, le mme code quen suivant la dmarche thorique. De tous ces e e e e discours, il faut surtout retenir lquivalence entre boucle et rcursion terminale. e e

Complment : programmation objet e

Dans tout ce chapitre nous avons expos la structure des listes. Cest-`-dire que les proe a grammes qui se servent de la classe des listes le font directement, en pleine conscience quils

24

CHAPITRE I. LISTES

manipulent des structures de listes. Cette technique est parfaitement correcte et fconde, mais e elle ne correspond pas ` lesprit dun langage objet. Supposons par exemple que nous voulions a implmenter les ensembles dentiers. Il est alors plus conforme ` lesprit de la programmation e a objet de dnir un objet ensemble dentiers Set. Cette mise en forme objet nabolit pas e la distinction fondamentale des deux sortes de structures de donnes, mutable et non mutable. e bien au contraire.

3.1

Ensembles non mutables

Spcions donc une classe Set des ensembles. e class Set { Set() { . . . } Set add(int x) { . . . } Set remove(int x) { . . . } } Comparons la signature de mthode add des Set ` celle de la mthode add de la classe List . e a e static List add(int x, List xs) ; La nouvelle mthode add nest plus statique, pour produire un ensemble augment, on utilise e e maintenant la notation objet : par exemple s.add(1) renvoie le nouvel ensemble s { 1} alors que prcdemment on crivait List .add(1,s). On note que pour pouvoir appeler la mthode e e e e add sur tous les objets Set, il nest plus possible dencoder lensemble vide par null . Lensemble vide est maintenant un objet construit par new Set (). Bien entendu, il faut quune structure concr`te regroupe les lments des ensembles. Le plus e ee simple est dencapsuler une liste dans chaque objet Set. class Set { // Partie prive e private List xs ; private Set ( List xs) { this.xs = xs ; } // Partie visible Set () { xs = null ; } Set add(int x) { return new Set ( List .add(x,xs)) ; } Set remove(int x) { return new Set ( List .remove(xs, x)) ; } } La variable dinstance xs et le constructeur qui prend une liste en argument sont privs, de sorte e que les clients de Set ne peuvent pas manipuler la liste cache. Les clients sont les parties e du programme qui appellent les mthodes de la classe Set. e Comme le montre lexemple suivant, il importe que les listes soient traites comme des listes e non-mutables. En eet, soit le bout de programme Set Set Set Set s0 s1 s2 s3 = = = = new Set () ; s0.add(1) ; s1.add(2) ; s2.add(3) ;

Nous obtenons ltat mmoire simpli e e e s3 xs 3 s2 xs 2 s1 xs 1 s0 xs

3. COMPLEMENT : PROGRAMMATION OBJET

25

La mthode remove (page 12) ne modie pas les champs des cellules de listes, de sorte que que e le programme Set s4 = s3.remove(2) ; nentra aucune modication des ensembles s2, s1 et s0. ne s3 xs 3 3 s4 xs s2 xs 2 s1 xs 1 s0 xs

Il semble bien que ce style dencapsulage dune structure non-mutable est peu utilis en e pratique. En tout cas, la biblioth`que de Java ne propose pas de tels ensembles fonctionnels. e Les deux avantages de la technique, ` savoir une meilleure structuration, et une reprsentation a e uniforme des ensembles, y compris lensemble vide, comme des objets, ne justient sans doute pas la cellule de mmoire supplmentaire alloue systmatiquement pour chaque ensemble. Lene e e e capsulage se rencontre plus frquemment quand il sagit de protger une structure mutable. e e

3.2

Ensembles mutables

On peut aussi vouloir modier un ensemble et non pas en construire un nouveau. Il sagit dun point de vue impratif, tant donn un ensemble s, appliquer lopration add( int x) ` s, e e e e a ajoute llment x ` s. Cette ide sexprime simplement en Java, en faisant de s un objet (dune ee a e e ee a nouvelle classe Set) et de add une mthode de cet objet. On ajoute alors un lment x ` s par un appel de mthode s.add(x), et la signature de add la plus naturelle est e void add(int x) ; La nouvelle signature de add rv`le lintention imprative : s connu est modi et il ny a donc e e e e pas lieu de le renvoyer apr`s modication. e Pour programmer la nouvelle classe Set des ensembles impratifs, on a recours encore une e fois ` la technique dencapsulage. Les objets de la classe Set poss`dent une variable dinstance a e qui est une List . Mais cette fois, les mthodes de la classe Set modient la variable dinstance. e class Set { // Partie prive e private List xs ; // Partie accessible Set () { this.xs = null ; } void add(int x) { xs = new List (x, xs) ; } void remove(int x) { xs = List .nremove(x, xs) ; } public String toString() { return List .toString(xs) ; } } Le champ xs est priv, an de garantir que la classe Set est la seule ` manipuler cette liste. Cette e a prcaution autorise, mais nimpose en aucun cas, lemploi des listes mutables au prix dun risque e modr, comme dans la mthode remove qui appelle List .nremove. Le caract`re impratif des ee e e e e e ensembles Set est clairement arm et on ne sera (peut-tre) pas surpris par lachage du programme suivant :

26 Set s1 = new Set() ; Set s2 = s1 ; s1.add(1) ; s1.add(2) ; System.out.println(s2) ;

CHAPITRE I. LISTES

Le programme ache {1, 2} car s1 et s2 sont deux noms du mme ensemble impratif. e e s2 xs 2 s1 xs 1

Plus gnralement, en utilisant une structure de donne imprative, on souhaite faire proter e e e e lensemble du programme de toutes les modications ralises sur cette structure. Cet eet ici e e recherch est facilement atteint en mutant le champ priv xs des objets Set, cest la raison e e mme de lencapsulage. e Exercice 6 Enrichir les objets de la classe Set avec une mthode diff de signature : e void diff(Set s) ; // enlever les lments de s e e Aller dabord au plus simple, puis utiliser les structures de donnes mutables. Ne pas oublier de e considrer le cas s.diff(s) e Solution. On peut crire dabord e void diff(Set s) { this.xs = List .diff(this.xs, s.xs) ; } O` on a crit this .xs au lieu de xs pour bien insister. u e Compte tenu de la smantique, il semble lgitime de modier les cellules accessibles ` partir e e a de this .xs, mais pas celles accessibles ` partir de s.xs. On tente donc : a void diff(Set s) { for ( List ys = s.xs ; ys != null ; ys = ys.next) this.xs = List .nremove(ys.val, this.xs) ; } Lemploi dune nouvelle variable ys simpose, car il ne faut certainement pas modier s.xs. Plus prcisment on ne doit pas crire for ( ; s.xs != null ; s.xs = s.xs.next ). Notons au e e e passage que la dclaration private List xs ninterdit pas la modication de s.xs : le code de e diff se trouve dans la classe Set et il a plein acc`s aux champs privs de tous les objets de la e e classe Set. On vrie que lauto-appel s.diff(s) est correct, mme si cest de justesse. Supposons par e e exemple que xs pointe vers la liste (1, (2, (3, ))). Au dbut de la premi`re itration, on a e e e this .xs 1 ys Dans ces conditions, enlever ys.val (1) de la liste this .xs revient simplement ` renvoyer a this .xs.next. Et ` la n de premi`re itration, on a a e e 2 3

3. COMPLEMENT : PROGRAMMATION OBJET

27

this .xs 1 ys Les itrations suivantes sont similaires : on enl`ve toujours le premi`re lment de la liste pointe e e e ee e par this .xs, de sorte quen dbut puis n de seconde itration, on a e e this .xs 1 ys Puis au dbut et ` la n de la derni`re itration, on a e a e e this .xs 1 2 ys 3 1 2 ys this .xs 3 2 3 1 ys 2 this .xs 3 2 3

Et donc, this .xs vaut bien null en sortie de mthode diff, ouf. Tout cela est quand mme e e assez acrobatique.

28

CHAPITRE I. LISTES

Chapitre II

Complexit des algorithmes. e Applications.


Lobjet de la thorie de la complexit est de mesurer les ressources (temps, mmoire, . . .) e e e utilises par un programme, ou ncessaires ` la rsolution dun probl`me. e e a e e Ce chapitre vise ` en prciser les premi`res bases, en discutant quelques algorithmes et leur a e e ecacit. Nous nous intressons ensuite aux algorithmes de tri, prsents comme algorithmes e e e e sur les listes, et ` une application des listes au calcul de lenveloppe convexe de points. a

1
1.1

Complexit dalgorithmes et complexit de probl`mes e e e


La notion dalgorithme

La thorie de la calculabilit, ne des travaux fondateurs de Emile Post et Alan Turing, ore e e e des outils pour formaliser la notion dalgorithme de faon tr`s gnrale. c e e e Elle sintresse en fait essentiellement ` discuter les probl`mes qui sont rsolubles informae a e e tiquement : cest-`-dire ` distinguer les probl`mes dcidables (qui peuvent tre rsolus infora a e e e e matiquement) des probl`mes indcidables (qui ne peuvent avoir de solution informatique). La e e calculabilit est tr`s proche de la logique mathmatique, et de la thorie de la preuve : lexise e e e tence de probl`mes qui nadmettent pas de solution informatique est tr`s proche de lexistence e e de thor`mes vrais mais qui ne sont pas dmontrables. e e e La thorie de la complexit se focalise sur les probl`mes dcidables, et sintresse aux rese e e e e sources (temps, mmoire, etc.) ncessaires ` la rsolution de ces probl`mes. Cest typiquement ce e e a e e dont on a besoin pour mesurer lecacit des algorithmes considrs dans ce cours : on consid`re e ee e des probl`mes qui admettent une solution, mais pour lesquels on cherche une solution ecace. e Ces thories permettraient de formaliser proprement la notion dalgorithme, en compl`te e e gnralit, en faisant abstraction du langage utilis, mais cela dpasserait compl`tement lambie e e e e e tion de ce polycopi. e Dans ce polycopi, on va prendre la notion suivante dalgorithme : un algorithme A est e un programme Java qui prend en entre une donne d et produit en sortie un rsultat A(d). e e e Cela peut par exemple tre un algorithme de tri, qui prend en entre une liste d dentiers, et e e produit en sortie cette mme liste trie A(d). Cela peut aussi tre par exemple un algorithme e e e qui prend en entre deux listes, i.e. un couple d constitu de deux listes, et retourne en sortie e e leur concatnation A(d). e 29

30

CHAPITRE II. COMPLEXITE DES ALGORITHMES. APPLICATIONS.

1.2

La notion de ressource lmentaire ee

On mesure toujours lecacit, cest-`-dire la complexit, dun algorithme en terme dune e a e mesure lmentaire ` valeur enti`re : cela peut tre le nombre dinstructions eectues, la taille ee a e e e de la mmoire utilise, ou le nombre de comparaisons eectues, ou toute autre mesure. e e e Il faut simplement qutant donne une entre d, on sache clairement associer ` lalgorithme e e e a A sur lentre d, la valeur de cette mesure, note (A, d) : par exemple, pour un algorithme de e e tri, si la mesure lmentaire est le nombre de comparaisons eectues, (A, d) est le nombre ee e de comparaisons eectues sur lentre d (une liste dentiers) par lalgorithme de tri A pour e e produire le rsultat A(d) (cette liste dentiers trie). e e Il est clair que (A, d) est une fonction de lentre d. La qualit dun algorithme A nest e e donc pas un crit`re absolu, mais une fonction quantitative (A, .) des donnes dentre vers les e e e entiers.

1.3

Complexit dun algorithme au pire cas e

En pratique, pour pouvoir apprhender cette fonction, on cherche souvent ` valuer cette e a e complexit pour les entres dune certaine taille : il y a souvent une fonction taille qui associe e e a ` chaque donne dentre d, un entier taille(d), qui correspond ` un param`tre naturel. Par e e a e exemple, cette fonction peut tre celle qui compte le nombre dlments dans la liste pour un e ee algorithme de tri, la taille dune matrice pour le calcul du dterminant, la somme des longueurs e des listes pour un algorithme de concatnation. e Pour passer dune fonction des donnes vers les entiers, ` une fonction des entiers (les tailles) e a vers les entiers, on consid`re alors la complexit au pire cas : la complexit (A, n) de lalgorithme e e e A sur les entres de taille n est dnie par e e (A, n) =
d

max (A, d). entre avec taille(d)=n e

Autrement dit, la complexit (A, n) est la complexit la pire sur les donnes de taille n. e e e Par dfaut, lorsquon parle de complexit dalgorithme en informatique, il sagit de complexit e e e au pire cas, comme ci-dessus. Si lon ne sait pas plus sur les donnes, on ne peut gu`re faire plus que davoir cette vision e e pessimiste des choses : cela revient ` valuer la complexit dans le pire des cas (le meilleur des cas ae e na pas souvent un sens profond, et dans ce contexte le pessimisme est de loin plus signicatif).

1.4

Complexit moyenne dun algorithme e

Pour pouvoir en dire plus, il faut en savoir plus sur les donnes. Par exemple, quelles sont e distribues selon une certaine loi de probabilit. e e Dans ce cas, on peut alors parler de complexit en moyenne : la complexit moyenne (A, n) e e de lalgorithme A sur les entres de taille n est dnie par e e (A, n) = E[(A, d)|d entre avec taille(d) = n], e o` E dsigne lesprance (la moyenne). u e e Si lon prf`re, ee (A, n) = d entre avec e

(d)(A, d),
taille(d)=n

o` (d) dsigne la probabilit davoir la donne d parmi toutes les donnes de taille n. u e e e e En pratique, le pire cas est rarement atteint et lanalyse en moyenne peut sembler plus sduisante. e

2. COMPLEXITES ASYMPTOTIQUES

31

Mais, dune part, il est important de comprendre que lon ne peut pas parler de moyenne sans loi de probabilit (sans distribution) sur les entres. Cela implique que lon connaisse dautre e e part la distribution des donnes en entre, ce qui est tr`s souvent dlicat ` estimer en pratique. e e e e a Comment anticiper par exemple les matrices qui seront donnes ` un algorithme de calcul de e a dterminant par exemple ? e On fait parfois lhypoth`se que les donnes sont quiprobables (lorsque cela a un sens, comme e e e lorsquon trie n nombres entre 1 et n et o` lon peut supposer que les permutations en entre sont u e quiprobables), mais cela est est bien souvent totalement arbitraire, et pas rellement justiable. e e Et enn, comme nous allons le voir sur quelques exemples, les calculs de complexit en e moyenne sont plus dlicats ` mettre en uvre. e a

1.5

Complexit dun probl`me e e

On peut aussi parler de la complexit dun probl`me : cela permet de discuter de loptimalit e e e ou non dun algorithme pour rsoudre un probl`me donn. e e e On xe un probl`me P : par exemple celui de trier une liste dentiers. Un algorithme A qui e rsout ce probl`me est un algorithme qui rpond ` la spcication du probl`me P : pour chaque e e e a e e donne d, il produit la rponse correcte A(d). e e La complexit du probl`me P sur les entres de taille n est dnie par e e e e (P, n) =
A

inf algorithme qui rsout e

max (A, d). entre avec taille(d)=n e

Autrement dit, on ne fait plus seulement varier les entres de taille n, mais aussi lalgorithme. e On consid`re le meilleur algorithme qui rsout le probl`me. Le meilleur tant celui avec la meilleur e e e e complexit au sens de la dnition prcdente, et donc au pire cas. Cest donc la complexit du e e e e e meilleur algorithme au pire cas. Lintrt de cette dnition est le suivant : si un algorithme A poss`de la complexit (P, n), ee e e e i.e. est tel que (A, n) = (P, n) pour tout n, alors cet algorithme est clairement optimal. Tout autre algorithme est moins performant, par dnition. Cela permet donc de prouver quun e algorithme est optimal.

2
2.1

Complexits asymptotiques e
Un exemple

Nous allons illustrer la discussion prcdente par un exemple : le probl`me du calcul du maxie e e mum. Ce probl`me est le suivant : on se donne en entre une liste dentiers naturels e1 , e2 , , en , e e avec n 1, et on cherche ` dterminer en sortie M = max1in ei , cest-`-dire le plus grand de a e a ces entiers. Si lentre est range dans un tableau, la fonction Java suivante rsout le probl`me. e e e e static int max(int T[]) { int M=T[0]; int i=1; while (i<T.length) { i f (M<T[i]) M = T[i]; i=i+1; } return M; } Si notre mesure lmentaire correspond au nombre de comparaisons, nous en faisons 2 ee par itration de la boucle, qui est excute n 1 fois, plus 1 derni`re du type i < T.length e e e e

32

CHAPITRE II. COMPLEXITE DES ALGORITHMES. APPLICATIONS.

lorsque i vaut T.length. Nous avons donc (A, d) = 2n 1 pour cet algorithme A. Ce nombre est indpendant de la donne d de taille n, et donc (A, n) = 2n 1. e e Par contre, si notre mesure lmentaire correspond au nombre daectations, nous en ee faisons au minimum 2 avant la boucle while. Chaque itration de la boucle eectue soit 1 ou e 2 aectations suivant le rsultat du test M<T[i]. On a donc pour une entre d de taille n, e e n + 1 (A, d) 2n : la valeur minimum est atteinte pour une liste ayant son maximum en T [0], et la valeur maximum pour une liste sans rptition trie dans lordre croissant. Cette fois e e e (A, d) dpend de lentre d. La complexit au pire cas est donne par (A, n) = 2n. e e e e e e Si lentre est range dans une liste (nous utilisons la classe List du chapitre prcdent), la e e fonction suivante rsout le probl`me. e e static int max( List a) { int M = a.val; for (a = a.next; a != null; i f (a.val > M) M = a.val;} return M; }

a = a.next) {

Si notre mesure lmentaire correspond au nombre de comparaisons entre entiers (nous ee ne comptons pas les comparaisons entre variables de type rfrence sur le type List ) nous en ee faisons 1 par itration de la boucle, qui est excute n 1 fois, soit au total n 1. e e e La complexit (A, n) de cet algorithme A sur les entrs de taille n est donc n 1. e e On peut se poser la question de savoir sil est possible de faire moins de n 1 telles comparaisons : la rponse est non. En eet, cet algorithme est optimal en nombre de comparaisons. e En eet, considrons la classe C des algorithmes qui rsolvent le probl`me de la recherche du e e e maximum de n lments en utilisant comme crit`re de dcision les comparaisons entre lments. ee e e ee Commenons par nonc la proprit suivante : Tout algorithme A de C est tel que tout c e e ee lment autre que le maximum est compar au moins une fois avec un lment qui lui est plus ee e ee grand. En eet, soit i0 le rang du maximum M retourn par lalgorithme sur une liste L = e e1 .e2 . .en : ei0 = M = max1in ei . Raisonnons par labsurde : soit j0 = i0 tel que ej0 ne soit pas compar avec un lment plus grand que lui. Llment ej0 na donc pas t compar e ee ee ee e avec ei0 le maximum. Considrerons la liste L = e1 .e2 . .ej0 1 .M + 1.ej0 +1 . .en obtenue ` partir de L en e a remplaant llment dindice j0 par M +1. c ee Lalgorithme A eectuera exactement les mmes comparaisons sur L et L , sans comparer e L [j0 ] avec L [i0 ] et donc retournera L [i0 ], ce qui est incorrect. Do` une contradiction, qui u prouve la proprit. ee Il dcoule de la proprit quil nest pas possible de dterminer le maximum de n-lments e ee e ee en moins de n 1 comparaisons entre entiers. Autrement dit, la complexit du probl`me P du e e calcul du maximum sur les entres de taille n est (P, n) = n 1. e Lalgorithme prcdent fonctionnant en n 1 telles comparaisons, il est optimal pour cette e e mesure de complexit. e Si notre mesure lmentaire correspond au nombre daectations ` lintrieur de la boucle ee a e for , on voit que ce nombre dpend de la donne. e e On peut sintresser ` sa complexit en moyenne : il faut faire une hypoth`se sur la distrie a e e bution des entres. Supposons que les listes en entres dont on cherche ` calculer le maximum e e a sont des permutations de {1, 2, , n}, et que les n! permutations sont quiprobables. e On peut montrer [13, 6] que la complexit moyenne sur les entres de taille n pour cette e e 1 mesure lmentaire est alors donne par Hn , le n`me nombre harmonique : Hn = 1 + 1 + 3 + ee e e 2 1 n . Hn est de lordre de log n lorsque n tend vers linni.

2. COMPLEXITES ASYMPTOTIQUES

33

Cependant, le calcul est laborieux, et pas si intressant a dvelopper dans ce polycopi, sans e ` e e quil ne paraisse obscur. Simplions, en nous intressons ` un probl`me encore plus simple : plutt que de rechercher e a e o le maximum dans la liste e1 , e2 , , en , avec n 1, donnons nous cette liste et un entier v, et cherchons ` dterminer sil existe un indice 1 i n avec ei = v. a e Lalgorithme suivant rsout le probl`me. e e static boolean trouve(int[] T, int v) { for (int i = 0; i < T.length; i++) i f (T[i] == v) return true; return false; } Sa complexit au pire cas en nombre dinstructions lmentaires est linaire en n, puisque la e ee e boucle est eectue n fois dans le pire cas. e Supposons que les listes en entres sont des permutations de {1, 2, , n}, et que les n! e permutations sont quiprobables. e Remarquons quil y a k n tableaux. Parmi ceux-ci, (k 1)n ne contiennent pas v et dans ce cas, lalgorithme proc`de ` exactement n itrations. Dans le cas contraire, lentier est dans le e a e tableau et sa premi`re occurrence est alors i avec une probabilit de e e (k 1)i1 ki et il faut alors procder ` i itrations. Au total, nous avons une complexit moyenne de e a e e (k 1)n n+ C= kn Or
n n i=1

(k 1)i1 i ki

x,

ixi1 =
i=1

1 + xn (nx n 1) (1 x)2
n i i=1 x

(il sut pour tablir ce rsultat de driver lidentit e e e e C=n

1xn+1 1x )

et donc 1 k
n

(k 1)n (k 1)n n +k 1 (1 + ) n n k k k

=k 1 1

2.2

Asymptotique

On le voit sur lexemple prcdent, raliser une tude prcise et compl`te de complexit est e e e e e e e souvent fastidieux, et parfois dicile. Aussi, on sintresse en informatique plutt ` lordre de e o a grandeur (lasymptotique) des complexits quand la taille n des entres devient tr`s grande. e e e Pour illustrer cette ide, dans le cas o` la mesure lmentaire est le nombre dinstruce u ee tions lmentaires, intressons nous au temps correspondant ` des algorithmes de complexit n, ee e a e n log2 n, n2 , n3 , 1.5n , 2n et n! pour des entres de taille n croissantes, sur un processeur capable e dexcuter un million dinstructions lmentaires par seconde. Nous notons dans le tableau e ee suivant d`s que le temps dpasse 1025 annes (ce tableau est emprunt ` [8]). e e e ea

34 Complexit e n = 10 n = 30 n = 50 n = 100 n = 1000 n = 10000 n = 100000 n = 1000000

CHAPITRE II. COMPLEXITE DES ALGORITHMES. APPLICATIONS. n <1 <1 <1 <1 <1 <1 <1 1s n log2 n <1s <1s <1s <1s <1s <1s 2s 20s n2 <1s <1s <1s <1s 1s 2 min 3 heures 12 jours n3 <1s <1s <1s 1s 18 min 12 jours 32 ans 31, 710 ans 1.5n <1s <1s 11 min 12, 9 ans 2n <1s 18 min 36 ans 1017 ans n! 4s 1025 ans

s s s s s s s

2.3

Conventions

Ce type dexprience invite ` considrer quune complexit en 1.5n , 2n ou n! ne peut pas tre e a e e e considre comme raisonnable. ee On peut discuter de savoir si une complexit en n158 est en pratique raisonnable, mais depuis e les annes 1960 environ, la convention en informatique est que oui : toute complexit borne par e e e un polynme en n est considre comme raisonnable : si vous prfrez, cela revient ` dire quune o ee ee a complexit est raisonnable d`s quil existe des constantes c, d, et n0 telles que la complexit soit e e e d , pour n > n . borne par cn e 0 Des complexits non raisonnables sont par exemple nlog n , 1.5n , 2n et n!. e Cela ore beaucoup davantages : on peut raisonner ` un temps (ou ` un espace mmoire, a a e ou ` un nombre de comparaisons) polynomial pr`s. Cela vite par exemple de prciser de faon a e e e c trop ne le codage. Par exemple, pour un algorithme de calcul de dterminant, comment son e codes les matrices : passer du codage dune matrice par des listes ` un codage par tableau se e a fait en temps polynomial et rciproquement. e Par convention, on raisonne dautre part souvent ` une constante multiplicative pr`s. On a e consid`re que deux complexits qui ne dirent que par une constante multiplicative sont e e e quivalentes : par exemple 9n3 et 67n3 sont considrs comme quivalents. e ee e Ces conventions expliquent que lon parle souvent de complexit en temps de lalgorithme, e sans prciser nement la mesure de ressource lmentaire , i.e. sans prciser le temps de chaque e ee e instruction lmentaire Java par exemple dans ce polycopi. ee e Si vous prfrez : on suppose dans ce polycopi quune opration arithmtique, ou une afee e e e fectation entre deux variables Java (qui ne sont pas des cha nes de caract`res) se fait en temps e constant (unitaire) : cela sappelle le mod`le RAM. e Cela nest pas totalement le reet du temps rel de lopration, mais perturbe les complexits e e e annonces des complexits relles au plus par un polynme (en fait un facteur log n). Les come e e o plexits en temps annonces dans le polycopi sont donc formellement relatives ` la mesure e e e a lmentaire du paragraphe prcdent, plutt qu` un vrai temps dexcution. ee e e o a e

2.4

Notations de Landau

En pratique, on discute dasymptotique de complexits via les notations O, et : Soient e f et g deux fonctions dnies des entiers naturels vers les entiers naturels (comme les fonctions e de complexit dalgorithme ou de probl`mes), on dit que : e e f (n) = O(g(n)) si et seulement si il existe deux constantes positives n0 et B telles que n n0 , f (n) Bg(n) Ce qui signie que f ne cro pas plus vite que g. t En particulier O(1) signie constant(e). Par exemple, un algorithme qui fonctionne en temps O(1), cest un algorithme dont le temps dexcution est constant et ne dpend e e

2. COMPLEXITES ASYMPTOTIQUES

35

pas de la taille des donnes. Cest donc un ensemble constant doprations lmentaires e e ee (exemple : laddition de deux entiers avec les conventions plus hautes). On dit dun algorithme quil est linaire si il utilise O(n) oprations lmentaires. Il est e e ee polynomial si il existe une constante a telle que le nombre total doprations lmentaires e ee est O(na ) : cest la notion de raisonnable plus haut. f (n) = (g(n)) si et seulement si il existe deux constantes positives n0 et B telles que n n0 , f (n) Bg(n). f (n) = (g(n)) si et seulement si il existe trois constantes positives n0 , B et C telles que n n0 , Bg(n) f (n) Cg(n).

2.5

Quelques exemples

Nous donnons ici des exemples simples et varis danalyse dalgorithmes. De nombreux autres e exemples sont dans le polycopi. e

2.6

Factorielle
static int factorielle(int n) { int f = 1; for (int i= 2; i <= n; i++) f = f * i; return f; }

La premi`re version de notre programme permettant de calculer n! est itrative. e e

Nous avons n 1 itrations au sein desquelles le nombre doprations lmentaires est constant. e e ee La complexit est donc O(n). e La seconde version est rcursive. Soit C(n) le nombre doprations ncessaires pour calculer e e e factorielle(n). Nous avons alors C(n) + C(n 1) o` est une constante qui majore le u nombre doprations prcdent lappel rcursif. De plus C(1) se calcule en temps constant et e e e e donc C(n) = O(n). static int factorielle(int n) { i f (n <= 0) return 1; else return n * factorielle(n-1); }

2.7

Recherche Dichotomique

Considrons un tableau T de n entiers tris. On cherche ` tester si un entier v donn se e e a e trouve dans le tableau. Pour ce faire, on utilise une recherche dichotomique. static boolean trouve(int[] T, int v, int min, int max){ i f (min >= max) // vide return false; int mid = (min + max) / 2; i f (T[mid] == v) return true; else i f (T[mid] > v)

36

CHAPITRE II. COMPLEXITE DES ALGORITHMES. APPLICATIONS. return trouve(T, v, min, mid); else return trouve(T, v, mid + 1, max); }

La fonction trouve cherche lentier v dans T entre les indices min et max -1. Pour eectuer une recherche sur tout le tableau, il sut dappeler trouve(T, v, 0, T.length). Le nombre total doprations est proportionnel au nombre de comparaisons C(n) eectues e e n par lalgorithme rcursif. Et donc, nous avons immdiatement : C(n) = 1 + C( 2 ). Soit donc, e e C(n) = O(log n).

2.8

Tours de Hanoi

Le tr`s classique probl`me des tours de Hanoi consiste ` dplacer des disques de diam`tres e e a e e dirents dune tour de dpart ` une tour darrive en passant par une tour intermdiaire. Les e e a e e r`gles suivantes doivent tre respectes : on ne peut dplacer quune disque ` la fois, et on ne e e e e a peut placer un disque que sur un disque plus grand ou sur un emplacement vide. Identions les tours par un entier. Pour rsoudre ce probl`me, il sut de remarquer que si e e lon sait dplacer une tour de taille n de la tour ini vers dest, alors pour dplacer une tour e e de taille n + 1 de ini vers dest, il sut de dplacer une tour de taille n de ini vers temp, un e disque de ini versdest et nalement la tour de hauteur n detemp versdest. public static void hanoi(int n, int ini, int temp, int dest){ i f (n == 1){ // on sait le faire System.out.println("deplace" + ini + " " + dest); return; // sinon recursion } hanoi(n - 1, ini, dest, temp); System.out.println("deplace" + ini + " " + dest); hanoi(n-1, temp, ini, dest); } Notons C(n) le nombre dinstructions lmentaires pour calculer hanoi(n, ini, temp, dest). ee Nous avons alors C(n + 1) 2C(n) + , o` est une constante. Tour de Hanoi est exponentielle. u

2.9

Co t estim vs. co t rel u e u e

Les mesures prsentes ne sont souvent que des estimations asymptotique des algorithmes. e e En pratique, il faut parfois atteindre de tr`s grandes valeurs de n pour quun algorithme en e O(n log n) se comporte mieux quun algorithme en O(n2 ) (quadratique). Les analyses de complexit peuvent servir ` comparer des algorithmes mais le mod`le de cot e a e u est relativement simple (par exemple, les oprations dacc`s aux disques, ou le trac rseau gnr e e e e ee ne sont pas pris en compte alors que ces param`tres peuvent avoir une inuence considrable sur e e un programme). Il est toujours plus able de procder ` des analyses exprimentales avant de e a e choisir le meilleur algorithme, i.e., lalgorithme spciquement adapt au probl`me que lon e e e cherche ` rsoudre, si cette complexit est cruciale. a e e

Tri des listes

Nous disposons depuis le chapitre prcdent de techniques de programmation lmentaires e e ee sur les listes. Il est temps daborder des programmes un peu plus audacieux, et notamment de considrer des algorithmes. Le tri est une opration frquemment eectue. Souvent les tris e e e e interviennent comme brique de base dautres algorithmes, car il est souvent plus simple ou plus ecace deectuer des traitements sur des donnes dans lordre. e

3. TRI DES LISTES Exercice 1 Programmer la mthode static e dune liste.

37 List uniq( List xs) qui limine les doublons e

Solution. Dans une liste trie les lments gaux se suivent. Pour enlever les doublons dune e ee e liste trie, il sut donc de parcourir la liste en la copiant et dviter de copier les lments gaux e e ee e a ` leur successeur dans la liste. private static List uniqSorted( List xs) { e e e i f (xs == null || xs.next == null) { // xs a zro ou un lment return xs ; } // desormais xs != null && xs.next != null else i f (xs.val == xs.next.val) ; return uniqSorted(xs.next) ; } else { return new List (xs.val, uniqSorted(xs.next)) ; } } La smantique squentielle du ou logique || permet la concision (voir A.7.3). En supposant e e donne une mthode sort de tri des listes, on crit la mthode uniq qui accepte une liste e e e e quelconque. static List uniq( List xs) { return uniqSorted(sort(xs)) ; } On peut discuter pour savoir si uniq ci-dessus est plus simple que la version sans tri ci-dessous ou pas. static List uniq( List xs) { List r = null ; for ( ; xs != null ; xs = xs.next ) i f (!mem(x.val, xs)) r = new List (xs.val, r) ; return r ; } En revanche, comme on peut trier (voir plus loin) une liste au prix de de lordre de n log n oprations lmentaires (n longueur de xs), et que le second uniq eectue au pire de lordre de e ee 2 oprations lmentaires, il est certain que le premier uniq est asymptotiquement plus ecace. n e ee Les algorithmes de tri sont nombreux et ils ont fait lobjet danalyses tr`s fouilles, en raison e e de leur intrt et de leur relative simplicit. Sur ce point [11] est une rfrence fondamentale. ee e ee

3.1

Tri par insertion

Ce tri est un des tris de listes les plus naturels. Lalgorithme est simple : pour trier la liste xs, on la parcourt en insrant ses lments ` leur place dans une liste rsultat qui est donc trie. e ee a e e Cest la technique gnralement employe pour trier une main dans les jeux de cartes. e e e static List insertionSort( List xs) { List r = null ; for ( ; xs != null ; xs = xs.next ) r = insert(xs.val, r) ; return r ; } Il reste ` crire linsertion dans une liste trie. Lanalyse inductive conduit ` la question suivante : ae e a soit x un entier et Y une liste trie, quand la liste (x, Y ) est elle trie ? De toute vidence cela e e e est vrai, si

38

CHAPITRE II. COMPLEXITE DES ALGORITHMES. APPLICATIONS.

Y est la liste vide , ou bien, Y = (y, Y ) et x y. En eet, si x y, alors tous les lments de Y trie sont plus grands (au sens large) que x, par ee e transitivit de . Notons P (X) le prdicat dni par e e e P (x, ) = vrai I(x, Y ) = (x, Y ) P (x, (y, Y )) = x y si P (x, Y ) Soit I fonction dinsertion, selon lanalyse prcdente on pose e e

Si P (x, Y ) est invalide, alors Y scrit ncessairement Y = (y, Y ) et on est tent de poser e e e I(x, (y, Y ) = (y, I(x, Y )) Et on aura raison. On montre dabord (par induction structurelle) le lemme vident que I(x, Y ) e regroupe les lments de Y plus x. Ensuite on montre par induction structurelle que I(x, Y ) est ee trie. e Base Si P (x, Y ) est vrai (ce qui comprend le cas Y = ), alors I(x, Y ) est trie. e Induction Sinon, on a Y = (y, Y ). Par hypoth`se dinduction I(x, Y ) est trie. Par ailleurs, e e ) sont ceux de Y plus x, et donc y est plus petit (au sens large) les lments de I(x, Y ee que tous les lments de I(x, Y ) (par hypoth`se (y, Y ) trie et P (x, Y ) invalide). Soit ee e e nalement (y, I(x, Y )) est trie. e private static boolean here(int x, List ys) { return ys == null || x <= ys.val ; } Lexpression boolenne ci-dessus est le ou logique des deux conditions ys == null et x <= ys.val, e elle exploite la smantique squentielle du ou logique (voir A.7.3). e e Bref, revenons ` la mthode dinsertion insert. a e private static List insert(int x, List ys) { i f (here(x, ys)) { return new List (x, ys) ; } else { // NB: !here(ys) => ys != null return new List (ys.val, insert(x, ys.next)) ; } } Penchons nous maintenant sur la complexit en temps de notre implmentation du tri par e e insertion. Les constructions lmentaires de Java employessexcutent en temps constant cest ee e e a ` dire que leur cot est born par une constante. Il en rsulte que mme si le cot dun appel ` u e e e u a la mthode here est variable, selon que le param`tre ys vaut null ou pas, ce cot reste infrieur e e u e a ` une constante correspondant ` la somme des cots dun appel de mthode ` deux param`tres, a u e a e dun test contre null , dun acc`s au champs val etc. e Il nen va pas de mme de la mthode insert qui est rcursive. De fait, le cot dun appel e e e u a ` insert sexprime comme une suite I(n) dune grandeur qui exprime la taille de lentre, ici e la longueur de la liste ys. I(0) k0 I(n + 1) I(n) + k1

Il reste ` programmer dabord le prdicat P a e

Notons que pour borner I(n + 1) nous avons suppos quun appel rcursif tait eectu. Il est e e e e donc immdiat que I(n) est majore par k1 n + k0 . En premi`re approximation, la valeur des e e e constantes k1 et k0 importe peu et on en dduit linformation pertinente que I(n) est en O(n). e

3. TRI DES LISTES

39

En regroupant le cot des diverses instructions de cot constant de la mthode insertionSort u u e selon quelles sont excutes une ou n fois, et en tenant compte des n appels ` insert, le e e a cot S(n) dun appel de cette mthode est born ainsi : u e e
n1

S(n)

k=0

I(k) + k2 n + k3 = k1

n(n 1) + (k0 + k2 ) n + k3 2

Et donc le cot de insertionSort est en O(n2 ). Il sagit dun cot dans le cas le pire en u u raison des majorations eectues (ici sur I(n) principalement). Notons que la notation f (n) e est en O(g(n)) (il existe une constante k telle que, pour n assez grand, on a f (n) k g(n)) est particuli`rement justie pour les cots dans le cas le pire. Il faut noter que lon peut e e u gnralement limiter les calculs aux termes dominants en n. Ici on dira, insertionSort appelle e e n fois insert qui est en O(k) pour k n, et que insertionSort est donc en O(n2 ). On peut aussi mesurer la complexit en terme de nombre de comparaisons. Pour un tri on e a tendance ` compter les comparaisons entre lments. La dmarche se justie de deux faons : a ee e c lopration compte est la plus coteuse (ici que lon songe au tri des lignes dun chier par e e u exemple), ou bien on peut aecter un compte born doprations lmentaires ` chacune des e e ee a oprations comptes. Nous y reviendrons, mais comptons dabord les comparaisons eectues e e e par un appel ` insert. a I(0) = 0 1 I(k + 1) k

On encadre ensuite facilement le nombre de comparaisons eectues par insertionSort pour e une liste de taille n + 1. n S(n + 1) n(n + 1) 2

Par consquent S(n) eectue au plus de lordre de n2 comparaisons et au moins de lordre de e n comparaisons. De lordre de a ici le sens prcis que nous avons trouv des ordres de e e grandeur asymptotiques pour n tendant vers +. Plus exactement encore, S(n) est borne e suprieurement par une fonction en (n2 ) et borne infrieurement par une une fonction en e e e (n). Il est rappel (cf. le cours prcdent) que f est dans (g) quand il existe deux constantes e e e k1 et k2 telles que, pour n assez grand, on a k1 g(n) f (n) k2 g(n) Par ailleurs, nous disposons dune estimation raliste du cot global. En eet, dune part, ` e u a une comparaison eectue dans un appel donn ` insert nous associons toutes les oprations du e ea e corps de cette mthode ; et dautre part, le cot des oprations en temps constant eectues lors e u e e dune itration de la boucle de insertionSort peut tre aect aux comparaisons eectues e e e e par lappel ` insert de cette itration. a e Exercice 2 Donner des expriences particuli`res qui conduisent dune part ` un cot en n2 et e e a u dautre ` un cot en n. Estimer le cot du tri de listes dentiers tirs au hasard. a u u e Solution. Trier des listes de taille croissantes et dj` tries en ordre croissant donne un cot ea e u 2 , car les insertions se font toujours ` la n de la liste r. Mais si les listes sont tries en en n a e ordre dcroissant, alors les insertion se font toujours au dbut de la liste r et le cot est en n. e e u Enn dans une liste quelconque, les insertions se feront plutt vers le milieu de la liste r et le o cot sera plutt en n2 . u o

40

CHAPITRE II. COMPLEXITE DES ALGORITHMES. APPLICATIONS.

Plus rigoureusement, on peut estimer le nombre moyen de comparaisons, au moins dans le cas plus facile du tri des listes de n entiers deux ` deux distincts. Considrons linsertion du k-i`me a e e lment dans la liste r trie qui est de taille k 1. Clairement, parmi toutes les permutations ee e de k lments distincts, les sites dinsertion du dernier lment parmi les k 1 premiers tris se ee ee e rpartissent uniformment entre les k sites possibles. Dans ces conditions, si les permutations e e sont quiprobables, le nombre moyen de comparaisons pour une insertion est donc e 1 1 (1 + 2 + + k 1 + k 1) = k k k(k + 1) 1 2

Par ailleurs, les permutations de n lments distincts se repartissent uniformment selon les ee e ensembles de leurs k premiers lments. On a donc le cot en moyenne ee u 1 S(n) = 0 + 2 Autrement dit (n + 1)(n + 2) 1 3 S(n) = ln n + O(1) = n2 + n ln n + O(1) 4 4 4 Soit nalement le cot moyen est en (n2 ). On note au passage que le cot moyen est ` peu u u a pr`s en rapport moiti du cot dans le cas le pire, ce qui conrme peut-tre le rsultat de notre e e u e e premier raisonnement hasardeux. On peut pareillement estimer le cot en mmoire. Ici on peut se contenter de compter les u e cellules de liste alloues, et voir quun tri alloue au plus de lordre de n2 cellules et au moins de e lordre de n cellules. 2(2 + 1) 1 1 + + 2 k k(k + 1) 1 1 + + 2 n n(n + 1) 1 2

3.2

Tri fusion

Le tri par insertion rsulte essentiellement de lopration dinsertion dans une liste trie. Un e e e autre tri plus ecace rsulte de lopration de fusion de deux listes tris. Par dnition, la fusion e e e e de deux listes tries est une liste trie qui regroupe les lments de deux listes fusionnes. Cette e e ee e opration peut tre programme ecacement, mais commenons par la programmer correctee e e c ment. Sans que la programmation soit bien dicile, cest notre premier exemple de fonction qui op`re par induction sur deux listes. Soit M (X, Y ) la fusion (M pour merge). Il y a deux cas ` e a considrer e Base De toute vidence, M (, Y ) = Y et M (X, ) = X e Induction On a X = (x, X ) et Y = (y, Y ). On distingue deux cas ` peu pr`s symtriques. a e e (X est trie) mais aussi Si x y, alors x minore non seulement tous les lments de X ee e tous les lments de Y (transitivit de et Y est trie). Procdons ` lappel rcursif ee e e e a e , Y ), qui par hypoth`se dinduction renvoie une liste trie regroupant les lments M (X e e ee de X et de Y . Donc, x minore tous les lments de M (X , Y ) cest ` dire que la liste ee a (x, M (X , Y )), qui regroupe bien tous les lments de X et Y , est trie. On pose donc ee e M (X, Y ) = (x, M (X , Y )), quand X = (x, X ), Y = (y, Y ) et x y

Sinon, alors y < x et donc y x. On raisonne comme ci-dessus, soit on pose M (X, Y ) = (y, M (X, Y )), Et en Java, il vient quand X = (x, X ), Y = (y, Y ) et x > y

3. TRI DES LISTES static List merge( List xs, List ys) { i f (xs == null) { return ys ; } else i f (ys == null) { return xs ; e } // NB: dsormais xs != null && ys != null else i f (xs.val <= ys.val) { return new List (xs.val, merge(xs.next, ys)) ; } else { // NB: ys.val < xs.val return new List (ys.val, merge(xs, ys.next)) ; } }

41

On observe que la mthode merge termine toujours, car les appels rcursifs seectuent sur une e e structure plus petite que la structure passe en argument, ici une paire de listes. e Estimons le cot de la fusion de deux listes de tailles respectives n1 et n2 , en comptant les u comparaisons entre lments. Au mieux, tous les lments dune des listes sont infrieurs ` ceux ee ee e a de lautre liste. Au pire, il y a une comparaison par lment de la liste produite. ee min(n1 , n2 ) M (n1 , n2 ) n1 + n2 Par ailleurs M (n1 , n2 ) est une estimation raisonnable du cot global, puisque compter les comu paraisons revient quasiment ` compter les appels et que, hors un appel rcursif, un appel donn a e e eectue un nombre born doprations toutes en cot constant. e e u Trier une liste xs ` laide de la fusion est un bon exemple du principe diviser pour rsoudre. a e On spare dabord xs en deux listes, que lon trie (inductivement) puis on fusionne les deux e listes. Pour sparer xs en deux listes, on peut regrouper les lments de rangs pair et impair e ee dans respectivement deux listes, comme proc`de le dbut du code de la mthode mergeSort. e e e static List mergeSort( List xs) { List ys = null, zs = null ; e boolean even = true ; // zro est pair for ( List p = xs ; p != null ; p = p.next ) { i f (even) { ys = new List (p.val, ys) ; } else { zs = new List (p.val, zs) ; } e e a even = !even ; // k+1 a la parit oppose ` celle de k } . . . Il reste ` procder aux appels rcursifs et ` la fusion, en prenant bien garde dappeler rcursivement a e e a e mergeSort exclusivement sur des listes de longueur strictement plus petite que la longueur de xs. . . . e e e i f (zs == null) { // xs a zro ou un lment return xs ; // et alors xs est trie e } else { // Les longueurs de ys et sz sont < ` la longueur de xs a return merge(mergeSort(ys), mergeSort(zs)) ; } } Comptons les comparaisons qui sont toutes eectues par la fusion : e S(0) = 0 S(1) = 0 S(n) = M (n/2 , n/2) + S(n/2) + S(n/2), pour n > 1

42 Soit lencadrement

CHAPITRE II. COMPLEXITE DES ALGORITHMES. APPLICATIONS.

Si n = 2p+1 on a la simplication tr`s nette e

n/2 + S(n/2) + S(n/2) S(n) n + S(n/2) + S(n/2) 2p + 2 S(2p ) S(2p+1 ) 2p+1 + 2 S(2p )

Ou encore 1/2 + S(2p )/2p S(2p+1 )/2p+1 1 + S(2p )/2p Les rcurrences sont de la forme T (0) = 0, T (p + 1) = k + T (p) de solution k p. On a donc e lencadrement 1/2 p 2p S(2p ) p 2p Soit un cot de lordre de n log n qui se gnralise ` n quelconque (voir lexercice). Le point u e e a important est que le tri fusion eectue toujours de lordre de n log n comparaisons dlments. ee Par ailleurs, le compte des comparaisons estime le cot global de faon raliste, puisque que u c e lon peut aecter le cot de entre une et deux itrations de la boucle de sparation ` chaque u e e a comparaison de la fusion. Exercice 3 (Franchement optionnel) Gnraliser lordre de grandeur du compte des come e paraisons ` n quelconque. a Solution. Dnissons une premi`re suite U (n) pour minorer S(n) e e U (0) = 0 U (1) = 0 U (2q +2) = q +1+2U (q +1) U (2q +3) = q +1+U (q +1)+U (q +2)

Notons que nous avons simplement dtaill e e U (n) = n/2 + U (n/2) + U (n/2) Si on y tient on peut montrer U (n) S(n) par une induction facile. Par ailleurs on sait dj` ea que U (2p ) vaut 1/2 p 2p . Toute lastuce est de montrer que pour p assez grand et n tel que 2p n < 2p+1 , on a U (2p ) U (n) < U (2p+1 ) Posons D(n) = U (n + 1) U (n). Il vient alors D(0) = 0 D(1) = 1 D(2q + 2) = D(q + 1) D(2q + 3) = 1 + D(q + 1) La suite D(n) est donc de toute vidence ` valeurs dans N (cest-`-dire D(n) 0) et on a e a a certainement D(n) > 0 pour n impair. Ce qui sut pour prouver lencadrement pour tout p. En eet U (2p ) U (n) rsulte de D(n) 0, tandis U (n) < U (2p+1 ) rsulte de ce que 2p+1 1 est e e impair. Soit maintenant n > 0, il existe un unique p, avec 2p n < 2p+1 et on a donc Par ailleurs, la fonction x log2 (x) est croissante. Or, pour n > 1 on a n/2 < 2p , il vient donc 1/2 log2 (n/2) n/2 U (n) De mme, on borne suprieurement S(n) par la suite e e V (0) = 0 V (1) = 0 V (n) = n + V (n/2) + V (n/2)) Suite dont on montre quelle est croissante, puis majore par log2 (2n) 2n. Soit enn e 1/2 log2 (n/2) n/2 S(n) log2 (2n) 2n On peut donc nalement conclure que S(n) est en (nlog n), ce qui est lessentiel. Si on souhaite estimer les constantes caches dans le et pas seulement lordre de grandeur asymptotique, notre e encadrement est sans doute un peu large. 1/2 p 2p U (n)

3. TRI DES LISTES

43

3.3

Trier de grandes listes

Pour conrmer le ralisme des analyses de complexit des tris, nous trions des listes de N e e entiers et mesurons le temps dexcution des deux tris par insertion I et fusion M. Les lments e ee des listes sont les N premiers entiers dans le dsordre. e 30 25 20 T (s) 15 10 5 3 3 3 3 I M 3 3 + 3

3 3 3 3 3 3 + + + + + + + 0 3 3 3 3 3 3 3 + + + + + + + + + + + + + 0 1000 2000 3000 4000 5000 6000 7000 8000 9000 10000 N Cette exprience semble conrmer que le tri par insertion est quadratique. Elle montre surtout e que le tri fusion est bien plus ecace que le tri par insertion. Encourags, nous essayons de mesurer le temps dexcution du tri fusion pour des valeurs e e de N plus grandes. Nous obtenons : N=10000 T=0.093999 N=20000 T=0.411111 N=30000 T=0.609787 Exception in thread "main" java.lang.StackOverflowError at List.merge(List.java:80) at List.merge(List.java:78) . . . O` de lordre de 1000 lignes at List.merge. . . sont eaces. u e On voit que notre programme choue en signalant que lexception StackOverowError e (dbordement de la pile) a t lance. Nous expliquerons plus tard prcisment ce qui se passe. e ee e e e Pour le moment, contentons nous dadmettre que le nombre dappels de mthode en attente est e limit. Or ici, merge appelle merge qui appelle merge qui etc. et merge doit attendre le retour e de merge qui doit attendre le retour de merge qui etc. Et nous touchons l` une limite de la a programmation rcursive. En eet, alors que nous avons programm une mthode qui semble e e e thoriquement capable de trier des centaines de milliers dentiers, nous ne pouvons pas lexcuter e e au del` de 30000 entiers. a Pour viter les centaines de milliers dappels imbriqus de merge nous pouvons reprogrammer e e la fusion itrativement. On emploie la technique de linitialisation dire (voir lexercice 2), e ee puisquil est ici crucial de construire la liste rsultat selon lordre des listes consommes. e e static List merge( List xs, List ys) { i f (xs == null) return ys ; i f (ys == null) return xs ; /* Ici le resultat a une premi`re cellule */ e /* reste ` trouver ce qui va dedans */ a List r = null ; i f (xs.val <= ys.val) { r = new List (xs.val) ; xs = xs.next ;

44

CHAPITRE II. COMPLEXITE DES ALGORITHMES. APPLICATIONS. } else { r = new List (ys.val) ; ys = ys.next ; } List last = r ; // Derni`re cellule de la liste rsultat e e /* Rgime permanent */ e while (xs != null && ys != null) { i f (xs.val <= ys.val) { last.next = new List (xs.val) ; xs = xs.next ; } else { last.next = new List (ys.val) ; ys = ys.next ; } e a last = last.next ; // Derni`re cellule ` nouveau } /* Ce nest pas fini, une des deux listes peut ne pas ^tre vide */ e i f (xs == null) { last.next = ys ; } else { last.next = xs ; } return r ;

} La nouvelle mthode merge na rien de dicile ` comprendre, une fois assimile linitialisation e a e dire. Il faut aussi ne pas oublier dinitialiser le champ next de la toute derni`re cellule alloue. ee e e Mais attendons ! Le nouveau tri peut il fonctionner en pratique, puisque mergeSort elle mme e est rcursive ? Oui, car mergeSort sappelle sur des listes de longueur divise par deux, et donc le e e nombre dappels imbriqus de mergeSort vaut au plus ` peu pr`s log2 (N ), certainement infrieur e a e e a ` 32 dans nos expriences o` N est un int . Ceci illustre quil ny a pas lieu de drcursiver e u ee systmatiquement. e Plutt que de simplement mesurer le temps dexcution du nouveau mergeSort, nous allons o e le comparer ` un autre tri : celui de la biblioth`que de Java. La mthode statique sort (classe a e e Arrays, package java.util) trie un tableau de int pass en argument. Puisque nous tenons e a ` trier une liste, il sut de changer cette liste en tableau, de trier le tableau, et de changer le tableau en liste. static List arraySort( List xs) { // Copier les elments de xs dans un tableau t e int sz = card(xs) ; // Longueur de la liste int [] t = new int[sz] ; int k = 0 ; for ( List p = xs ; xs != null ; xs = xs.next, k++) { t[k] = xs.val ; } // Trier t java.util.Arrays.sort(t) ; // Fabriquer une liste avec les lments de t, attention ` lordre ! e e a List r = null ; for (k = sz-1 ; k >= 0 ; k--) { r = new List (t[k], r) ; } return r ; } Employer une mthode de tri de la biblioth`que est probablement une bonne ide, car elle est e e e crite avec beaucoup de soin et est donc tr`s ecace [2]. Observons quen supposant, et cest e e

4. LISTES BOUCLEES, CALCUL DE LENVELOPPE CONVEXE

45

plus que raisonnable, un cot du tri en O(N log N ), qui domine asymtotiquement nos transferts u de liste en tableau et rciproquement (en O(N )), on obtient un tri en O(N log N ). Dans les e mesures, M est notre nouveau tri fusion, tandis que A est le tri arraySort. 5 4.5 4 3.5 3 T (s) 2.5 2 1.5 1 0.5 0 3 M 3 3 3 A + 3 3 3 3 3 3 3 3 3 3

3 3 + 3 + + + + + + + + + + + + + + + + + + + 0 50000 100000 150000 200000 250000 300000 N

3 3

On voit que notre mthode mergeSort conduit ` un temps dexcution qui semble ` peu pr`s e a e a e linaire, mais quil reste des progr`s ` accomplir. De fait, en programmant un tri fusion destructif e e a (voir [12, Chapitre 12] par ex.) qui nalloue aucune cellule, on obtient des temps bien meilleurs, mais encore domins par ceux du tri de la biblioth`que. e e

Listes boucles, Calcul de lenveloppe convexe e

Des lments peuvent tre organiss en squence, sans que ncessairement la squence soit ee e e e e e nie. Si la squence est priodique, elle devient reprsentable par une liste qui au lieu de nir, e e e repointe sur une de ses propres cellules. Par exemple, on peut tre tent de reprsenter les e e e dcimales de 1/2 = 0.5, 1/11 = 0.090909 et 1/6 = 0.16666 par e 5 0 9 1 6

Nous distinguons ici, dabord une liste nie, ensuite une liste circulaire le retour est sur la premi`re cellule, et enn une liste boucle la plus gnrale. e e e e Nous allons illustrer la notion de liste boucle par la programmation du calcul de lenveloppe e convexe.

4.1

Reprsentation des polygones e

Il est pratique de reprsenter un polygone ferm par la liste circulaire de ses sommets. La e e squence des points est celle qui correspond ` lorganisation des sommets et des artes. e a e A4 A3 B1 B0 B1 B0

A0 A2 A1 B2 B3 B2 B3

Les polygones tant ferms, les listes circulaires sont naturelles. Lordre des sommets dans la e e liste est signicatif, ainsi les polygones (B0 B1 B2 B3 ) et (B0 B1 B3 B2 ) sont distincts.

46

CHAPITRE II. COMPLEXITE DES ALGORITHMES. APPLICATIONS.

Pour reprsenter les polygones en machine, nous dnissons une classe des cellules de liste e e de points. import java.awt.* ; class Poly { private Point val ; private Poly next ; private Poly (Point val, Poly next) { this.val = val ; this.next = next ; } . . . } Tout est priv, compte tenu des manipulations dangereuses auxquelles nous allons nous livrer. e La classe Point des points est celle de la biblioth`que (package java.awt), elle est sans surprise : e p.x et p.y sont les coordonnes, p.distance(q) renvoie la distance entre les points p et q. e Nous allons concevoir et implmenter un algorithme de calcul de lenveloppe convexe dun e nuage de points, algorithme qui sappuie naturellement sur la reprsentation en machine des e polygones (convexes) par les listes circulaires. Mais commenons dabord par nous familiariser c tant avec la liste circulaire, quavec les quelques notions de gomtrie plane ncessaires. Pour e e e nous simplier un peu la vie nous allons poser quelques hypoth`ses. e Les polygones ont toujours au moins un point. Par consquent, null nest pas un polygone. e Les points sont en position gnrale. Ce qui veut dire que les points sont deux ` deux e e a distincts, et quil ny a jamais trois points alignes. e Le plan est celui dune fentre graphique Java, cest ` dire que lorigine des coordonnes e a e est en haut et ` gauche. Ce qui entra que le rep`re est en sens inverse du rep`re usuel a ne e e du plan (laxe des y pointe vers le bas).

4.2

Oprations sur les polygones e

Lintrt de la liste circulaire appara pour, par exemple, calculer le prim`tre dun polygone. ee t e e Pour ce faire, il faut sommer les distances (A0 A1 ), (A1 A2 ), . . . (An1 A0 ). Si nous reprsentions e le polygone par la liste ordinaire de ses points, le cas de la derni`re arte (An1 A0 ) serait e e particulier. Avec une liste circulaire, il nen est rien, puisque le champ next de la cellule du point An1 pointe vers la cellule du point A0 . La sommation se fait donc simplement en un parcours des sommets du polygone. An de ne pas sommer indniment, il convient darrter e e le parcours juste apr`s cette derni`re arte, cest ` dire quand on retrouve la premi`re e e e a e cellule une seconde fois. static double perimeter(Poly p) { double r = 0.0 ; Poly q = p ; do { r += q.val.distance(q.next.val) ; q = q.next ; } while (q != p) ; // Attention : diffrence des rfrences (voir A.3.1.1) e e e return r ; } Lusage dune boucle do {. . .} while (. . .) (section A.3.4) est naturel. En eet, on veut nir ditrer quand on voit la premi`re cellule une seconde fois, et non pas une premi`re fois. On e e e observe que le prim`tre calcul est 0.0 pour un polygone ` un sommet et deux fois la distance e e e a (A0 A1 ) pour deux sommets. Nous construisons un polygone ` partir de la squence de ses sommets, extraite dun chier a e ou bien donne ` la souris. La lecture des sommets se fait selon le mod`le du Reader (voir e a e

4. LISTES BOUCLEES, CALCUL DE LENVELOPPE CONVEXE

47

section A.6.2.1). Soit donc une classe PointReader des lecteurs de points pour lire les sources de points (chier ou interface graphique). Ses objets poss`dent une mthode read qui consomme e e et renvoie le point suivant de la source, ou null quand la source est tarie (n de chier, double clic). Nous crivons une mthode de construction du polygone form par une source de points. e e e static Poly readPoly(PointReader in) { Point pt = in.read() ; i f (pt == null) throw new Error ("Polygone vide interdit") ; Poly r = new Poly (pt, readPoints(in)) ; nappend(r,r) ; // Pour refermer la liste, voir exercice 4 return r ; } private static Poly readPoints(PointReader in) { Point pt = in.read() ; i f (pt == null) { return null ; } else { return new Poly(pt, readPoints(in)) ; } } La programmation est rcursive pour facilement prserver lordre des points de la source. La e e liste est dabord lue puis referme. Cest de loin la technique la plus simple. Conformment e e ` a ` nos conventions, le programme refuse de construire un polygone vide. A la place, il lance lexception Error avec pour argument un message explicatif. Leet du lancer dexception peut se rsumer comme un chec du programme, avec achage du message (voir A.4.2 pour les e e dtails). e Nous nous posons maintenant la question didentier les polygones convexes. Soit maintenant P un polygone. La paire des points p.val et p.next.val est une arte (A0 A1 ), cest-`-dire un e a segment de droite orient. Un point quelconque Q, peut se trouver ` droite ou ` gauche du e a a segment (A0 A1 ), selon le signe du dterminant det(A0 A1 , A0 P ). La mthode suivante permet e e donc didentier la position dun point relativement ` la premi`re arte du polygone P . a e e private static int getSide(Point a0, Point a1, Point q) { return (q.y - a0.y) * (a1.x - a0.x) - (q.x - a0.x) * (a1.y - a0.y) ; } static int getSide(Point q, Poly p) { return getSide (p.val, p.next.val, q) ; } Par lhypoth`se de position gnrale, getSide ne renvoie jamais zro. Une vrication rapide e e e e e nous dit que, dans le syst`me de coordonnes des fentres graphiques, si getSide(q, p) est e e e ngatif, alors le point q est a gauche de la premi`re arte du polygone p. e ` e e Un polygone est convexe (et bien orient) quand tous ses sommets sont ` gauche de toutes e a ses artes. La vrication de convexit revient donc ` parcourir les artes du polygone et, pour e e e a e chaque arte ` vrier que tous les sommets autres que ceux de larte sont bien ` gauche. e a e e a static boolean isConvex(Poly p) { Poly a = p ; // a est lar^te courante e do { Poly s = a.next.next ; // s est le sommet courant while (s != a) { i f (getSide(s.val, a) > 0) return false ; s = s.next ; // sommet suivant

48

CHAPITRE II. COMPLEXITE DES ALGORITHMES. APPLICATIONS. } e a = a.next ; // ar^te suivante } while (a != p) ; return true ;

} On note les deux boucles imbriques, une boucle sur les sommets dans une boucle sur les artes. e e La boucle sur les sommets est une boucle while par souci esthtique de ne pas appeler getSide e avec pour arguments des points qui ne seraient pas en position gnrale. e e Exercice 4 Que se passe-t-il si p est un polygone ` un, ` deux sommets ? a a Solution. Dans les deux cas, aucune itration de la boucle intrieure sur les sommets nest e e excute. Par consquent les polygones ` un et deux sommets sont jugs convexes et bien orients. e e e a e e Pour un polygone ` n sommets, la boucle sur les artes est excute n fois, et ` chaque fois, la a e e e a boucle sur les sommets est excute n 2 fois. La vrication de convexit cote donc de lordre e e e e u de n2 oprations lmentaires. e ee Notons que la mthode getSide nous permet aussi de dterminer si un point est ` lintrieur e e a e dun polygone convexe. static boolean isInside(Point q, Poly p) { Poly a = p ; do { i f (getSide(q, a) > 0) return false a = a.next ; } while (a != p) ; return true ; } Notons que la convexit permet une dnition et une dtermination simplie de lintrieur. e e e e e La mthode isInside a un cot de lordre de n opration lmentaires, o` n est la taille du e u e ee u polygone. Exercice 5 Que se passe-t-il si p est un polygone ` un, ` deux sommets ? a a Solution. Le cas limite du polygone ` deux sommets est bien trait : pour trois points en a e position gnrale, lun des deux appels ` getSide renvoie ncessairement un entier positif. e e a e Par consquent isInside renvoie ncessairement false, ce qui est conforme ` lintuition que e e a lintrieur dun polygone ` deux sommets est vide. e a Il nen va pas de mme du polygone ` un seul sommet, dont lintrieur est a priori vide. e a e Or, dans ce cas, getSide est appele une seule fois, les deux sommets de larte tant le mme e e e e point. La mthode getSide renvoie donc zro, et isInside renvoie toujours true. e e

4.3

Calcul incrmental de lenveloppe convexe e

Lenveloppe convexe dun nuage de points est le plus petit polygone convexe dont lintrieur e contient tous les points du nuage. Pour calculer lenveloppe convexe, nous allons procder e incrmentalement, cest-a-dire, en supposant que nous possdons dj` lenveloppe convexe des e e ea points dj` vus, construire lenveloppe convexe des points dj` vus plus un nouveau point. ea ea Soit donc la squence de points P0 , P1 , . . . Pk , dont nous connaissons lenveloppe convexe E = e (E0 E1 . . . Ec ), Et soit Q = Pk+1 le nouveau point.

4. LISTES BOUCLEES, CALCUL DE LENVELOPPE CONVEXE

49

Si Q est ` lintrieur de E, lenveloppe convexe ne change pas. a e Sinon, il faut identier deux sommets Ei et Ej , tels que tous les points de E sont ` gauche a de (Ei Q) et (QEj ). La nouvelle enveloppe convexe est alors obtenue en remplaant la c squence (Ei . . . Ej ) par (Ei QEj ). e E1 E0 = Ej

E2 E4 Q

E3 = Ei Sans raliser une dmonstration compl`te, il semble clair que Q se trouve ` droite des e e e a artes supprimes et ` gauche des des artes conserves. Mieux, lorsque lon parcourt les e e a e e sommets du polygone, le sommet Ei marque une premi`re transition : Q est ` gauche de e a (Ei1 Ei ) et ` droite de (Ei Ei+1 ), tandis que le sommet Ej marque la transition inverse. a On peut aussi imaginer que Q est une lampe, il faut alors supprimer les artes claires e e e de lenveloppe. Il faut donc identier les sommets de E o` seectuent les transitions Q ` gauche puis ` u a a droite et inversement. Le plus simple para de se donner dabord une mthode getRight qui t e renvoie la premi`re arte qui laisse Q ` droite, ` partir dun pointeur initial vers lenveloppe e e a a suppos quelconque. e static Poly getRight(Poly e, Point q) { Poly p = e ; do { i f (getSide(q, p) > 0) return p ; p = p.next ; } while (p != e) ; return null ; } Notons que dans le cas o` le point Q est ` lintrieur de E, cest-`-dire ` gauche de toutes ses u a e a a artes, getRight renvoie null . On crit aussi une mthode getLeft pour dtecter la premi`re e e e e e arte de E qui laisse Q ` gauche. Le code est identique, au test getSide(q, p) > 0, ` changer e a a en getSide(q, p) < 0, pr`s. e La mthode extend dextension de lenveloppe convexe, trouve dabord une arte qui laisse e e Q ` droite, si une telle arte existe. Puis, le cas chant, elle continue le parcours, en encha a e e e nant un getLeft puis un getRight an didentier les points Ej puis Ei qui dlimitent la partie e claire de lenveloppe. e e private static Poly extend(Poly e, Poly oneRight = getRight(e, q) ; i f (oneRight == null) return e ; // Lar^te issue de oneRight est e Poly ej = getLeft(oneRight.next, Poly ei = getRight(ej.next, q) ; Poly r = new Poly(q, ej) ; ei.next = r ; return r ; } Point q) { // Le point q est intrieur e e claire e q) ; // lumi`re -> ombre e // ombre -> lumi`re e

La structure de liste circulaire facilite lcriture des mthodes getRight et getLeft. Elle pere e met aussi, une fois trouvs Ei et Ej , une modication facile et en temps constant de lenveloppe e convexe. Les deux lignes qui eectuent cette modication sont directement inspires du dese

50

CHAPITRE II. COMPLEXITE DES ALGORITHMES. APPLICATIONS.

sin dextension de polygone convexe ci-dessus. Il est clair que chacun des appels ` getRight a et getLeft eectue au plus un tour du polygone E. La mthode extend eectue donc au pire e trois tours du polygone E (en pratique au plus deux tours, en raison de lalternance des appels a ` getRight et getLeft). Elle est donc en O(c), o` c est le nombre de sommets de lenveloppe u convexe. Il nous reste pour calculer lenveloppe convexe dun nuage de points lus dans un PointReader a ` initialiser le processus. On peut se convaincre assez facilement que extend fonctionne correctement d`s que E poss`de deux points, car dans ce cas il existe une arte claire et une arte e e e e e e sombre. On crit donc. e static Poly convexHull(PointReader in) { Point q1 = in.read() ; Point q2 = in.read() ; i f (q1 == null || q2 == null) throw new (Error "Pas assez de points") ; Poly e = new Poly (q1, new Poly (q2, null)) ; nappend(e,e) ; // Refermer la liste for ( ; ; ) { // Idiome : boucle infinie Point q = in.read() i f (q == null) return e ; e = extend(e, q) ; } } Pour un nuage de n points, on excute de lordre de n fois extend, dont le cot est en O(c), e u sur des enveloppes qui poss`dent c n sommets. Le programme est donc en O(n2 ). Le temps e dexcution dpend parfois crucialement de lordre de prsentation des points. Dans le cas o` par e e e u exemple lenveloppe convexe est forme par les trois premiers points lus, tous les points suivants e tant intrieurs ` ce triangle, alors on a un cot linaire. Malheureusement, si tous les points de e e a u e lentre se retrouvent au nal dans lenveloppe convexe, le cot est bien quadratique. La version e u web du cours pointe vers un programme de dmonstration de cet algorithme. e

4.4

Calcul ecace de lenveloppe convexe

Lenveloppe convexe dun nuage de n points du plan peut se calculer en O(n log n) oprations, e a ` laide de lalgorithme classique de la marche de Graham (Graham scan) Nous nexpliquerons pas la marche de Graham, voir par exemple [4, Chapitre 35] ou [12, Chapitre 25]. En eet, un ranement de la technique incrmentale inspire un algorithme tout aussi ecace, dumoins e asymptotiquement. De plus ce nouvel algorithme va nous permettre daborder une nouvelle sorte de liste. Rappelons lide de lalgorithme incrmental, sous une forme un peu image. Lenveloppe e e e convexe E est soumise ` la lumi`re mise par le nouveau point Q. En raison de sa convexit mme, a e e e e E poss`de une face claire et une face sombre. La face claire va du sommet Ei au sommet Ej , e e e e e et lextension de lenveloppe revient ` remplacer la face claire par Ei QEj . Supposons conna a e e tre un sommet P de lenveloppe convexe qui est clair par le nouveau point Q (cest-a-dire que e e larte issue de P laisse Q ` droite). Le sens de rotation selon les next nous permet de trouver e a la n de la face claire directement, cest-`-dire sans passer par la face sombre. Pour pouvoir e e a trouver le dbut de la face claire tout aussi directement, il faut pouvoir tourner dans lautre e e e sens ! Nous allons donc ajouter un nouveau champ prev ` nos cellules de polygones, pour les a cha ner selon lautre sens de rotation. class Poly { . . .

4. LISTES BOUCLEES, CALCUL DE LENVELOPPE CONVEXE private Poly prev ; private Poly (Point val, Poly next, Poly prev) { this(val, next) ; // Appeler lautre constructeur this.prev = prev ; } . . . }

51

Nos listes de sommets deviennent des listes doublement cha ees (et circulaires qui plus est), une n structure de donnes assez sophistique, que nous pouvons illustrer simplement par le schma e e e suivant : Ej

P Q Ei En suivant les champs prev (nots par des `ches courbes), nous trouvons facilement la premi`re e e e arte inverse sombre ` partir du point P , cest-`-dire le premier sommet Ei dont est issue une e a a arte inverse qui laisse le point Q ` droite. e a static int getSidePrev(Poly a, Point q) { return getSide(a.val, a.prev.val, q) ; } static Poly getRightPrev(Poly e, Point q) { Poly p = e ; for ( ; ; ) { i f (getSidePrev(q, p) > 0) return p ; p = p.prev ; } } Et nous pouvons crire la nouvelle mthode dextension de lenveloppe convexe. e e // Attention e.val doit ^tre eclair par q (ie getSide(e,q) > 0) e e private static Poly extend2(Poly e, Point q) { Poly ej = getLeft(e.next, q) ; Poly ei = getRightPrev(e, q) ; Poly r = new Poly(q, ej, ei) ; ei.next = r ; ej.prev = r ; return r ; } La mthode extend2 identie la face claire pour un cot proportionnel ` la taille de cette e e e u a face. Puis, elle construit la nouvelle enveloppe en remplaant la face claire par (Ei QEj ). Les c e e manipulations des champs next et prev sont directement inspires par le schma ci-dessus. e e La mthode nest correcte que si Q claire larte issue de P . Ce sera le cas, quand e e e (1) P est le point dabscisse maximale parmi les points dj` vus. ea

52

CHAPITRE II. COMPLEXITE DES ALGORITHMES. APPLICATIONS.

(2) Q est dabscisse strictement suprieure ` celle de de P . e a En fait lhypoth`se de position gnrale ninterdit pas que deux points poss`dent la mme abscisse e e e e e (mais cest interdit pour trois points). En pareil cas, pour que Q claire larte issue de P , il e e sut que lordonne de Q soit infrieure ` celle de P , en raison de la convention que laxe des e e a y pointe vers le bas. Autrement dit, P est strictement infrieur ` Q selon lordre suivant. e a // p1 < p2 <=> compare(p1,p2) < 0 static int compare(Point p1, Point p2) { i f (p1.x < p2.x) return -1 ; else i f (p1.x > p2.x) return 1 ; else i f (p1.y < p2.y) return 1 ; else i f (p1.y > p2.y) return -1 ; else return 0 ; } On note que le point ajout Q fait toujours partie de la nouvelle enveloppe convexe. En fait, il e sera le nouveau point distingu lors de la prochaine extension, comme exprim par la mthode e e e compl`te de calcul de lenveloppe convexe. e static Poly convexHullSorted(PointReader in) { Point pt1 = in.read() ; Point pt2 = in.read() ; i f (pt1 == null || pt2 == null || compare(pt1,pt2) >= 0) throw new Error ("Deux premiers point incorrects") ; Poly p1 = new Poly(pt1, null, null) ; Poly p2 = new Poly(pt2, p1, null) ; p1.next = p2 ; p1.prev = p2 ; p2.prev = p1 ; Poly e = p2 ; for ( ; ; ) { Point q = in.read() ; i f (q == null) return e ; e i f (compare(e.val, q) >= 0) throw new Error ("Points non tris") ; e = extend2(e, q) ; } } On peut donc assurer la correction de lalgorithme complet en triant les points de lentre e selon les abscisses croissantes, puis les ordonnes dcroissantes en cas dgalit. Evidemment e e e e pour pouvoir trier les points de lentre il faut la lire en entier, lalgorithme nest donc plus e incrmental. e Estimons maintenant le cot du nouvel algorithme de calcul de lenveloppe convexe de u n points. On peut trier les n points en O(n log n). Nous prtendons ensuite que le calcul propree ment dit de lenveloppe est en O(n). En eet, le cot dun appel ` extend2 est en O(j i) o` u a u j i 1 est le nombre de sommets supprims de lenveloppe ` cette tape. Or un sommet est e a e supprim au plus une fois, ce qui conduit ` une somme des cots des appels ` extend2 en O(n). e a u a Le tri domine et lalgorithme complet est donc en O(n log n). Cet exemple illustre que le choix dune structure de donnes approprie (ici la liste doublement cha ee). est parfois crucial pour e e n atteindre la complexit thorique dun algorithme. Ici, avec une liste cha ee seulement selon e e n les champs next, il nest pas possible de trouver le sommet Ei ` partir dun sommet de la face a claire sans faire le tour par la face sombre. e e

Chapitre III

Piles et les
Les piles et les les sont des exemples de structures de donnes que faute de mieux, nous e appellerons des sacs. Un sac ore trois oprations lmentaires : e ee (1) tester si le sac est vide, (2) ajouter un lment dans le sac, ee (3) retirer un lment du sac (tenter de retirer un lment dun sac vide dclenche une erreur). ee ee e Le sac est une structure imprative : un sac se modie au cours du temps. En Java, il est e e logique de reprsenter un sac (dlments E) par un objet (dune classe Bag E qui poss`de e ee trois mthodes. e class Bag E { Bag E { . . . } // Construire un sac vide boolean isEmpty() { . . . } add(E e) { . . . } E remove() { . . . } } Ainsi on ajoute par exemple un lment e dans le sac b par b.add(e), ce qui modie ltat ee e interne du sac. Piles et les se distinguent par la relation entre lments ajouts et lments retirs. Dans le ee e ee e cas des piles, cest le dernier lment ajout qui est retir. Dans le cas dune le cest le premier ee e e lment ajout qui est retir. On dit que la pile est une structure LIFO (last-in rst-out), et que ee e e la le est une structure FIFO (rst-in rst-out). Si on reprsente pile et le par des tas de cases, on voit que la pile poss`de un sommet, o` e e u sont ajouts et do` sont retirs les lments, tandis que la le poss`de une entre et une sortie. e u e ee e e Tout ceci est parfaitement conforme ` lintuition dune pile dassiettes, ou de la queue devant a Fig. 1 Une pile (ajouter et retirer du mme ct), une le (ajouter et retirer de cts opposs). e oe oe e
5 4 3 2 1 0 0 1 5 4 3 2 1 0 0 1

Sommet de pile

Sortie de le

Entre de le e

un guichet, et sugg`re fortement des techniques dimplmentations pour les piles et les les. e e 53

54

CHAPITRE III. PILES ET FILES

Exercice 1 Soit la mthode build suivante qui ajoute tous les lments dune liste dans un sac e ee de int puis construit une liste en vidant le sac. static List build( List p) { Bag bag = new Bag () ; for ( ; p != null ; p = p.next) bag.add(p.val) ; List r = null ; while (!bag.isEmpty()) r = new List (bag.remove(), r) ; return r ; } Quelle est la liste renvoye dans le cas o` le sac est une pile, puis une le ? e u Solution. Si le sac est une pile, alors build renvoie une copie de la liste p. Si le sac est une le, alors build renvoie une copie en ordre inverse de la liste p. Il y a un peu de vocabulaire spcique aux piles et aux les. Traditionnellement, ajouter e un lment dans une pile se dit empiler (push), et lenlever dpiler (pop), tandis quajouter un ee e lment dans une le se dit enler, et lenlever dler. ee e

` A quoi a sert ? c

La le est peut-tre la structure la plus immdiate, elle modlise directement les les dattente e e e gres selon la politique premier-arriv, premier-servi. La pile est plus informatique par nature. ee e Dans la vie, la politique dernier-arriv, premier-servi nest pas tr`s populaire. Elle correspond e e pourtant ` une situation courante, la survenue de tches de plus en plus urgentes. Les piles a a modlisent aussi tout syst`me o` lentre et la sortie se font par le mme passage oblig : e e u e e e wagons sur une voie de garage, cabine de tlphrique, etc. ee e

1.1

Premier arriv, premier servi e

Utiliser une le informatique est donc naturel lorsque lon modlise une le dattente au sens e courant du terme. Considrons un exemple simple : e Un unique guichet ouvert 8h00 conscutives (soit 8 60 60 secondes). e Les clients arrivent et font la queue. La probabilit darrive dun nouveau client dans e e lintervalle [t . . . t + 1[ est p. Le temps que prend le service dun client suit une loi uniforme sur [30 . . . 300[. Les clients, sil attendent trop longtemps, partent sans tre servis. Leur patience est une e loi uniforme sur [120 . . . 1800[ Notre objectif est dcrire une simulation informatique an destimer le rapport clients repartis e sans tre servis sur nombre total de clients, en fonction de la probabilit p. e e Nous implmentons la simulation par une classe Simul. Pour raliser les tirages alatoires e e e ncessaires, nous employons une source pseudo-alatoire, les objets de la classe Random du e e package java.util (voir A.1.4 pour la notion de package et A.6.3.1 pour une description de Random). // Source pseudo-alatoire e static private Random rand = new Random () ; // Loi uniforme sur [min. . .max[ static private int uniform(int min, int max) { return min + rand.nextInt(max-min) ;

` 1. A QUOI CA SERT ? } // Survenue dun vnement de probabilit prob [0. . .1] e e e static private boolean occurs(double prob) { return rand.nextDouble() < prob ; }

55

ee Un client en attente est modlis par un objet de la classe Client . Un objet Client est cr e e lors de larrive du client au temps t, ce qui donne loccasion de calculer lheure ` laquelle le e a client exaspr repartira sil il na pas t servi. ee ee class Client { // Temps dattente final static int waitMin = 120, int arrival, departure ; Client (int t) { arrival = t ; departure = t+Simul.uniform(waitMin, waitMax) ; } } Noter que les constantes qui gouvernent le temps dattente sont des variables nal de la classe Client . Limplmentation dune le sera dcrite plus tard. Pour linstant une le (de clients) est un e e objet de la classe Fifo qui poss`de les mthodes isEmpty, add et remove dcrites pour les Bag e e e de lintroduction. La simulation est discr`te. Durant la simulation, une variable tFree indique la date o` le e u ` guichet sera libre. A chaque seconde t, il faut : Faire un tirage alatoire pour savoir si un nouveau client arrive. e Si oui, ajouter le nouveau client (avec sa limite de temps) dans la le dattente. Quand un client est disponible et que le guichet est libre (si t est suprieur ` tFree). e a Extraire le client de la le. Vrier quil est toujours l` (sinon, passer au client suivant). e a Tirer alatoirement un temps de traitement. e Et ajuster tFree en consquence. e Le code qui ralise cette simulation na en donn par la gure 2. La mthode simul prend e ve e e la probabilit darrive dun client en argument et renvoie le rapport clients non-servis sur e e nombre total de clients. Le code suit les grandes lignes de la description prcdente. Il faut e e surtout noter comment on rsout le probl`me dune boucle while qui doit dune part sefe e fectuer tant que la le nest pas vide et dautre part cesser d`s quun client eectivement e prsent est extrait de la le (sortie par break). On note aussi la conversion de type explie cite ((double)nbFedUp)/nbClients, ncessaire pour forcer la division ottante. Sans cette e conversion on aurait la division euclidienne. La gure 3 donne les rsultats de la simulation pour e diverses valeurs de la probabilit prob. Il sagit de moyennes sur dix essais. e

waitMax = 1800 ;

1.2

Utilisation dune pile

Nous allons prendre en compte la nature tr`s informatique de la pile et proposer un exemple e plutt informatique. Les calculatrices dune cl`bre marque tats-unienne ont popularis une o ee e e notation des calculs dite parfois polonaise inverse 1 ou plus gnralement postxe. Les calculs e e
En hommage au philosophe et mathmaticien Jan Lukasiewicz qui a introduit cette notation dans les e annes 20. e
1

56

CHAPITRE III. PILES ET FILES

Fig. 2 Code de la simulation. static double simul(double probNewClient) { Fifo f = new Fifo () ; int nbClients = 0, nbFedUp = 0 ; int tFree = 0 ; // Le guichet est libre au temps tFree for (int t = 0 ; t < tMax ; t++) { // Tirer larrive dun client dans [t..t+1[ e i f (occurs(probNewClient)) { nbClients++ ; f.add(new Client(t)) ; } i f (tFree <= t) { // Le guichet est libre, servir un client (si il y en a encore un) while (!f.isEmpty()) { Client c = f.remove() ; i f (c.departure >= t) { // Le client est encore l` a tFree = t + uniform(serviceMin, serviceMax) ; break ; // Sortir de la boucle while } else { // Le client tait parti e nbFedUp++ ; } } } } return ((double)nbFedUp)/nbClients ; }

Fig. 3 Rsultat de la simulation de la le dattente e 0.8 ++ + 0.7 ++ ++ + + ++ + + + + + ++ + ++ + + ++ +++ ++ 0.6 ++ + +++ + ++ +++ + ++ 0.5 ++ + + ++ ++ + ++ + 0.4 + + + + + ++ + 0.3 ++ + + + ++ 0.2 + ++ + + ++ 0.1 + + + + ++ 0 ++ + + ++ + + ++ + ++ + + 0 0.005 0.01 0.015 0.02 0.025 Clients/sec

Non-servis/venus

` 1. A QUOI CA SERT ?

57

se font ` laide dune pile, les nombres sont simplement empils, et lexcution dune opration a e e e op revient ` dabord dpiler un premier oprateur e1 , puis un second e2 , et enn ` empiler le a e e a rsultat e2 op e1 . Par exemple, la gure 4 donne les tapes successives du calcul 6 3 2 - 1 + / e e 9 6 - * exprim en notation postxe. Le fabricant tats-unien de calculatrices arme quavec e e Fig. 4 Calcul de lexpression postxe 6 3 2 - 1 + / 9 6 - * 6 3 2 2 3 6 1 1 1 6 + / 9 6 6 9 3

3 6

1 6

2 6

9 3

3 3

un peu dhabitude la notation postxe est plus commode que la notation usuelle (dite inxe). Cest peut-tre vrai, mais on peut en tout cas tre sr que linterprtation de la notation postxe e e u e par une machine est bien plus facile ` raliser que celle de la notation inxe. En voici pour preuve a e e e le programme Calc qui ralise le calcul postxe donn sur la ligne de commande. Dans ce code les objets de la classe Stack sont des piles dentiers. class Calc { public static void main (String [] arg) { Stack stack = new Stack () ; for (int k = 0 ; k < arg.length ; k++) { String x = arg[k] ; i f (x.equals("+")) { int i1 = stack.pop(), i2 = stack.pop() ; stack.push(i2+i1) ; } else i f (x.equals("-")) { int i1 = stack.pop(), i2 = stack.pop() ; stack.push(i2-i1) ; ... /* Idem pour "*" et "/" */ ... } else { stack.push(Integer.parseInt(x)) ; } System.err.println(x + " -> " + stack) ; } System.out.println(stack.pop()) ; } } Le source contient des messages de diagnostic (sur System.err on ninsistera jamais assez), un essai2 nous donne (le sommet de pile est ` droite) : a % java Calc 6 3 2 - 1 + / 9 6 - * 6 -> [6] 3 -> [6, 3] . . .
2

Il faut citer le symbole * par exemple par *, an dviter son interprtation par le shell, voir VII.3.1. e e

58 6 -> [3, 9, 6] - -> [3, 3] * -> [9] 9

CHAPITRE III. PILES ET FILES

La facilit dinterprtation de lexpression postxe provient de ce que sa dnition est tr`s e e e e oprationnelle, elle dit exactement quoi faire. Dans le cas de la notation inxe, il faut rgler le e e probl`me des priorits des oprateurs. Par exemple si on veut eectuer le calcul inxe 1 + 2 * e e e 3 il faut procder ` la multiplication dabord (seconde opration), puis ` laddition (premi`re e a e a e opration) ; tandis que pour calculer 1 * 2 + 3, on eectue dabord la premi`re opration (mule e e tiplication) puis la seconde (addition). Par contraste, la notation postxe oblige lutilisateur de la calculatrice ` donner lordre dsir des oprations (comme par exemple 1 2 3 * + et 1 2 * 3 a e e e +) et donc simplie dautant le travail de la calculatrice. Dans le mme ordre dide, la notation e e postxe rend les parenth`ses inutiles (ou empche de les utiliser selon le point de vue). e e Exercice 2 Ecrire un programme Inx qui lit une expression sous-forme postxe et ache lexpression inxe (compl`tement parenthse) qui correspond au calcul eectu par le proe ee e gramme Calc. On supposera donne une classe Stack des piles de cha e nes. Solution. Il sut tout simplement de construire la reprsentation inxe au lieu de calculer. e class I n f i x { public static void main (String [] arg) { Stack stack = new Stack () ; for (int k = 0 ; k < arg.length ; k++) { String x = arg[k] ; i f (x.equals("+") || x.equals("-") || x.equals("*") || x.equals("/")) { String i1 = stack.pop(), i2 = stack.pop() ; stack.push("(" + i2 + x + i1 + ")") ; } else { stack.push(x) ; } System.err.println(x + " -> " + stack) ; } System.out.println(stack.pop()) ; } } Lemploi systmatiques des parenth`ses permet de produire des expressions inxes justes. Sans e e les parenth`ses 1 2 3 - - et 1 2 - 3 - conduiraient au mme rsultat 1 - 2 - 3 qui serait e e e incorrect pour 1 2 3 - -. Sur lexpression dj` donne, on obtient : ea e % java Infix 6 3 2 - 1 + / 9 6 - * 6 -> [6] 3 -> [6, 3] . . . 6 -> [(6/((3-2)+1)), 9, 6] - -> [(6/((3-2)+1)), (9-6)] * -> [((6/((3-2)+1))*(9-6))] ((6/((3-2)+1))*(9-6)) Nous verrons plus tard que enlever les parenth`ses inutiles ne se fait simplement qu` laide e a dune nouvelle notion.

2. IMPLEMENTATION DES PILES

59

Implmentation des piles e

Nous donnons dabord deux techniques dimplmentation possibles, avec un tableau et avec e une liste simplement cha ee. Ensuite, nous montrons comment utiliser la structure de pile toute n faite de la biblioth`que. e

2.1

Une pile dans un tableau

Cest la technique conceptuellement la plus simple, directement inspire par le dessin de e gauche de la gure 1. Plus prcisment, une pile (dentiers) contient un tableau dentiers, dont nous xons la taille e e arbitrairement ` une valeur constante. Seul le dbut du tableau (un segment initial) est utilis, a e e et un indice variable sp indique le pointeur de la pile. Le pointeur de pile indique la prochaine position libre dans la pile, mais aussi le nombre dlments prsents dans la pile. ee e 0 6 1 2 2 7 3 0 4 2 5 8 6 0 7 2 8 3 9 4 sp 5

Dans ce schma, seules les 5 premi`res valeurs du tableau sont signicatives. e e class Stack { final static int SIZE = 10; // assez grand ? private int sp ; private int [] t ; Stack () { // Construire une pile vide t = new int[SIZE] ; sp = 0; } . . . } e Notons que tous les champs des objets Stack sont privs (private, voir A.1.4). La taille des 0 1 2 3 4 5 6 7 8 9 10 piles est donne par une constante (nal), statique puisque quil ny a aucune raison de crer e e de nombreux exemplaires de cette constante. ` A la cration, une pile contient donc un tableau de SIZE = 10 entiers. La valeur initiale e contenue dans les cases du tableau est zro (voir A.3.6.2), mais cela na pas dimportance ici, e car aucune case nest valide (sp est nul). 0 0 1 0 2 0 3 0 4 0 5 0 6 0 7 0 8 0 9 0 sp 0

Il nous reste ` coder les trois mthodes des objets de la classe Stack pour respectivement, tester a e si la pile est vide, empiler et dpiler. e boolean isEmpty() { return sp == 0 ; } void push(int x) { i f (sp >= SIZE) throw new Error ("Push : pile pleine") ; t[sp++] = x ; // Ou bien : t[sp] = x ; sp = sp+1 ; }

10

60

CHAPITRE III. PILES ET FILES

int pop() { i f (isEmpty()) throw new Error ("Pop : pile vide") ; return t[--sp] ; // Ou bien : sp = sp-1 ; return t[sp] ; } La gure 5 donne un exemple dvolution du tableau t et du pointeur de pile sp. Le code cie Fig. 5 Empiler 9, puis dpiler. e 0 6 Empiler 9 0 6 Dpiler e 0 6 1 2 2 7 3 0 4 2 5 9 6 0 7 2 8 3 9 4 sp 5 1 2 2 7 3 0 4 2 5 9 6 0 7 2 8 3 9 4 sp 6 1 2 2 7 3 0 4 2 5 8 6 0 7 2 8 3 9 4 sp 5

dessus signale les erreurs en lanant lexception Error (voir A.4.2), et emploie les oprateurs c e de post-incrment et pr-dcrment (voir A.3.4). Notons surtout que toutes oprations sont en e e e e e temps constant, indpendant du nombre dlments contenus dans la pile, ce qui semble attendu e ee dans le cas doprations aussi2simples 4 5 6 et 7 8 9 10 e que push pop. 0 1 3 Les deux erreurs possibles "Push : pile pleine" et "Pop : pile vide" sont de natures bien direntes. En eet, la premi`re dcoule dune contrainte technique (la pile est insusame e e ment dimensionne), tandis que la seconde dcoule dune erreur de programmation de lutilisae e teur de la pile. Nous allons voir ce que nous pouvons faire au sujet de ces deux erreurs. 0 1 2 3 4 5 6 7 8 9 10 Commenons par lerreur "pile pleine". Une premi`re technique simple est de passer le c e bb ` lutilisateur, en lui permettant de dimensionner la pile lui-mme. e ea e class Stack { ... 0 6 8 Stack (int sz) { t 1 new int[sz] 5; sp =7 0 ; } 9 = 2 3 4

10

void push(int x) { i f (sp >= t.length) throw new Error ("Push : pile pleine") ; t[sp++] = x ; } ... } Ainsi, en cas de pile pleine, on peut toujours compter sur lutilisateur pour recommencer avec une pile plus grande. Cela para abusif, mais cest ` peu pr`s ce qui se passe au sujet de la pile t a e des appels de mthode. e Mais en fait, les tableaux de Java sont susamment exibles pour autoriser le redimensionnement automatique de la pile. Il sut dallouer un nouveau tableau plus grand, de recopier les lments de lancien tableau dans le nouveau, puis de remplacer lancien tableau par le nouveau ee (et dans cet ordre, cest plus simple).

2. IMPLEMENTATION DES PILES

61

private void resize() { int [] newT = new int [2 * this.t.length] ; // Allouer le nouveau tableau for (int k = 0 ; k < sp ; k++) // Recopier newT[k] = this.t[k] ; // Remplacer this.t = newT ; } void push(int x) { i f (sp >= t.length) resize() ; t[sp++] = x ; } Le redimensionnement automatique ore une exibilit maximale. En contrepartie, il a un cot : e u un appel ` push ne sexcute plus en temps garanti constant, puisquil peut arriver quun appel a e a ` push entra un redimensionnement dont le cot est manifestement proportionnel ` la taille ne u a de la pile. Mais vu globalement sur une excution compl`te, le cot de N appels ` push reste de lordre e e u a de N . On dira alors que le cot amorti de push est constant. En eet, considrons N oprations u e e push. Au pire, il ny a pas de pop et la pile contient nalement N lments pour une taille ee 2P du tableau interne, ou 2P est la plus petite puissance de 2 suprieure ` N . Pour atteindre e a cette taille, supposons que le tableau est redimensionn P fois (taille initiale 20 ). Lordre de e grandeur du cot cumul de tous les push est donc de lordre de N (cot constant de N push) u e u k=P k P +1 1 (co t des P redimensionnements). Soit un co t nal de lordre de N , u u plus k=1 2 = 2 puisque 2P < 2N . Pour ce qui est de la mmoire, on alloue au total de lordre de 2P +1 cellules de e mmoire, dont la moiti ont pu tre rcupres par le garbage collector. Notons nalement que le e e e e ee cot amorti constant est assur quand les tailles des tableaux successifs suivent une progression u e gomtrique, dont la raison nest pas forcment 2. En revanche, le cot amorti devient linaire e e e u e pour des tableaux les tailles suivent une progression arithmtique. Pour cette raison, crire e e int [] newT = new int [t.length + K] est rarement une bonne ide. e Abordons maintenant lerreur "pile vide". Ici, il ny a aucun moyen de supprimer lerreur, car rien nempchera jamais un programmeur maladroit ou fatigu de dpiler une pile vide. Mais e e e on peut signaler lerreur plus proprement, ce qui donne la possibilit au programmeur fautif de e la rparer. Pour ce faire, il convient de lancer, non plus lexception Error, mais une exception e spcique. Les exceptions sont compl`tement dcrites dans lannexe Java en A.4. Ici, nous nous e e e contentons dune prsentation minimale. La nouvelle exception StackEmpty se dnit ainsi. e e class StackEmpty extends Exception { } Cest en fait une dclaration de classe ordinaire, car une exception est un objet dune classe un e peu spciale (voir A.4.2). La nouvelle exception se lance ainsi. e int pop () throws StackEmpty { i f (isEmpty()) throw new StackEmpty () ; return t[--sp] ; } On note surtout que la mthode pop dclare (par throws, noter le s ) quelle peut lane e cer lexception StackEmpty. Cette dclaration est ici obligatoire, parce que StackEmpty est e une Exception et non plus une Error. La prsence de la dclaration va obliger lutilisateur des e e piles ` soccuper de lerreur possible. a Pour illustrer ce point revenons sur lexemple de la calculatrice Calc (voir 1.2). La calculatrice emploie une pile stack, elle dpile et empile directement par stack.pop() et stack.push(...). e e e Nous supposons que la classe Stack est telle que ci-dessus (avec une mthode pop qui dclare lancer StackEmpty) et que nous ne pouvons pas modier son code. Pour organiser un peu

62

CHAPITRE III. PILES ET FILES

la suite, nous ajoutons deux nouvelles mthodes statiques push et pop ` la classe Calc, et e a transformons les appels de mthode dynamiques en appels statiques. e class Calc { static int pop(Stack stack) { return stack.pop() ; } static void push(Stack stack, int x) { stack.push(x) ; } public static void main (String [] arg) { ... i f (x.equals("+")) { int i1 = pop(stack), i2 = pop(stack) ; push(stack, i2+i1) ; ... } } Le compilateur refuse le programme ainsi modi. Il exige le traitement de StackEmpty qui e peut tre lance par stack.pop(). Traiter lexception peut se faire en attrapant lexception, par e e linstruction try. static int pop(Stack stack) { try { return stack.pop() ; } catch (StackEmpty e) { return 0 ; } } Leet est ici que si linstruction return stack.pop() choue parce que lexception StackEmpty e est lance, cest alors linstruction return 0 qui sexcute. Cela revient ` remplacer la valeur e e a exceptionnelle StackEmpty par la valeur plus normale 0 (voir A.4.1 pour les dtails). Par e consquent, une pile vide se comporte comme si elle contenait une innit de zros. e e e On peut aussi dcider de ne pas traiter lexception, mais alors il faut signaler que les mthodes e e Calc.pop puis main peuvent lancer lexception StackEmpty, mme si cest indirectement. e class Calc { static int pop(Stack stack) throws StackEmpty { return stack.pop() ; } public static void main (String [] arg) throws StackEmpty { ... } } Si lexception survient, elle remontera de mthode en mthode et fera nalement chouer le e e e programme. En fait, comme les dclarations throws, ici obligatoires, sont quand mme pnibles, e e e on a plutt tendance ` faire chouer le programme explicitement d`s que lexception atteint le o a e e code que lon contrle. o static int pop(Stack stack) { try { return stack.pop() ; } catch (StackEmpty e) { System.err.println("Adieu : pop sur pile vide") ; System.exit(2) ; // Arr^ter le programme immdiatement (voir~A.6.1.5) e e return 0 ; // Le compilateur exige ce return jamais excut e e } }

2. IMPLEMENTATION DES PILES

63

Bien sr, si on veut que dpiler une pile vide provoque un arrt du programme, il aurait t u e e ee e plus court de lancer new Error ("Pop : pile vide") dans la mthode pop des objets Stack. Mais nous nous sommes interdit de modier la classe Stack.

2.2

Une pile dans une liste

Cest particuli`rement simple. En eet, les lments sont empils et dpils du mme cte e ee e e e e o de la pile, qui peut tre le dbut dune liste. On suppose donc donne une classe des listes e e e (dentiers). Et on crit : e class Stack { private List p ; Stack () { p = null ; } boolean isEmpty() { return p == null ; } void push(int x) { p = new List (x, p) ; } int pop() throws StackEmpty { i f (p == null) throw new StackEmpty () ; int r = p.val ; p = p.next ; return p ; } } Voici un exemple dvolution de la liste p. e p 2 Empiler 9 p 9 Dpiler e p 9 2 0 7 2 6 2 0 7 2 6 0 7 2 6

2.3

Les piles de la biblioth`que e

Il existe dj` une classe des piles dans la biblioth`que, la classe Stack du package java.util, ea e implmente selon la technique des tableaux redimensionns. Mais une classe des piles de quoi ? e e e En eet, dans les deux sections prcdentes, nous navons cod que des piles de int en e e e nous disant que pour coder une pile, par exemple de String, il nous susait de remplacer int par String partout dans le code. Il est clair quune telle solution ne convient pas ` une a classe de biblioth`que qui est compile par le fabricant. Apr`s bien des hsitations, Java a rsolu e e e e e ce probl`me en proposant des classes gnriques, ou paramtres. La classe Stack est en fait une e e e e e classe Stack<E>, o` E est nimporte quelle classe, et les objets de la classe Stack<E> sont des u

64

CHAPITRE III. PILES ET FILES

piles dobjets de la classe E. Formellement, Stack nest donc pas exactement une classe, mais plutt une fonction des classes dans les classes. Informellement, on peut considrer que nous o e disposons dune innit de classes Stack<String>, Stack< List > etc. Par exemple on code facie lement la pile de cha nes de lexercice 2 comme un objet de la classe java.util.Stack<String>. import java.util.* ; class I n f i x { public static void main (String [] arg) { Stack<String> stack = new Stack<String> () ; ... String i1 = stack.pop(), i2 = stack.pop() ; stack.push("(" + i2 + x + i1 + ")") ; ... } } (Pour import, voir A.3.5.) Cela fonctionne parce que la classe Stack<E> poss`de bien des e mthodes pop et push, o` par exemple pop() renvoie un objet E, ici un String. e u Comme le param`tre E dans Stack<E> est une classe, il nest pas possible de fabriquer des e piles de int . Plus gnralement, il est impossible de fabriquer des piles de scalaires (voir A.3.1 e e pour la dnition des scalaires). Mais la biblioth`que fournit une classe associe par type scae e e laire, par exemple Integer pour int . Un objet Integer nest gu`re plus quun objet dont une e variable dinstance (prive) contient un int (voir A.6.1.1). Il existe deux mthodes pour convere e tir un scalaire int en un objet Integer et rciproquement, valueOf (logiquement statique) e et intValue (logiquement dynamique). De sorte qucrire la calculatrice Calc (voir 1.2) avec e une pile de la biblioth`que semble assez lourd au premier abord. e import java.util.* ; class Calc { public static void main (String [] arg) { Stack<Integer> stack = new Stack<Integer> () ; ... int i1 = stack.pop().intValue(), i2 = stack.pop().intValue() ; stack.push(Integer.valueOf(i2+i1)) ; ... } } Mais en fait, le compilateur Java sait insrer les appels aux mthodes de conversion automatie e quement, cest-`-dire que lon peut crire plus directement. a e import java.util.* ; class Calc { public static void main (String [] arg) { Stack<Integer> stack = new Stack<Integer> () ; ... int i1 = stack.pop(), i2 = stack.pop() ; stack.push(i2+i1) ; ... } } Tout semble donc se passer presque comme si il y avait une pile de int ; mais attention, il sagit bien en fait dune pile de Integer. Tout se passe plutt comme si le compilateur traduisait o le programme avec les int vers le programme avec les Integer, et cest bien ce dernier qui sexcute, avec les consquences prvisibles sur la performance en gnral et la consommation e e e e e mmoire en particulier. e

3. IMPLEMENTATION DES FILES

65

Implmentation des les e

Comme pour les piles, nous prsentons trois techniques, tableaux, listes et biblioth`que. e e

3.1

Une le dans un tableau

Lide, conceptuellement simple mais un peu dlicate ` programmer, est de grer deux ine e a e dices : in qui marque la position o` ajouter le prochain lment et out qui marque la position u ee do` proviendra le prochain lment enlev. Lindice out marque le dbut de la le et in sa u ee e e n. Autrement dit, le contenu de la le va la case dindice out ` la case qui prc`de la case a e e dindice in. 0 1 2 3 4 5 6 7 8 9 6 2 7 0 9 8 2 2 0 4

1 out (dbut) e

5 in (n)

Dans le schma ci-dessus, les cases valides sont grises, de sorte que la le ci-dessus contient e e les entiers 2, 7, 0, 9 (dans lordre, 2 est le premier entr et 9 le dernier entr). Au cours de la e e vie de la le, les deux indices sont croissants. Plus prcisment, on incrmente in apr`s ajout e e e e et on incrmente out apr`s suppression. Lorsquun indice atteint la n du tableau, il fait tout e e simplement le tour du tableau et repart ` zro. Il en rsulte que lon peut avoir out < in. Par a e e exemple, voici une autre le contenant cette fois 2, 0, 4, 6, 2, 7. 0 1 2 3 4 5 6 7 8 9 6
0

2
1

7
2

0
33

9
4

8
5

2
6

2
77

0
8

4
9 10

in (n)

out (dbut) e

Le tableau dune le est un tableau circulaire, que lon parcourt en incrmentant un indice e modulo n, o` n est la taille du tableau. Par exemple pour parcourir le contenu de la le, on u parcourt les indice de out ` (in 1) mod n. Soit un parcours de 1 ` 4 dans le premier exemple a a et de 7 ` 2 dans le second. a Une derni`re dicult est de distinguer entre le vide et le pleine. Comme le montre le e e schma suivant, les deux indices out et in ny susent pas. e 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 1 6 2 7 0 9 8 2 2 0 4 6 2 7 0 9 8 2 2 0 4 0 4 4 0 in 1 out 4
2 3 4 5 6 7 8 9 10out

4 in

Ici, out (dbut) et in (n) valent tous deux 4. Pour conna le contenu de la le il faut donc e tre parcourir le tableau de lindice 4 ` lindice 3. Une premi`re interprtation du parcours donne a e e une le vide, et une seconde une le pleine. On rsout facilement la dicult par une variable nb e e supplmentaire, qui contient le nombre dlments contenus dans la le. Une autre solution aurait e ee t de dcrter quune le qui contient n 1 lments est pleine. On teste en eet cette derni`re ee e e ee e condition sans ambigu e par in + 1 congru ` out modulo n. Nous choisissons la solution de la t a variable nb, car conna simplement le nombre dlments de la le est pratique, notamment tre ee 2 3 4 5 6 7 0 1 2 3 4 5 6 7 8 9 10 pour grer le1redimensionnement.8 9 10 e 0

66

CHAPITRE III. PILES ET FILES

Fig. 6 Implmentation dune le dans un tableau e class FifoEmpty extends Exception { } class Fifo { final static int SIZE=10 ; private int in, out, nb ; private int [] t ; Fifo () { t = new int[SIZE] ; in = out = nb = 0 ; } /* Increment modulo la taille du tableau t, utilis partout */ e private int incr(int i) { return (i+1) % t.length ; } boolean isEmpty() { return nb == 0 ; } int remove() throws FifoEmpty { i f (isEmpty()) throw new FifoEmpty () ; int r = t[out] ; out = incr(out) ; nb-- ; // Effectivement enlever return r ; } void add(int x) { i f (nb+1 >= t.length) resize() ; t[in] = x ; in = incr(in) ; nb++ ; // Effectivement ajouter } private void resize() { int [] newT = new int[2*t.length] ; // Allouer int i = out ; // indice du parcours de t for (int k = 0 ; k < nb ; k++) { // Copier newT[k] = t[i] ; i = incr(i) ; } // Remplacer t = newT ; out = 0 ; in = nb ; } /* Mthode toString, donne un exemple de parcours de la file */ e public String toString() { StringBuilder b = new StringBuilder () ; b.append("[") ; i f (nb > 0) { int i = out ; b.append(t[i]) ; i = incr(i) ; for ( ; i != in ; i = incr(i)) b.append(", " + t[i]) ; } b.append("]") ; return b.toString() ; } }

3. IMPLEMENTATION DES FILES

67

e e La gure 6 donne la classe Fifo des les implmentes par un tableau, avec redimensionnement automatique. Un point cl de ce code est la gestion des indices in et out. Pour ajouter un e lment dans la le, in est incrment (modulo n). Par exemple, voici lajout (mthode add) de ee e e e lentier 11 dans une le. 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 6 2 7 0 9 8 2 2 0 4 6 2 7 11 9 8 2 2 0 4 3 in 7 out 4 in 7 out

Pour supprimer un lment, on incrmente out. Par exemple, voici la suppression du premier ee e lment dune le (la mthode remove renvoie ici 2). ee e 0 1 2 3 4 5 6 7 8 9 6 2 7 11 9 8 2 2 0 4 4 in 7 out 0 1 2 3 4 5 6 7 8 9 6 2 7 11 9 8 2 2 0 4 4 in 8 out

Le redimensionnement (mthode resize) a pour eet de tasser les lments au dbut du nouveau e ee e tableau. Voici lexemple de lajout de 13 dans une le pleine de taille 4. 0 1 2 3 6 2 7 11 2 in 2 out 0 1 2 3 4 5 6 7 7 11 6 2 13 0 0 0 0 out 5 in

Avant ajout la le contient 7, 11, 6, 2, apr`s ajout elle contient 7, 11, 6, 2, 13. e Le code du redimensionnement (mthode resize) est un peu simpli par la prsence de e e e la variable nb. Pour copier les lments de t dans newT, le contrle est assur par une simple ee o e boucle sur le compte des lments transfrs. Un contrle sur lindice i dans le tableau t serait ee ee o plus dlicat. La mthode toString donne un exemple de ce style de parcours du contenu de e e la le, entre les indices out (inclus) et in (exclu), qui doit particulariser le cas dune le vide et eectuer la comparaison ` in ` partir du second lment parcouru (` cause du cas de la le a a ee a pleine).

3.2

Une le dans une liste

Limplmentation est conceptuellement simple. Les lments de la le sont dans une liste, on e ee enl`ve au dbut de la liste et on ajoute ` la n de la liste. Pour garantir des oprations en temps e e a e constant, on utilise une rfrence sur la derni`re cellule de liste. Nous avons dj` largement ee e ea exploit cette ide, par exemple pour copier une liste itrativement (voir lexercice I.2). Nous e e e nous donnons donc une rfrence out sur la premi`re cellule de la liste, et une rfrence in sur ee e ee la derni`re cellule. e out 2 7 0 in 9

La le ci-dessus contient donc les entiers 2, 7, 0, 9 dans cette ordre. Une le vide est logiquement identie par une variable out valant null . Par souci de cohrence in vaudra alors aussi null . e e

68 class Fifo { private List out, in ; Fifo () { out = in = null ; } boolean isEmpty() { return out == null ; }

CHAPITRE III. PILES ET FILES

int remove() throws FifoEmpty { i f (isEmpty()) throw new FifoEmpty () ; int r = out.val ; out = out.next ; i f (out == null) in = null ; // La file est vide return r ; } void add(int x) { List last = in ; in = new List (x, null) ; e i f (last == null) { // La file tait vide out = in ; } else { last.next = in ; } } } On constate que le code est bien plus simple que dans le cas des tableaux (gure 6). La mthode remove est essentiellement la mme que la mthode pop des piles (voir 2.2), car les deux e e e mthodes ralisent la mme opration denlever le premier lment dune liste. La mthode add e e e e ee e est ` peine plus technique, voici par exemple la suite des tats mmoire quand on ajoute 3 ` la a e e a le dj` dessine. La premi`re instruction List last = in garde une copie de la rfrence in. ea e e ee out 2 7 0 last Linstruction suivante in = new List (x, null ) alloue la nouvelle cellule de liste. out 2 7 0 last Et enn, linstruction last.next = in, ajoute eectivement la nouvelle cellule pointe par in e a ` la n de la liste-le. out 2 7 0 last 9 in 3 9 in 3 in 9

3. IMPLEMENTATION DES FILES

69

3.3

Les les de la biblioth`que e

Le package java.util de la biblioth`que ore plusieurs classes qui peuvent servir comme e une le, mme si nous ne respectons pas trop lesprit dans lequel ces classes sont organises. (Les e e les de la biblioth`que Queue ne sont pas franchement compatibles avec notre mod`le simple e e des les). Nous nous focalisons sur la classe LinkedList. Comme le nom lindique ` peu pr`s, cette a e classe est implmente par une liste cha ee. Elle ore en fait bien plus que la simple fonctione e n nalit de le, mais elle poss`de les mthodes add et remove des les. En fait, si vous lisez la e e e documentation, vous verrez que add ajoute un lment en n de liste, tandis que remove enl`ve ee e le premier lment de la liste. Disons le, ca fait toujours une le. La classe LinkedList est ee gnrique, comme la classe Stack de la section 2.3, ce qui la rend relativement simple demploi. e e Exercice 3 Les objets LinkedList implmentent en fait les queues ` deux bouts (double ene a ded queue), qui orent deux couples de mthodes addFirst/removeFirst et addLast/removeLast e pour ajouter/enlever respectivement au dbut et ` la n de queue. An dassurer des oprations e a e en temps constant, la classe de biblioth`que repose sur les listes doublement cha ees. Voici une e n n classe DeList des cellules de liste doublement cha ee. class DeList { int val ; DeList next, prev ; DeList (int val, DeList next, DeList prev) { this.val = val ; this.next = next ; this.prev = prev ; } } Le cha nage double permet de parcourir la liste de la premi`re cellule ` la derni`re en suivant e a e les champs next, et de la derni`re ` la premi`re en suivant le champ prev. e a e fst 2 0 7 11 lst

Autrement dit, si p est une rfrence vers une cellule de liste doublement cha ee qui nest pas la ee n premi`re, on a lgalit p.prev.next == p ; et si p pointe vers la premi`re cellule, on a p.prev e e e e == null. De mme, si p pointe vers une cellule qui nest pas la derni`re, on a p.next.prev == e e p ; et si p pointe vers la derni`re cellule, on a p.next == null. e Solution. Dcomposons une opration, par exemple addFirst, en ajoutant 3 au dbut de la e e e queue dj` dessine. Dans un premier temps on peut allouer la nouvelle cellule, avec un champ ea e next correct (voir push en 2.2). Le champ prev peut aussi tre initialis immdiatement ` null . e e e a fst 3 2 0 7 11 lst

Reste ensuite ` ajuster le champ prev de la nouvelle seconde cellule de la liste, cellule accessible a par fst.next ((fst.next).prev = fst). fst 3 2 0 7 11 lst

70

CHAPITRE III. PILES ET FILES

Fig. 7 Classe des queues ` deux bouts a class DeQueue { private DeList fst, lst ; // First and last cell DeQueue () { fst = lst = null ; } boolean isEmpty() { return fst == null ; } void addFirst(int x) { fst = new DeList(x, fst, null) ; i f (lst == null) { lst = fst ; } else { fst.next.prev = fst ; } } void addLast(int x) { lst = new DeList(x, null, lst) ; i f (fst == null) { fst = lst ; } else { lst.prev.next = lst ; } } int removeFirst() { i f (fst == null) throw new Error ("removeFirst: empty queue") ; int r = fst.val ; fst = fst.next ; i f (fst == null) { lst = null ; } else { fst.prev = null ; } return r ; } int removeLast() { i f (lst == null) throw new Error ("removeLast: empty queue") ; int r = lst.val ; lst = lst.prev ; i f (lst == null) { fst = null ; } else { lst.next = null ; } return r ; } }

4. TYPE ABSTRAIT, CHOIX DE LIMPLEMENTATION

71

Pour supprimer par exemple le dernier lment de la queue, il sut de changer le contenu de ee lst (lst = lst.prev). 3 2 0 7 11

fst

lst

Reste ensuite ` ajuster le champ next de la nouvelle derni`re cellule (lst.next = null ). a e 3 2 0 7 11

fst

lst

Le code, pas si dicile est donn par la gure 7. Une fois compris le mcanisme dajout et de e e retrait des cellules, le point dlicat est de bien grer le cas de la queue vide qui correspond ` e e a fst et lst tous deux gaux ` null . e a

Type abstrait, choix de limplmentation e

Nous avons prsent trois faons dimplmenter piles et les. Il est naturel de se demander e e c e quelle implmentation choisir. Mais nous tenons dabord a faire remarquer que, du strict point e ` de vue de la fonctionnalit, la question ne se pose pas. Tout ce qui compte est davoir une pile e (ou une le). En eet, les trois implmentations des piles orent exactement le mme service, e e de sorte que le choix dune implmentation particuli`re na aucun impact sur le rsultat dun e e e programme qui utilise une pile. La pile qui est dnie exclusivement par les services quelle ore e est un exemple de type de donnes abstrait. e Java ore des traits qui permettent de fabriquer des types abstraits qui prot`gent leur e implmentation des interventions intempestives. Dans nos deux classes Stack, le tableau (ainsi e que le pointeur de pile sp) et la liste sont des champs privs. Un programme qui utilise nos piles e doit donc le faire exclusivement par lintermdiaire des mthodes push et pop. Champs privs e e e et mthodes accessibles correspondent directement ` la notion de type abstrait dni par les e a e services oerts. Il faut toutefois remarquer que, du point de vue du langage de programmation, lexistence des deux (trois en fait) mthodes reste pour le moment une convention : nous avons convene tionnellement appel Stack la classe des piles et conventionnellement admis quelle poss`de ces e e trois mthodes : e boolean isEmpty() { . . . } push(int x) { . . . } int pop() throws StackEmpty { . . . } Il en rsulte par exemple que nous ne pouvons pas encore mlanger les piles en tableau et les e e piles en liste dans le mme programme, ` moins de changer le nom des classes ce qui contredirait e a lide dun type abstrait. Nous verrons au chapitre suivant comment procder. e e Rpondons maintenant srieusement ` la question du choix de limplmentation. Dans disons e e a e 99 % des cas pour les les et 90 % des cas pour les piles, il faut choisir la classe de biblioth`que, e parce que cest la solution qui demande dcrire le moins de code. La dirence de pourcentage e e sexplique en comparant le temps de lecture de la documentation au temps dcriture dune e classe des les ou des piles, et parce que la classe des piles est quand mme simple ` crire. e ae

72

CHAPITRE III. PILES ET FILES

La biblioth`que peut ne pas convenir pour des raisons decacit : le code est trop lent ou e e trop gourmand en mmoire pour notre programme particulier et nous pouvons faire mieux que e lui. Par exemple, le code de biblioth`que entra la fabrication dobjets Integer en pagaille, et e ne une pile de scalaires se montre au nal plus rapide. La classe de biblioth`que peut aussi ne pas e convenir parce quelle nore pas la fonctionnalit indite dont nous avons absolument besoin (par e e exemple une inversion compl`te de la pile), ou plus frquemment parce que programmer cette e e fonctionnalit de la faon autorise par la structuration de la biblioth`que serait trop compliqu, e c e e e trop coteux, ou tout simplement impossible avec notre connaissance limite de Java. u e Il reste alors ` choisir entre tableaux et listes. Il ny a pas de rponse toute faite ` cette a e a derni`re question. En eet, dune part, la dicult de lcriture dun programme dpend aussi e e e e du got et de lexprience de chacun ; et dautre part, lecacit respective de lune ou de lautre u e e technique dpend de nombreux facteurs, (la pile cro e t-elle beaucoup, par exemple) et aussi de lecacit de la gestion de la mmoire par le syst`me dexcution de Java. Toutefois, dans le cas e e e e o` le redimensionnement est inutile, le choix dune pile-tableau simpose probablement, puisque u simplicit et ecacit concordent. Dans le cas gnral, on peut tout de mme prvoir quutiliser e e e e e e les listes demande dcrire un peu moins de code que dutiliser les tableaux (redimensionns). On e e peut aussi penser que les tableaux seront un plus ecaces que les listes, ou en tout cas utiliseront moins de mmoire au nal. En eet, fabriquer une pile-tableau de N lments demande dallouer e ee log2 N objets (tableaux) contre N objets (cellules de listes) pour une pile-liste. Or, allouer un objet est une opration ch`re. e e Supposons quun objet occupe deux cases de mmoire en plus de lespace ncessaire pour ses e e donnes (cest une hypoth`se assez probable pour Java). Une cellule de liste occupe donc 4 cases e e et un tableau de taille n occupe 3+n cases (dont une case pour la longueur). Pour un programme eectuant au total P push, la pile-liste aura allou au total 4 P cases de mmoire, tandis que la e e pile-tableau aura allou en entre une (si la taille initiale du tableau est 1, et que le programme e alterne push et pop) et environ 4 P + 3 log2 P cases de mmoire (dans le cas o` aucun pop ne e u spare les push et o` le tableau est redimensionn par le dernier push). La pile-tableau nalloue e u e donc jamais signicativement plus de mmoire que la pile-liste, et gnralement plutt moins. e e e o Un autre cot intressant est lempreinte mmoire, la quantit de mmoire mobilise ` un u e e e e e a instant donn. Une pile-le de N lments mobilise 4 N cases de mmoire. Une pile-tableau e ee e mobilise entre 3+N et un nombre arbitrairement grand de cases de mmoire (le nombre arbitraire e correspond ` la taille du tableau lorsque, dans le pass, la pile a atteint sa taille maximale). Un a e nouvel lment intervient donc dans le choix de limplmentation : la profondeur maximale de ee e pile au cours de la vie du programme. Si cette profondeur reste raisonnable le choix de la piletableau simpose, autrement le choix est moins net, mais la exibilit de lallocation petit-`-petit e a des cellules de la pile-liste est un avantage. On peut aussi envisager de rduire la taille du tableau e interne de la pile tableau, par exemple en rduisant le tableau de la moiti de sa taille quand e e il nest plus quau quart plein. Mais il faut alors y regarder ` deux fois, notre classe des piles a commence ` devenir complique est tout ce code ajout entra un prix en temps dexcution. a e e ne e

Chapitre IV

Associations Tables de hachage


Ce chapitre aborde un probl`me tr`s frquemment rencontr en informatique : la recherche e e e e dinformation dans un ensemble gr de faon dynamique. Nous nous plaons dans le cadre o` ee c c u une information compl`te se retrouve normalement ` laide dune cl qui lidentie. La notion se e a e rencontre dans la vie de tous les jours, un exemple typique est lannuaire tlphonique : conna ee tre le nom et les prnoms dun individu sut normalement pour retrouver son numro de tlphone. e e ee En cas dhomonymie absolue (tout de mme rare), on arrive toujours ` se dbrouiller. Dans les e a e socits modernes qui refusent le ou (et dans les ordinateurs) la cl doit identier un individu ee e unique, do`, par exemple, lide du numro de scurit sociale, u e e e e Nous voulons un ensemble dynamique dinformations, cest-`-dire aussi pouvoir ajouter ou a supprimer un lment dinformation. On en vient naturellement ` dnir un type abstrait de ee a e donnes, appel table dassociation qui ore les oprations suivantes. e e e Trouver linformation associ ` une cl donne. ea e e Ajouter une nouvelle association entre une cl et une information. e Retirer une cl de la table (avec linformation associe). e e La seconde opration mrite dtre dtaille. Lors de lajout dune paire cl-information, nous e e e e e e prcisons : e Sil existe dj` une information associe ` la cl dans la table, alors la nouvelle information ea e a e remplace lancienne. Sinon, une nouvelle association est ajoute ` la table. e a Il en rsulte quil ny a jamais dans la table deux informations distinctes associes ` la mme e e a e cl. e Il nest pas trop dicile denvisager la possibilit inverse, en ajoutant une nouvelle association e a ` la table dans tous les cas, que la cl sy trouve dj` ou pas. Il faut alors rviser un peu les deux e ea e autres oprations, an didentier linformation concerne parmi les ventuellement multiples qui e e e sont associes ` une mme cl. En gnral, on choisit linformation la plus rcemment entre, ce e a e e e e e e qui revient ` un comportement de pile. a Enn, il peut se faire que la cl fasse partie de linformation, comme cest gnralement le e e e cas dans une base de donnes. Dans ce cas appelons enregistrement un lment dinformation. e ee Un enregistrement est compos de plusieurs champs (nom, prnom, numro de scurit sociale, e e e e e sexe, date de naissance etc.) dont un certain nombre peuvent servir de cl. Il ny a l` aucune e a dicult du moins en thorie. Il se peut aussi que linformation se rduise ` la cl, dans ce cas e e e a e la table dassociation se rduit ` lensemble (car il ny a pas de doublons). e a

Statistique des mots

Nous illustrons lintrt de la table dassociation par un exemple ludique : un programme Freq ee qui compte le nombre doccurrences des mots dun texte, dans le but de produire dintressantes e 73

74

CHAPITRE IV. ASSOCIATIONS TABLES DE HACHAGE

statistiques. Soit un texte, par exemple notre hymne national, nous aurons % java Freq marseillaise.txt nous: 10 vous: 8 franais: 7 c leur: 7 dans: 6 libert: 6 e ...

1.1

Rsolution par une table dassociation e

Le probl`me ` rsoudre se dcompose naturellement en trois : e a e e (1) Lire un chier texte mot ` mot. a (2) Compter les occurrences des mots. (3) Produire un bel achage, par exemple prsenter les mots dans lordre dcroissant du e e nombre de leurs occurrences. Supposons les premier et troisi`me points rsolus. Le troisi`me par un tri et le premier par une e e e e e classe WordReader des ux de mots. Les objets de cette classe poss`dent une mthode read qui renvoie le mot suivant du texte (un String) ou null ` la n du texte. a Une table dassociation permet de rsoudre facilement le deuxi`me point : il sut dassocier e e ` mots et entiers. Un mot qui nest pas dans la table est conventionnellement associ ` zro. A ea e chaque mot lu m on retrouve lentier i associ ` m, puis on change lassociation en m associ ea e a ` i + 1. Nous pourrions spcier en franais une version rane de table dassociation (pas de e c e suppression, information valant zro par dfaut). Nous prfrons le faire directement en Java en e e ee dnissant une interface Assoc. e interface Assoc { /* Crer/remplacer lassociation key val */ e void put(String key, int val) ; /* Trouver lentier associ ` key, ou zro. */ e a e int get(String key) ; } La dnition dinterface ressemble ` une dnition de classe, mais sans le code des mthodes. e a e e Cette absence nempche nullement demployer une interface comme un type, nous pouvons e donc parfaitement crire une mthode count, qui compte les occurrences des mots du texte e e dans un Assoc t pass en argument, sans que la classe exacte de t soit connue1 . e static void count(WordReader in, Assoc t) { for (String word = in.read() ; word != null ; word = in.read()) { // Retenir les mots suffisamment longs i f (word.length() >= 4) { word = word.toLowerCase() ; // Minusculer le mot t.put(word, t.get(word)+1) ; } } } (Voir A.6.1.3 pour les deux mthodes des String employes). Un fois remplie, la table est e e simplement ache sur la sortie standard. e
1

e e Vous noterez la dirence avec les classes Fifo et Stack du chapitre prcdent e

1. STATISTIQUE DES MOTS Assoc t = . . . ; WordReader in = . . . ; count(in, t) ; System.out.println(t.toString()) ;

75

Les points de la cration du WordReader et du tri de la table dans sa mthode toString sont e e supposs rsolus, nous estimerons donc le programme Freq crit d`s que nous aurons implment e e e e e e la table dassociation ncessaire. e

1.2

Implmentation simple de la table dassociation e

La technique dimplmentation la plus simple dune table dassociations est denvisager une e liste des paires cl-information. Dans lexemple de la classe Freq, les cls sont des cha e e nes et les informations des entiers, nous dnissons donc une classe simple des cellules de listes. e class AList { String key ; int val ; AList next ; AList (String key, int val, List next) { this.key = key ; this.val = val ; this.next = next ; } } Nous aurions pu dnir dabord une classe des paires, puis une classe des listes de paires, mais e nous prfrons la solution plus conome en mmoire qui consiste ` ranger les deux composantes ee e e a de la paire directement dans la cellule de liste. Nous dotons la classe AList dune unique mthode (statique, null tant une liste valide) e e destine ` retrouver une paire cl-information ` partir de sa cl. e a e a e static AList getCell(String key, AList p) { for ( ; p != null ; p = p.next) i f (key.equals(p.key)) return p ; return null ; } La mthode getCell renvoie la premi`re cellule de la liste p dont le champ vaut la cl passe en e e e e argument. Si cette cellule nexiste pas, null est renvoy. Notons que les cha e nes sont compares e par la mthode equals et non pas par loprateur ==, cest plus prudent (voir A.3.1.1). Cette e e mthode getCell sut pour crire la classe L des tables dassociations base sur une liste e e e dassociation interne. class L implements Assoc { private AList p ; L() { p = null ; } public int get(String key) { AList r = AList.getCell(key, p) ; i f (r == null) { // Absent de la table return 0 ; // Prsent dans la table e } else { return r.val ; } }

76

CHAPITRE IV. ASSOCIATIONS TABLES DE HACHAGE public void put(String key, int val) { AList r = AList.getCell(key, p) ; e i f (r == null) { // Absent de la table, crer une nouvelle association p = new AList (key, val, p) ; e } else { // Prsent dans la table, modifier lancienne association r.val = val ; } }

} e Les objets L encapsulent une liste dassociation. Les mthodes get et put emploient toutes les deux la mthode AList .getCell pour retrouver linformation associe ` la cl key passe en e e a e e argument. La technique de lencapsulage est dsormais famili`re, nous lavons dj` exploite e e ea e pour les ensembles, les piles et les les dans les chapitres prcdents. e e Mais il faut surtout noter une nouveaut : la classe L dclare implmenter linterface Assoc e e e (mot-cl implements). Cette dclaration entra deux consquences importantes. e e ne e Le compilateur Java vrie que les objets de la classe L poss`dent bien les deux mthodes e e e e e spcies par linterface Assoc, avec les signatures conformes. Il y a un dtail trange : les e e mthodes spcies par une interface sont obligatoirement public. e e e Un objet L peut prendre le type Assoc, ce qui arrive par exemple dans lappel suivant : // in est un WordReader count(in, new L()) ; // Compter les mots de in. Notez que count ne conna pas la classe L, seulement linterface Assoc. t Notre programme Freq fonctionne, mais il nest pas tr`s ecace. En eet si la liste dassociation e est de taille N , une recherche par getCell peut prendre de lordre de N oprations, en particulier e dans le cas frquent o` la cl nest pas dans la liste. Il en rsulte que le programme Freq est en e u e e O(n2 ) o` n est le nombre de mots de lentre. Pour atteindre une ecacit bien meilleure, nous u e e allons introduire la nouvelle notion de table de hachage.

Table de hachage

La table de hachage est une implmentation ecace de la table dassociation. Appelons e univers des cls lensemble U de toutes les cls possibles. Nous allons dabord observer quil existe e e un cas particulier simple quand lunivers des cls est un petit intervalle entier, puis ramener le e cas gnral ` ce cas simple. e e a

2.1

Adressage direct

Cette technique tr`s ecace ne peut malheureusement sappliquer que dans des cas tr`s e e particuliers. Il faut que lunivers des cls soit de la forme {0, . . . , n 1}, o` n est un entier pas e u trop grand, et dautre part que deux lments distincts aient des cls distinctes (ce que nous ee e avons dailleurs dj` suppos). Il sut alors dutiliser un tableau de taille n pour reprsenter la ea e e table dassociation. Ce cas sapplique par exemple ` la base de donne des concurrents dune preuve sportive a e e qui exclut les ex-quos. Le rang ` larrive dun concurrent peut servir de cl dans la base de a e e donnes. Mais, ne nous leurrons pas un cas aussi simple est exceptionnel en pratique. Le fait e pertinent est de remarquer que le probl`me de la recherche dinformation se simplie beaucoup e quand les cls sont des entiers pris dans un petit intervalle. e

2. TABLE DE HACHAGE

77

2.2

Table de hachage

Lide fondamentale de la table de hachage est de se ramener au cas de ladressage direct, e cest-`-dire de cls qui sont des indices dans un tableau. Soit m, entier pas trop grand, ` vrai a e a dire entier de lordre du nombre dlments dinformations que lon compte grer. On se donne ee e une fonction h : U {0, . . . , m 1} appele fonction de hachage. Lide est de ranger llment de cl k non pas dans une case de e e ee e tableau t[k], comme dans ladressage direct (cela na dailleurs aucun sens si k nest pas un entier) , mais dans t[h(k)]. Nous reviendrons en 3 sur le choix, relativement dlicat, de la e fonction de hachage. Mais nous devons aronter d`s ` prsent une dicult. En eet, il devient e a e e draisonnable dexclure le cas de cls (distinctes) k et k telles que h(k) = h(k ). La gure 1 e e illustre la survenue dune telle collision entre les cls k1 et k3 distinctes qui sont telles que e h(k1 ) = h(k3 ). Prcisons un peu le probl`me, supposons que la collision survient lors de lajout e e Fig. 1 Une collision dans une table de hachage.
10 9 8 7 6 5 4 3 2 1 0 0

0 h(k2 )

Univers U des cls e


k2 k1 k3 k0

h(k1 ) = h(k3 )

h(k0 ) m1
1

de llment dinformation v3 de cl k3 , alors quil existe dj` dans la table une cl k1 avec ee e ea e h(k1 ) = h(k3 ). La question est alors : o` ranger linformation associe ` la cl k3 ? u e a e

2.3

Rsolution des collisions par cha e nage

La solution la plus simple pour rsoudre les collisions consiste ` mettre tous les lments dine a ee formation dont les cls ont mme valeur de hachage dans une liste. On parle alors de rsolution e e e des collisions par cha nage. Dans le cas de lexemple de collision de la gure 1, on obtient la situation de la gure 2. On remarque que les lments de la table t sont tout btement des listes ee e dassociation, la liste t[i] regroupant tous les lments dinformation (k, v) de la table qui sont ee tels que h(k) vaut lindice i. Nous proposons une nouvelle implmentation H des tables dassociations Assoc, en encape sulant cette fois une table de hachage. class H implements Assoc { final static int SIZE=1024 ; // Assez grand ? private AList [] t ; H() { t = new AList [SIZE] } ; private int hash(String key) { return Math.abs(key.hashCode()) % t.length ;}

78

CHAPITRE IV. ASSOCIATIONS TABLES DE HACHAGE Fig. 2 Rsolution des collisions par cha e nage.
10 9 8 7 6 5 4 3 2 1 0 0 1

k2 v2

Univers U des cls e


k2 k1 k3 k0

k3 v3

k1 v1

k0 v0

public int get(String key) { int h = hash(key) ; AList r = AList.getCell(key, t[h]) ; i f (r == null) { return 0 ; } else { return r.val ; } } public void put(String key, int val) { int h = hash(key) ; AList r = AList.getCell(key, t[h]) ; i f (r == null) { t[h] = new AList(key, val, t[h]) ; } else { r.val = val ; } } } Le code de la fonction de hachage hash est en fait assez simple, parce quil utilise la mthode e de hachage des cha nes fournie par Java (toute la complexit est cache dans cette mthode) e e e dont il rduit le rsultat modulo la taille du tableau interne t, an de produire un indice valide. e e La valeur absolue Math.abs est malheureusement ncessaire, car pour n ngatif, loprateur e e e reste de la division euclidienne % renvoie un rsultat ngatif (mis`re). e e e Il est surtout important de remarquer : Le code est en fait presque le mme que celui de la classe L (page 75), en remplaant p e c par t[h]. La classe H dclare implmenter linterface Assoc et le fait eectivement ce que le come e pilateur vrie. Un objet de la nouvelle classe H est donc un argument valide pour la e mthode count de la classe Freq. e Estimons le cot de put et de get pour une table qui contient N lments dinformation. On u ee suppose que le cot du calcul de la fonction hachage est en O(1), et que hachage est uniforme, u cest-`-dire que la valeur de hachage dune cl vaut h [0 . . . m[ avec une probabilit 1/m. a e e

2. TABLE DE HACHAGE

79

Ces deux hypoth`ses sont ralistes. Pour la premi`re, en supposant que le cot de calcul de la e e e u fonction de hachage est proportionnel ` la longueur des mots, nous constatons que la longueur a des mots dun texte ordinaire de N mots est faible et indpendante de N .2 La seconde hypoth`se e e traduit simplement que nous disposons dune bonne fonction de hachage, faisons conance a ` la mthode hashCode des String. e Sous ces deux hypoth`ses, la recherche dun lment se fait en moyenne en temps (1 + ), e ee o` = n/m est le facteur de charge (load factor ) de la table (n est le nombre de cls ` ranger et u e a m est la taille du tableau). Plus prcisment une recherche infructueuse dans la table parcourt e e en moyenne cellules de listes, et une recherche fructueuse 1 + /2 + 1/2m cellules, cots u auxquels on ajoute le cot du calcul de la fonction de hachage. Ce rsultat est dmontr dans [4, u e e e section 12.2], contentons nous de remarquer que est tout simplement la longueur moyenne des listes dassociations t[h]. Peut-tre faut il remarquer que le cot dune recherche dans le cas le pire est O(n), quand e u toutes les cls entrent en collision. Mais employer les tables de hachage suppose de faire conance e au hasard (hachage uniforme) et donc de considrer plus le cas moyen que le cas le pire. Une e faon plus concr`te de voir les choses est de considrer que, par exemple lors du comptage des c e e mots dun texte, on ins`re et recherche de nombreux mots uniformment hachs, et que donc le e e e cot moyen donne une tr`s bonne indication du cot rencontr en pratique. u e u e Dans un premier temps, pour notre implmentation simple de H qui dimensionne le tableau t e initialement, nous pouvons interprter le rsultat de complexit en moyenne dune recherche en e e e (1 + ), en constatant que si la taille du tableau interne est de lordre de n, alors nous avons atteint un cot (en moyenne) de put et get en temps constant. Il peut sembler que nous nous u sommes livrs ` une suite dapproximations et d`-peu-pr`s, et cest un peu vrai. Il nen reste pas e a a e moins, et cest le principal, que les tables de hachage sont ecaces en pratique, essentiellement sous rserve que pour une excution donne, les valeurs de hachage des cls se rpartissent e e e e e uniformment parmi les indices du tableau interne correctement dimensionn, mais aussi que le e e cot du calcul de la fonction de hachage ne soit pas trop lev. Dans cet esprit pragmatique, on u e e peut voir la table de hachage comme un moyen simple de diviser le cot des listes dassociation u dun facteur n, au prix de lallocation dun tableau de taille de lordre de n. 2.3.1 Complment : redimensionnement dynamique e

Dans un deuxi`me temps, il est plus convenable, et ce sera aussi plus pratique, de redie mensionner dynamiquement la table de hachage an de maintenir borner le facteur de charge. Pour atteindre un cot amorti en temps constant pour put, il sut de deux conditions (comme u pour push dans le cas des piles, voir III.2.1) La taille des tableaux internes doit suivre une progression gomtrique au cours du temps. e e Le cot du redimensionnement doit tre proportionnel au nombre dinformations stockes u e e dans la table au moment de ce redimensionnement. Dnissons dabord une constante alpha qui est notre borne suprieure du facteur de charge, e e et une variable dinstance nbKeys qui compte le nombre dassociations eectivement prsentes e dans la table. final static double alpha = 4.0 ; private int nbKeys = 0 ; final static int SIZE = 16 ;
2 Un autre argument est de dire quil existe de lordre de N = K mots de taille infrieure ` , o` K est le e a u nombre de caract`res possibles. Dans ce cas le cot du calcul de la fonction de hachage est en O(log N ), nglig e u e e en supposant n N .

80

CHAPITRE IV. ASSOCIATIONS TABLES DE HACHAGE

Nous avons aussi chang la valeur de la taille par dfaut de la table, an de ne pas mobiliser e e une quantit consquente de mmoire a priori. Cest aussi une bonne ide de procder ainsi an e e e e e que le redimensionnement ait eectivement lieu et que le code correspondant soit test. e La mthode de redimensionnement resize, ` ajouter ` la classe H double la taille du tableau e a a interne t. private void resize() { // Ancienne taille int old_sz = t.length ; int new_sz = 2*old_sz ; // Nouvelle taille AList [] oldT = t ; // garder une rfrence sur lancien tableau e e t = new AList [new_sz] ; // Allouer le nouveau tableau /* Insrer toutes les paires cl-information de oldT e e dans le nouveau tableau t */ for (int i = 0 ; i < old_sz ; i++) { for (AList p = oldT[i] ; p != null ; p = p.next) { int h = hash(p.key) ; t[h] = new AList (p.key, p.val, t[h]) ; } } } Il faut noter que la fonction de hachage hash qui transforme les cls en indices du tableau t e dpend de la taille de t (de fait son code emploie this .t.length). Pour cette raison, le nouveau e tableau est directement rang dans le champ t de this et une rfrence sur lancien tableau est e ee conserve dans la variable locale oldT, le temps de parcourir les paires cl-information contenues e e dans les listes de lancien tableau pour les ajouter dans le nouveau tableau t. Le redimensionnement nest pas gratuit, il est mme assez coteux, mais il reste bien proportionnel au nombre e u dinformations stockes sous rserve dun calcul en temps constant de la fonction de hachage. e e Cest la mthode put qui tient ` jour le compte nbKey et appelle le mthode resize quand e a e le facteur de charge nbKeys/t.length dpasse alpha. e public void put(String key, int val) { int h = hash(key) ; AList r = AList.getCell(key, t[h]) ; i f (r == null) { t[h] = new AList(key, val, t[h]) ; nbKeys++ ; i f (t.length * alpha < nbKeys) { resize() ; } } else { r.val = val ; } } Notez que le redimensionnement est, le cas chant, eectu apr`s ajout dune nouvelle associae e e e tion. En eet, la valeur de hachage h nest valide que relativement ` la longueur de tableau t. a

2.4

Adressage ouvert

Dans le hachage ` adressage ouvert, les lments dinformations sont stocks directement a ee e dans le tableau. Plus prcisment, la table de hachage est un tableau de paires cl-information. e e e Le facteur de charge est donc ncessairement infrieur ` un. Etant donne une cl k on e e a e e recherche linformation associe ` k dabord dans la case dindice h(k), puis, si cette case est e a occupe par une information de cl k dirente de k, on continue la recherche en suivant une e e e

2. TABLE DE HACHAGE

81

squence dindices prdnie, jusqu` trouver une case contenant une information dont la cl e e e a e vaut k ou une une case libre. Dans le premier cas il existe un lment de cl k dans la table, ee e dans le second il nen existe pas. La squence la plus simple consiste ` examiner successivement e a les indices h(k), h(k) + 1, h(k) + 2 etc. modulo m taille de la table. Cest le sondage linaire e (linear probing). Pour ajouter une information (k, v), on proc`de exactement de la mme mani`re, jusqu` e e e a trouver une case libre (auquel cas k ntait pas dans la table et un dispose dune case o` ranger e u (k, v)), ou une case contenant une information associe ` k (auquel cas on dispose dune case e a pour ranger (k, v)). Une fois entre dans la table, une cl reste donc ` la mme place dans le e e a e tableau et est accde selon la mme squence, ` condition de ne pas supprimer dinformations, e e e e a ce que nous supposons. Pour coder une nouvelle implmentation O de la table dassociation Assoc, qui utilise le e hachage avec adressage ouvert. Nous dnissons dabord une classe des paires cl-information. e e class Pair { String key ; int val ; Pair(String key, int val) { this.key = key ; this.val = val ; } } e Les objets Pair sont les elments du tableau interne de des objets O. Le code de la classe O est donn par la gure 3. Dans le constructeur, les cases du tableau new Pair [SIZE] sont e initialises ` null (voir A.3.6.2). qui est justement la valeur qui permet ` getSlot didentier e a a une cases vides . On observe que la mthode getSlot est appele par les deux mthodes e e e put et get. La mthode getSlot peut chouer, quand la table est pleine (notez lemploi de la e e boucle do, voir A.3.4), ce qui rend plus critique la question du dimensionnement du tableau interne que dans le cas du cha nage. Exercice 1 Modier le code de la classe O an de redimensionner automatiquement le tableau interne, d`s que le facteur de charge dpasse une valeur critique alpha e e final static double alpha = 0.5 ; Solution. Comme dans le cas du cha nage, nous allons crire une mthode prive resize charge e e e e dagrandir la table quand elle devient trop charge. La mthode put est modie pour grer le e e e e compte nbKeys des informations eectivement prsentes dans la table, et appeler resize si e besoin est. private int nbKeys = 0 ; public void put(String key, int val) { int h = getSlot(key) ; Pair p = t[h] ; i f (p == null) { nbKeys++ ; t[h] = new Pair(key, val) ; i f (t.length * alpha < nbKeys) resize() ; } else { p.val = val ; } } La mthode resize fait appel ` getSlot pour transfrer les informations de lancienne ` la e a e a nouvelle table. Cest le meilleur moyen de garantir des ajouts compatibles avec les mthodes e put et get.

82

CHAPITRE IV. ASSOCIATIONS TABLES DE HACHAGE

Fig. 3 Implmentation dune table de hachage ` adressage ouvert e a class O implements Assoc { private final static int SIZE = 1024 ; // Assez grand ? private Pair [] t ; // Table O() { t = new Pair[SIZE] ; } private int hash(String key) { return Math.abs(key.hashCode()) % t.length ; } /* Mthode de recherche de la case associe ` key */ e e a private int getSlot(String key) { int h0 = hash(key) ; int h = h0 ; do { /* Si t[h] est vide ou contient la cl key, on a trouv */ e e i f (t[h] == null || key.equals(t[h].key)) return h ; /* Sinon, passer ` la case suivante */ a h++ ; i f (h >= t.length) h = 0 ; } while (h != h0) ; throw new Error ("Table pleine") ; // On a fait le tour complet } public int Pair p = i f (p == return } else { return } } get(String key) { t[getSlot(key)] ; null) { 0 ; p.val ;

public void put(String key, int val) { int h = getSlot(key) ; Pair p = t[h] ; i f (p == null) { t[h] = new Pair(key, val) ; } else { p.val = val ; } } }

2. TABLE DE HACHAGE private void resize() { int old_sz = t.length ; int new_sz = 2*old_sz ; Pair [] oldT = t ; t = new Pair[new_sz] ; for (int k = 0 ; k < old_sz ; k++) { Pair p = oldT[k] ; i f (p != null) t[getSlot(p.key)] = p ; } } }

83

Il faut, comme dans le cas du cha nage, prendre la prcaution de ranger le nouveau tableau dans e la variable dinstance t avant de commencer ` calculer les valeurs de hachage dans le nouveau a tableau. On note aussi que oldT[k] peut valoir null et quil faut en tenir compte. On dmontre [11, section 6.4] quune recherche infructueuse entra en moyenne lexamen e ne denviron 1/2 (1 + 1/(1 )2 ) cases et une recherche fructueuse denviron 1/2 (1 + 1/(1 )), o` est le facteur de charge et sous rserve de hachage uniforme. Ces rsultats ne sont stricto u e e sensu plus valables pour proche de un, mais les formules donnent toujours un majorant. En tous cas pour un facteur de charge de 50 % on examine en moyenne pas plus de trois cases. Le sondage linaire provoque des phnom`nes de regroupement (en plus des collisions). e e e Considrons par exemple la table ci-dessous, o` les cases encore libres sont en blanc : e u
1 0

0
0

1
1

2
2

3
3

4
4

5
5

6
6

7
7

8
8

9
9

10 11 12 13 14 15 16 17 18
10 11 12 13 14 15 16 17 18 19

En supposant que h(k) soit distribu uniformment, la probabilit pour que la case i soit choisie e e e a ` la prochaine insertion est la suivante P (0) = 1/19 P (12) = 3/19 P (2) = 2/19 P (13) = 1/19 P (3) = 1/19 P (14) = 1/19 P (8) = 5/19 P (16) = 2/19 P (9) = 1/19 P (18) = 2/19

Comme on le voit, la case 8 a la plus grande probabilit dtre occupe, ce qui accentuera le le e e e regroupement des cases 4-7. Ce phnom`ne se rv`le rapidement quand des cls successives sont e e e e e haches sur des entiers successifs, un cas qui se prsente en pratique avec le hachage modulo e e (voir 3), quand les cls sont par exemple les valeurs successives dun compteur, ou, dans des e applications plus techniques, des adresses dobjets qui se suivent dans la mmoire. e Plusieurs solutions ont t proposes pour viter ce probl`me. La meilleure solution consiste ee e e e a ` utiliser un double hachage. On se donne deux fonctions de hachage h : U {0, . . . , m 1} et h : U {0, . . . , r 1}. Ensuite le sondage est eectu selon la squence h(k) + h (k), e e h(k) + 2h (k), h(k) + 3h (k), etc. Les regroupements ne sont plus ` craindre essentiellement a parce que lincrment de la squence est lui aussi devenu une fonction uniforme de la cl. En e e e particulier en cas de collision selon h, il ny a aucune raison que les sondages se fassent selon le mme incrment. Pour que le sondage puisse parcourir toute la table on prend h (k) > 0 et e e h (k) premier avec m taille de la table. Pour ce faire on peut prendre m gal ` une puissance e a de deux et h (k) toujours impair, ou m premier et h (k) strictement infrieur ` m (par exemple e a pour des cl enti`res h(k) = k mod m et h (k) = 1 + (k mod (m 2)) e e Dans [11, section 6.4] D. Knuth arme que, sous des hypoth`ses de distribution uniforme et e dindpendance des deux fonctions de hachage, le nombre moyen de sondages pour un hachage e double est environ ln(1 )/ en cas de succ`s et ` 1/(1 ) en cas dchec.3 Le tableau e a e ci-dessous donne quelques valeurs numriques : e
3

Ces valeurs proviennent dun mod`le simpli, et ont t vries exprimentalement. e e ee e e e

84

CHAPITRE IV. ASSOCIATIONS TABLES DE HACHAGE Facteur de charge Succ`s e Echec 50 % 1.39 2.00 80 % 2.01 5.00 90 % 2.56 10.00 99 % 4.65 100.00

Comme on le voit = 80 % est un excellent compromis. Insistons sur le fait que les valeurs de ce tableau sont indpendantes de n. Par consquent, avec un facteur charge de 80 %, il sut e e en moyenne de deux essais pour retrouver un lment, mme avec dix milliards de cls ! Notons ee e e que pour le sondage linaire cet ordre de grandeur est atteint pour des tables ` moiti pleines. e a e Tandis que pour le cha nage on peut aller jusqu` un facteur de charge denviron 4. a En xant ces valeurs de facteur de charge pour les trois techniques, nous galisons plus e ou moins les temps de recherche. Examinons alors la mmoire occupe pour n associations. e e Nous constatons que le cha nage consomme un tableau de taille n/4 plus n cellules de listes ` a trois champs (soit 3 + n/4 + 5 n cases de mmoire en Java, en tenant compte de deux cases e supplmentaires par objet (voir III.4). Tandis que Le sondage linaire consomme 3 + 2 n + 4 n e e (tableau et paires), et le double hachage 3 + 5/4 n + 4 ()n (tableau et paires encore). Le gain des deux derni`res techniques est donc minime, mais on peut coder avec moins de mmoire e e (par exemple en grant deux tableaux, un pour les cls, un pour les valeurs) Lespace mmoire e e e employ devient alors respectivement 6 + 4 n et 6 + 5/2 n, soit nalement une conomie de e e mmoire consquente pour le double hachage. e e

2.5

Tables de hachage de la libraire

Il aurait t tonnant que la libraire de Java nore pas de table de hachage, tant cette struce ee ture est utile. La classe HashMap est une classe gnrique HashMap<K,V> paramtre par les e e e e classes des cls K et des informations V (voir III.2.3). On crit donc une derni`re implmentation, e e e e tr`s courte, de notre interface Assoc. e import java.util.* ; class Lib { private HashMap <String,Integer> t ; H() { t = new HashMap <String,Integer> () ; } public int get(String key) { Integer val = t.get(key) ; i f (val == null) { return 0 ; } else { return val ; } } public void put(String key, int val) { t.put(key,val) ; } } La table de hachage t est construite avec la taille initiale et le facteur de charge par dfaut e (respectivement 16 et 0.75). Les mthodes t.put et t.get sont celles des HashMap, qui se e ` comportent comme les ntres. A ceci pr`s que cls et informations sont obligatoirement des o e e objets et que lappel t.get(key) renvoie null quand la cl key nest pas dans la table t, e fait que nous exploitons directement dans notre mthode get. On note la conversion automae tique dun Integer en int (return val dans le corps de get) et dun int en Integer (appel t.put(key,val) dans put).

3. CHOIX DES FONCTIONS DE HACHAGE

85

2.6

Performance

a e Nous nous livrons ` une exprience consistant ` appliquer le programme Freq ` une srie de a e a chiers contenant du source Java. Nous mesurons le temps cumul dexcution de la mthode count e e e (celle qui lit les mots un par un et accumule les comptes dans la table dassociation Assoc, page 74), pour les quatre implmentations des tables dassociations. e La table L base sur les listes dassociations. e La table H base sur le hachage avec cha e nage (facteur de charge 4.0, taille initiale 16) La table O base sur le hachage ouvert (sondage linaire, facteur de charge 0.5, taille e e initiale 16) La table Lib base sur les HashMap de la librairie (probablement cha e nage, facteur de charge 0.75, taille initiale 16) Toute les tables de hachage sont redimensionnes automatiquement. Les rsultats (gure 4) font e e Fig. 4 temps dexcution de la mthode count e e 14 12 10 t (sec) 8 6 4 2 0 3 + + 3 + 2 2 3 + 2 2 3 3 + 2 + 2 3 + 3 22 3 + 3 + + 2 2 3 ++ 3 2 2 3 22 + + 3 L 3 + 3 2 3 H + + + 3 2 2 + 3 O 2 + 2 3 22 3+ + Lib 3 2 + 3 2 0 100000 200000 300000 400000 500000 600000 Nombre total de mots

clairement appara que les listes dassociations sont hors-jeu et que les tables de hachage ont tre le comportement linaire attendu et mme que toutes les implmentations des tables de hachages e e e se valent, au moins dans le cadre de cet exprience. e Il faut noter que toutes les tables de hachage reposent sur la fonction de hachage des cha nes de Java, et que donc cest surtout cette derni`re qui se montre performante sur des mots extraits e de programmes Java. En voici pour preuve (gure 5) lhistogramme des eectifs des longueurs des listes de collisions ` la n de lexprience H (tableau de taille m = 4096, nombre dassociations a e n = 16092). Cette gure montre par exemple quil y a un peu plus de 800 listes de collisions de longueur trois. En conclusion lintrt en pratique des tables de hachage est tr`s important, ee e compte tenu des performances atteintes pour une dicult de ralisation particuli`rement faible e e e (surtout si on utilise la librairie).

3
3.1

Choix des fonctions de hachage


En thorie e

Rappelons quune fonction de hachage est une fonction h : U {0, . . . m 1}. Une bonne fonction de hachage doit se rapprocher le plus possible dune rpartition uniforme. Formellement, e cela signie que si on se donne une probabilit P sur U et que lon choisit alatoirement chaque e e

86

CHAPITRE IV. ASSOCIATIONS TABLES DE HACHAGE Fig. 5 Rpartition des collisions e 900 800 700 600 500 400 300 200 100 0

10

11

12

cl k dans U , on ait pour tout j avec 0 j m 1, e P (k) =


{k|h(k)=j}

1 m

En pratique, on conna rarement la probabilit P et on se limite ` des heuristiques. En partit e a culier, on veut souvent que des cls voisines qui surviennent en pratique, donnent des valeurs de e hachages tr`s distinctes. e Le cas que nous traitons ci-dessous est celui o` lunivers U est un sous-ensemble des entiers u naturels, car on peut toujours en pratique se ramener ` ce cas. Par exemple, si les cls sont des a e cha nes de caract`res, on peut interprter chaque caract`re comme un chire dune base B et la e e e cha comme un entier crit dans cette base. Cest ` dire que la cha a0 a1 . . . an1 est lentier ne e a ne a0 B n1 + a1 B n2 + + an1 . Si les caract`res sont quelconques on a B = 216 en Java e et B = 28 en C ; si les caract`res des cls sont limits ` lalphabet minuscule, on peut prendre e e e a B = 26 ; etc. Nus pouvons donc revenir aux fonctions de hachage sur les entiers. Une technique courante consiste ` prendre pour h(k) le reste de la division de k par m : a h(k) = k mod m

Mais dans ce cas, certaines valeurs de m sont ` viter. Par exemple, si on prend m = 2r , h(k) ae ne dpendra que des r derniers bits de k. Ce qui veut dire, par exemple, que le dbut des e e p ) ne compte plus, et conduit ` de nombreuses cha nes longues (vues comme des entiers en base 2 a collisions si les cls sont par exemple des adverbes (se terminant toutes par ment). Dans le mme e e contexte, si on prend m = B 1, linterversion de deux caract`res passera inaperue. En eet, e c comme (a B + b) (b B + a) = (a b)(B 1), on a a B + b b B + a (mod m) En pratique, une bonne valeur pour m est un nombre premier tel que B k a nest pas divisible par m, pour des entiers k et a petits. Une autre technique consiste ` prendre a h(k) = m(Ck Ck)

3. CHOIX DES FONCTIONS DE HACHAGE

87

o` C est une constante relle telle que 0 < C < 1. Cette mthode a lavantage de pouvoir u e e sappliquer ` toutes les valeurs de m. Il est mme conseill dans ce cas de prendre m = 2r a e e pour faciliter les calculs. Pour le choix de la constante C, Knuth recommande le nombre dor, C = ( 5 1)/2 0, 618.

3.2

En pratique

Continuons de considrer les cha e nes comme des entiers en base 216 . Ces entiers sont tr`s e vite grands, ils ne tiennent plus dans un int d`s que la cha est de taille suprieure ` 2. e ne e a On ne peut donc pas (pour un cot raisonnable, il existe des entiers en prcision arbitraire) u e dabord transformer la cha en entier, puis calculer h. Dans le cas du hachage modulo un ne nombre premier, il reste possible de calculer modulo m. Mais en fait ce nest pas tr`s pratique : e la fonction de hachage (mthode hashCode) des cha e nes de Java est indpendante de la tailles e des tableaux internes des tables, puisquelle est dnie dans la classe des cha e nes et ne prend ` pas une taille de tableau en argument. A toute cha elle associe un int qui est ensuite rduit ne e en indice. Selon la documentation, lappel hashCode() de la cha a0 a1 . . . an1 renvoie a0 ne 31n1 + a1 31n2 + + an2 31 + an1 . Autrement dit le code de hashCode pourrait tre e celui-ci : public int hashCode() { int h = 0 ; for (int k = 0 ; k < this.length ; k++) h = 31 * h + this.charAt(k) ; return h ; } Dans nos tables de hachage nous avons ensuite simplement rduit cet entier modulo la taille du e tableau interne. Le calcul h = 31 * h + this .charAt(k) eectue un mlange entre une valeur courante e de h (qui est la valeur de hachage du prxe de la cha e ne) et la valeur de hachage dun caract`re e de la cha qui est le caract`re lui-mme, cest-`-dire son code en Unicode (voir A.3.2.3). Le ne e e a multiplicateur 31 est un nombre premier, et ce nest pas un hasard. Lexprience nous a montr e e que ce mlange simple fonctionne en pratique, sur un ensemble de cls qui sont les mots que lon e e trouve dans des sources Java (voir en particulier la gure 5). Pour des cls plus gnrales cette e e e faon de mlanger est critique [14], mais nous allons nous en tenir ` elle. c e e a Car il faut parfois construire nos propres fonction de hachage, ou plus exactement rednir e nos propres mthodes hashCode. Notre table de hachage H appelle dabord la mthode hashCode e e dune cl (pour trouver un indice dans le tableau interne, page 77), puis la mthode equals de e e la mme cl (pour par exemple trouver sa place dans une liste de collision, page 75). La table e e de hachage de la librairie proc`de similairement. Or, mme si tous les objets poss`dent bien e e e des mthodes hashCode et equals (comme ils poss`dent une mthode toString), les mthodes e e e e par dfaut ne conviennent pas. En eet equals par dfaut exprime lgalit physique des cls e e e e e (voir A.3.1.1), tandis que hashCode renvoie essentiellement ladresse en mmoire de lobjet ! Il e faut rednir ces deux mthodes, comme nous avons dj` parfois redni la mthode toString e e ea e e (voir A.2.3). La classe String ne proc`de pas autrement, son hashCode et son equals se basent e non pas sur ladresse en mmoire de la cha e ne, mais sur le contenu des cha nes. Supposons donc que nous voulions nous servir de paires dentiers comme cls, cest ` dire e a dobjets dune classe Pair . class Pair { int x, y ; Pair (int x, int y) { this.x = x ; this.y = y ; } }

88

CHAPITRE IV. ASSOCIATIONS TABLES DE HACHAGE

Pour rednir hashCode nous utilisons tout simplement le mlangeur multiplicatif. e e public int hashCode() { return x * 31 + y ; } La mthode rednie est dclare public comme la mthode dorigine. Il importe en fait de e e e e e respecter toute la signature de la mthode dorigine. Or, la signature de la mthode equals des e e Object (celle que nous voulons rednir) est : e public boolean equals(Object o) Nous crivons donc (dans la classe Pair ) : e public boolean equals(Object o) { Pair p = (Pair)o ; // Conversion de type, choue si o nest pas un Pair e return this.x == p.x && this.y == p.y ; } Pour la conversion de type, voir A.3.2.2. Evidemment, pour que les tables de hachage fonctionnent correctement il faut que lgalit selon equals (lappel p1 .equals(p2 ) renvoie true) e e entra lgalit des code de hachage (p1 .hashCode() == p2 .hashCode()), ce que la documenne e e tation de la biblioth`que appelle le contrat gnral de hashCode. e e e

Chapitre V

Arbres
Ce chapitre est consacr aux arbres, lun des concepts algorithmiques les plus importants de e linformatique. Les arbres servent ` reprsenter un ensemble de donnes structures hirarchia e e e e quement. Plusieurs notions distinctes se cachent en fait sous cette terminologie : arbres libres, arbres enracins, arbres binaires, etc. Ces dnitions sont prcises dans la section 1. e e e e Nous prsentons plusieurs applications des arbres : les arbres de dcision, les les de priorit, e e e le tri par tas et lalgorithme baptis union-nd , qui sapplique dans une grande varit de e ee situations. Les arbres binaires de recherche seront traits dans le chapitre suivant. e

Dnitions e

Pour prsenter les arbres de mani`re homog`ne, quelques termes emprunts aux graphes e e e e sav`rent utiles. Nous prsenterons donc les graphes, puis successivement, les arbres libres, les e e arbres enracins et les arbres ordonns. e e

1.1

Graphes

Un graphe G = (S, A) est un couple form dun ensemble de nuds S et dun ensemble A e darcs. Lensemble A est une partie de S S. Les nuds sont souvent reprsents par des points e e dans le plan, et un arc a = (s, t) par une ligne oriente joignant s ` t. On dit que larc a part e a de s et va ` t. Un chemin de s ` t est une suite (s = s0 , . . . , sn = t) de nuds tels que, pour a a 1 i n, (si1 , si ) soit un arc. Le nud s0 est lorigine du chemin et le nud sn son extrmit. e e Lentier n est la longueur du chemin. Cest un entier positif ou nul. Un circuit est un chemin de longueur non nulle dont lorigine co ncide avec lextrmit. e e

` Fig. 1 A gauche un graphe, ` droite un graphe non orient. a e ` oe A ct de ces graphes, appels aussi graphes orients ( digraph en anglais, pour directed e e graph ), il existe la variante des graphes non orients. Au lieu de couples de nuds, on consid`re e e 89

90

CHAPITRE V. ARBRES

des paires {s, t} de nuds. Un graphe non orient est donn par un ensemble de ces paires, e e appeles artes. Les concepts de chemin et circuit se transposent sans peine ` ce contexte. e e a Un chemin est simple si tous ses nuds sont distincts. Un graphe est connexe si deux quelconques de ses nuds sont relis par un chemin. e

1.2

Arbres libres

Dans la suite de chapitre, nous prsentons des familles darbres de plus en plus contraints. e La famille la plus gnrale est forme des arbres libres. Un arbre libre est un graphe non orient e e e e non vide, connexe et sans circuit. La proposition suivante est laisse en exercice. e

Fig. 2 Un arbre libre . Proposition 1 Soit G = (S, A) un graphe non orient non vide. Les conditions suivantes sont e quivalentes : e (1) G est un arbre libre, (2) Deux nuds quelconques de S sont connects par un chemin simple unique, e (3) G est connexe, mais ne lest plus si lon retire une arte quelconque, e (4) G est sans circuit, mais ne lest plus si lon ajoute une arte quelconque, e (5) G est connexe, et Card(A) = Card(S) 1, (6) G est sans circuit, et Card(A) = Card(S) 1.

1.3

Arbres enracins e

Un arbre enracin ou arbre ( rooted tree en anglais) est un arbre libre muni dun nud e distingu, appel sa racine. Soit T un arbre de racine r. Pour tout nud x, il existe un chemin e e simple unique de r ` x. Tout nud y sur ce chemin est un anctre de x, et x est un descendant a e de y. Le sous-arbre de racine x est larbre contenant tous les descendants de x. Lavant-dernier nud y sur lunique chemin reliant r ` x est le parent (ou le p`re ou la m`re) de x, et x est un a e e enfant (ou un ls ou une lle) de y. Larit dun nud est le nombre de ses enfants. Un nud e sans enfant est une feuille, un nud darit strictement positive est appel nud interne. La e e hauteur dun arbre T est la longueur maximale dun chemin reliant sa racine ` une feuille. Un a arbre rduit ` un seul nud est de hauteur 0. e a 1 2 5 6 7 3 8 9 4

Fig. 3 Un arbre enracin . e

2. UNION-FIND, OU GESTION DES PARTITIONS

91

Les arbres admettent aussi une dnition rcursive. Un arbre sur un ensemble ni de nuds e e est un couple form dun nud particulier, appel sa racine, et dune partition des nuds restants e e en un ensemble darbres. Par exemple, larbre de la gure 3 correspond ` la dnition a e T = (1, {(2, {(5), (6)}), (3, {(7), (8), (9)}), (4)}) Cette dnition rcursive est utile dans les preuves et dans la programmation. On montre ainsi e e facilement que si tout nud interne dun arbre est darit au moins 2, alors larbre a strictement e plus de feuilles que de nuds internes. Une fort est un ensemble darbres. e

1.4

Arbres ordonns e

Un arbre ordonn est un arbre dans lequel lensemble des enfants de chaque nud est totae lement ordonn. e

1 1.1 1.2.1 1.2 1.2.2 2.1

2 2.2 2.3

Fig. 4 Larbre ordonn de la table des mati`res dun livre. e e Par exemple, un livre, structur en chapitres, sections, etc., se prsente comme un arbre e e ordonn (voir gure 4). Les enfants dun nud dun arbre ordonn sont souvent reprsents, e e e e dans un programme, par une liste attache au nud. Un autre solution est dassocier, ` chaque e a nud, un tableau de ls. Cest une solution moins souple si le nombre de ls est destin ` e a changer. Enn, on verra plus loin une autre reprsentation au moyen darbres binaires. e

Union-Find, ou gestion des partitions

Comme premier exemple de lemploi des arbres et des forts, nous considrons un probl`me e e e cl`bre, et ` ce jour pas encore enti`rement rsolu, appel le probl`me Union-Find. Rappelons ee a e e e e quune partition dun ensemble E est un ensemble de parties non vides de E, deux ` deux a disjointes et dont la runion est E. Etant donn une partition de lensemble {0, . . . , n 1}, on e e veut rsoudre les deux probl`mes que voici : e e trouver la classe dun lment (nd ) ee faire lunion de deux classes (union). Nous donnons dabord une solution du probl`me Union-Find, puis nous donnerons quelques e exemples dapplication.

2.1

Une solution du probl`me e

En gnral, on part dune partition o` chaque classe est rduite ` un singleton, puis on traite e e u e a une suite de requtes de lun des deux types ci-dessus. e Avant de traiter ce probl`me, il faut imaginer la faon de reprsenter une partition. Une e c e premi`re solution consiste ` reprsenter la partition par un tableau classe. Chaque classe est e a e

92

CHAPITRE V. ARBRES

identie par un entier par exemple, et classe[x] contient le numro de la classe de llment x e e ee (cf. gure 5). x classe[x] 0 2 1 3 2 1 3 4 4 4 5 1 6 2 7 4 8 1 9 4

Fig. 5 Tableau associ ` la partition {{2, 5, 8}, {0, 6}, {1}, {3, 4, 7, 9}}. ea Trouver la classe dun lment se fait en temps constant, mais fusionner deux classes prend ee un temps O(n), puisquil faut parcourir tout le tableau pour reprer les lments dont il faut e ee changer la classe. Une deuxi`me solution, que nous dtaillons maintenant, consiste ` choisir un e e a reprsentant dans chaque classe. Fusionner deux classes revient alors ` changer de reprsentant e a e pour les lments de la classe fusionne. Il appara avantageux de reprsenter la partition par ee e t e une fort. Chaque classe de la partition constitue un arbre de cette fort. La racine de larbre e e est le reprsentant de sa classe. La gure 6 montre la fort associe ` une partition. e e e a

Fig. 6 Fort associe ` la partition {{2, 5, 8}, {0, 6}, {1}, {3, 4, 7, 9}}. e e a Une fort est reprsente par un tableau dentiers pere (cf. Figure 7). Chaque nud est e e e reprsent par un entier, et lentier pere[x] est le p`re du nud x. Une racine r na pas de e e e parent. On convient que, dans ce cas, pere[r] = r. x pere[x] 0 0 1 1 2 5 3 4 4 7 5 5 6 0 7 7 8 2 9 7

Fig. 7 Tableau associ ` la fort de la gure 6. ea e On suppose donc dni un tableau e int[] pere = new int[n]; Ce tableau est initialis ` lidentit par ea e static void initialisation() { for (int i = 0; i < pere.length ; i++) pere[i] = i; } Chercher le reprsentant de la classe contenant un lment donn revient ` trouver la racine de e ee e a larbre contenant un nud donn. Ceci se fait par la mthode suivante : e e static int trouver(int x)

2. UNION-FIND, OU GESTION DES PARTITIONS { while (x != pere[x]) x = pere[x]; return x; }

93

Lunion de deux arbres se ralise en ajoutant la racine de lun des deux arbres comme nouveau e ls ` la racine de lautre : a static void union(int x, int y) { int r = trouver(x); int s = trouver(y); if (r != s) pere[r] = s; }

Fig. 8 La fort de la gure 6 apr`s lunion pondre de 5 et 0. e e ee Il nest pas dicile de voir que chacune de ces deux mthodes est de complexit O(h), o` h e e u est la hauteur larbre (la plus grande des hauteurs des deux arbres). En fait, on peut amliorer e lecacit de lalgorithme par la r`gle suivante (voir gure 8) : e e R`gle. Lors de lunion de deux arbres, la racine de larbre de moindre taille devient ls de la e racine de larbre de plus grande taille. Pour mettre en uvre cette stratgie, on utilise un tableau supplmentaire qui mmorise la e e e taille des arbres, qui doit tre initialis ` 1 : e ea int[] taille = new int[n]; La nouvelle mthode dunion scrit alors : e e static void unionPondre(int x, int y) e e { int r = trouver(x); int s = trouver(y); if (r == s) return; if (taille[r] > taille[s]) { pere[s] = r; taille[r] += taille[s]; } else {

94 pere[r] = s; taille[s] += taille[r]; } } Lintrt de cette mthode vient de lobservation suivante : ee e

CHAPITRE V. ARBRES

Lemme 2 La hauteur dun arbre ` n nuds cr par union pondre est au plus 1 + log2 n. a ee ee Preuve. Par rcurrence sur n. Pour n = 1, il ny a rien ` prouver. Si un arbre est obtenu par e a union pondre dun arbre ` m nuds et dun arbre ` n m nuds, avec 1 ee a a m n/2, sa hauteur est majore par e max(1 + log2 (n m), 2 + log2 m) . Comme log2 m log2 (n/2) = log2 n 1, cette valeur est majore par 1 + log2 n. e 1 1

10

10

11

11

Fig. 9 Trouver 10 avec compression du chemin. Une deuxi`me stratgie, applique cette fois-ci lors de la mthode trouver permet une nouvelle e e e e amlioration considrable de la complexit. Elle est base sur la r`gle de compression de chemins e e e e e suivante : R`gle. Apr`s tre remont du nud x ` sa racine r, on refait le parcours en faisant de chaque e e e e a nud rencontr un ls de r. e La gure 9 montre la transformation dun arbre par une compression de chemin. Chacun des nuds 10 et 4 devient ls de 1. Limplantation de cette r`gle se fait simplement. e static int trouverAvecCompression(int x) { int r = trouver(x); while (x != r) { int y = pere[x]; pere[x] = r; x = y; } return r; }

3. ARBRES BINAIRES

95

Lensemble des deux stratgies permet dobtenir une complexit presque linaire. e e e Observons que lon peut combiner les deux parcours en un seul avec la version suivante rcursive de la mme fonction. e e static int trouverAvecCompression(int x) { if (x==pere[x]) return x; return pere[x]=trouverAvecCompression(pere[x]); } Thor`me 3 (Tarjan) Avec lunion pondre et la compression des chemins, une suite de n 1 e e ee unions et de m trouver (m n) se ralise en temps O(n + m(n, m)), o` est linverse e u dune sorte de fonction dAckermann. En fait, on a (n, m) 2 pour m n et n < 265536 et par consquent, lalgorithme prcdent e e e se comporte, dun point de vue pratique, comme un algorithme linaire en n + m. Pourtant, e Tarjan a montr quil nest pas linaire et on ne conna pas ` ce jour dalgorithme linaire. e e t a e

2.2

Applications de lalgorithme Union-Find

Un premier exemple est la construction dun arbre couvrant un graphe donn. Au dpart, e e chaque nud constitue ` lui seul un arbre. On prend ensuite les artes, et on fusionne les arbres a e contenant les extrmits de larte si ces extrmits appartiennent ` des arbres dirents. e e e e e a e Un second exemple concerne les probl`mes de connexion dans un rseau. Voici un exemple de e e tel probl`me. Huit ordinateurs sont connects ` travers un rseau. Lordinateur 1 est connect e e a e e au 3, le 2 au 3, le 5 au 4, le 6 au 3, le 7 au 5, le 1 au 6 et le 7 au 8. Est-ce que les ordinateurs 4 et 6 peuvent communiquer ` travers le rseau ? Certes, il nest pas tr`s dicile de rsoudre a e e e ce probl`me ` la main, mais imaginez la mme question pour un rseau dont la taille serait e a e e de lordre de plusieurs millions. Comment rsoudre ce probl`me ecacement ? Cest la mme e e e solution que prcdemment ! On consid`re le rseau comme un graphe dont les nuds sont les e e e e ordinateurs. Au dpart, chaque nud constitue ` lui seul un arbre. On prend ensuite les artes e a e (i.e. les connexions entre deux ordinateurs), et on fusionne les arbres contenant les extrmits e e de larte si ces extrmits appartiennent ` des arbres dirents. e e e a e

Arbres binaires

La notion darbre binaire est assez dirente des dnitions prcdentes. Un arbre binaire e e e e sur un ensemble ni de nuds est soit vide, soit lunion disjointe dun nud appel sa racine, e dun arbre binaire appel sous-arbre gauche, et dun arbre binaire appel sous-arbre droit. Il est e e utile de reprsenter un arbre binaire non vide sous la forme dun triplet A = (Ag , r, Ad ). e 1 2 3 2 3 1

Fig. 10 Deux arbres binaires dirents. e

96

CHAPITRE V. ARBRES

Par exemple, larbre binaire sur la gauche de la gure 10 est (, 1, ((, 3, ), 2, )), alors que larbre sur la droite de la gure 10 est ((, 2, (, 3, )), 1, ). Cet exemple montre quun arbre binaire nest pas simplement un arbre ordonn dont tous les nuds sont darit au plus 2. e e La distance dun nud x ` la racine ou la profondeur de x est gale ` la longueur du chemin a e a de la racine ` x. La hauteur dun arbre binaire est gale ` la plus grande des distances des feuilles a e a a ` la racine. Proposition 4 Soit A un arbre binaire ` n nuds, de hauteur h. Alors h + 1 a Preuve. Il y a au plus 2i nuds ` distance i, donc n a 2h+1 1. log2 (n + 1).

Un arbre binaire est complet si tout nud a 0 ou 2 ls. Proposition 5 Dans un arbre binaire complet, le nombre de feuilles est gal au nombre de e nuds internes, plus 1. Preuve. Notons, pour simplier, f (A) le nombre de feuilles et n(A) le nombre de nuds internes de larbre binaire complet A. Il sagit de montrer que f (A) = n(A) + 1. Le rsultat est vrai pour larbre binaire de hauteur 0. Considrons un arbre binaire complet e e A = (Ag , r, Ad ). Les feuilles de A sont celles de Ag et de Ad et donc f (A) = f (Ag ) + f (Ad ). Les nuds internes de A sont ceux de Ag , ceux de Ad et la racine, et donc n(A) = n(Ag ) + n(Ad ) + 1. Comme Ag et Ad sont des arbres complets de hauteur infrieure ` celle de A, la rcurrence e a e sapplique et on a f (Ag ) = n(Ag ) + 1 et f (Ad ) = n(Ad ) + 1. On obtient nalement f (A) = f (Ag ) + f (Ad ) = (n(Ag ) + 1) + (n(Ad ) + 1) = n(A) + 1. On montre aussi que, dans un arbre binaire complet, il y a un nombre pair de nuds ` a chaque niveau, sauf au niveau de la racine.

3.1

Compter les arbres binaires

La gure 11 montre les arbres binaires ayant 1, 2, 3 et 4 nuds.

Fig. 11 Les premiers arbres binaires. Notons bn le nombre darbres ` n nuds. On a donc b0 = b1 = 1, b2 = 2, b3 = 5, b4 = 14. a Comme tout arbre A non vide scrit de mani`re unique sous forme dun triplet (Ag , r, Ad ), on e e a pour n 1 la formule
n1

bn =
i=0

bi bni1

La srie gnratrice B(x) = e e e

n 0 bn

xn

vrie donc lquation e e

xB 2 (x) B(x) + 1 = 0 .

3. ARBRES BINAIRES Comme les bn sont positifs, la rsolution de cette quation donne e e bn = 2n 1 n+1 n = (2n)! n!(n + 1)!

97

Les nombres bn sont connus comme les nombres de Catalan. Lexpression donne aussi bn 1/2 4n n3/2 + O(4n n5/2 ).

3.2

Arbres binaires et mots

Nous prsentons quelques concepts sur les mots qui servent ` plusieurs reprises. Dabord, ils e a mettent en vidence des liens entre les parcours darbres binaires et certains ordres. Ensuite, ils e seront employs dans des algorithmes de compression. e

3.2.1

Mots

Un alphabet est un ensemble de lettres, comme {0, 1} ou {a, b, c, d, r}. Un mot est une suite de lettres, comme 0110 ou abracadabra. La longueur dun mot u, note |u|, est le nombre de lettres e de u : ainsi, |0110| = 4 et |abracadabra| = 11. Le mot vide, de longueur 0, est not . Etant e donns deux mots, le mot obtenu par concatnation est le mot form des deux mots juxtaposs. e e e e Le produit de concatnation est not comme un produit. Si u = abra et v = cadabra, alors e e uv = abracadabra. Un mot p est prxe (propre) dun mot u sil existe un mot v (non vide) e tel que u = pv. Ainsi, , abr, abrac sont des prxes propres de abraca. Un ensemble de mots e P est prxiel si tout prxe dun mot de P est lui-mme dans P . Par exemple, les ensembles e e e {, 1, 10, 11} et {, 0, 00, 01, 000, 001} sont prxiels. e 3.2.2 Ordres sur les mots

Un ordre total sur lalphabet stend en un ordre total sur lensemble des mots de multiples e mani`res. Nous considrons deux ordres, lordre lexicographique et lordre des mots croiss ( rae e e dix order ou shortlex en anglais). Lordre lexicographique, ou ordre du dictionnaire, est dni par u <lex v si seulement si u e , v = pbv , o` p est un mot, a et b sont des est prxe de v ou u et v peuvent scrire u = pau e e u lettres, et a < b. Lordre des mots croiss est dni par u <mc v si et seulement si |u| < |v| ou e e |u| = |v| et u <lex v. Par exemple, on a bar <mc car <mc barda <mc radar <mc abracadabra

3.2.3

Codage des arbres binaires

Chaque arte (p, f ) dun arbre A binaire est tiquete par 0 si f est ls gauche de p, et par e e e 1 si f est ls droit. Ltiquette du chemin qui m`ne de la racine a un nud est le mot form des e e ` e tiquettes de ses artes. Le code de larbre A est lensemble des tiquettes des chemins issus de la e e e racine. Cet ensemble est clairement prxiel, et rciproquement, tout ensemble ni prxiel de e e e mots forms de 0 et 1 est le code dun arbre binaire. La correspondance est, de plus, bijective. e

98

CHAPITRE V. ARBRES

0 0 0 1 0

1 1 1 0

Fig. 12 Le code de larbre est {, 0, 1, 00, 01, 10, 11, 010, 101, 110}. Le code c(A) dun arbre binaire A se dnit dailleurs simplement par rcurrence : on a e e c() = {} et si A = (Ag , r, Ad ), alors c(A) = 0c(Ag ) {} 1c(Ad ).

3.3

Parcours darbre

Un parcours darbre est une numration des nuds de larbre. Chaque parcours dnit un e e e ordre sur les nuds, dtermin par leur ordre dapparition dans cette numration. e e e e On distingue les parcours de gauche ` droite, et les parcours de droite ` gauche. Dans un a a parcours de gauche ` droite, le ls gauche dun nud prc`de le ls droit (et vice-versa pour un a e e parcours de droite ` gauche). Ensuite, on distingue les parcours en profondeur et en largeur. a Le parcours en largeur num`re les nuds niveau par niveau. Ainsi, le parcours en largeur e e de larbre 13 donne la squence a, b, c, d, e, f, g, h, i, k. On remarque que les codes des nuds e correspondants, , 0, 1, 00, 01, 10, 11, 010, 101, 110, sont en ordre croissant pour lordre des mots croiss. Cest en fait une r`gle gnrale. e e e e R`gle. Lordre du parcours en largeur correspond ` lordre des mots croiss sur le code de e a e larbre. On dnit trois parcours en profondeur privilgis qui sont e e e le parcours prxe : tout nud est suivi des nuds de son sous-arbre gauche puis des e nuds de son sous-arbre droit, en abrg NGD (Nud, Gauche, Droite). e e le parcours inxe : tout nud est prcd des nuds de son sous-arbre gauche et suivi des e e e nuds de son sous-arbre droit, en abrg GND (Gauche, Nud, Droite). e e le parcours suxe, ou postxe : tout nud est prcd des nuds de son sous-arbre gauche e e e puis des nuds de son sous-arbre droit, en abrg GDN (Gauche, Droite, Nud). e e Les ordres correspondant sont appels ordres prxe, inxe et suxe. Considrons larbre de la e e e gure 13. a 0 b 0 d 0 h 1 e f 1 i 0 k 0 1 c 1 g

Fig. 13 Un arbre binaire. Les nuds sont nomms par des lettres. e

3. ARBRES BINAIRES

99

Le parcours prxe donne les nuds dans lordre a, b, d, e, h, c, f, i, g, k, le parcours inxe e donne la suite d, b, h, e, a, f, i, c, k, g et le parcours suxe donne d, h, e, b, i, f, k, g, c, a. De mani`re e formelle, les parcours prxe (inxe, suxe) sont dnis comme suit. Si A est larbre vide, alors e e pref(A) = inf(A) = su(A) = ; si A = (Ag , r, Ad ), et e(r) est le nom de r, alors pref(A) = e(r)pref(Ag )pref(Ad ) inf(A) = inf(Ag )e(r)inf(Ad ) su(A) = su(Ag )su(Ad )e(r)

R`gle. Le parcours prxe dun arbre correspond ` lordre lexicographique sur le code de larbre. e e a Le parcours suxe correspond ` loppos de lordre lexicographique si lon convient que 1 < 0. a e Quon se rassure, il y a aussi une interprtation pour le parcours inxe, mais elle est un peu e ` plus astucieuse ! A chaque nud x de larbre, on associe un nombre form du code du chemin e menant ` x, suivi de 1. Ce code complt est interprt comme la partie fractionnaire dun a ee ee nombre entre 0 et 1, crit en binaire. Pour larbre de la gure 13, les nombres obtenus sont e donns dans la table suivante e a .1 = 1/2 b .01 = 1/4 c .11 = 3/4 d .001 = 1/8 e .011 = 3/8 f .101 = 5/8 g .111 = 7/8 h .0101 = 5/16 i .1011 = 11/16 k .1101 = 13/16 Lordre induit sur les mots est appel lordre fractionnaire. e R`gle. Lordre inxe correspond ` lordre fractionnaire sur le code de larbre. e a La programmation de ces parcours sera donne au chapitre VI. e

3.4

Une borne infrieure pour les tris par comparaisons e

Voici une application surprenante des arbres ` lanalyse de complexit. Il existe de nombreux a e algorithmes de tri, certains dont la complexit dans le pire des cas est en O(n2 ) comme les tris e par slection, par insertion ou ` bulles, dautres en O(n3/2 ) comme le tri Shell, et dautres en e a O(n log n) comme le tri fusion ou le tri par tas, que nous verrons page 109. On peut se demander sil est possible de trouver un algorithme de tri de complexit infrieure dans le pire des cas. e e Avant de rsoudre cette question, il faut bien prciser le mod`le de calcul que lon consid`re. e e e e Un tri par comparaison est un algorithme qui trie en nutilisant que des comparaisons. On peut supposer que les lments ` trier sont deux-`-deux distincts. Le mod`le utilis pour reprsenter ee a a e e e un calcul est un arbre de dcision. Chaque comparaison entre lments dune squence ` trier e ee e a est reprsente par un nud interne de larbre. Chaque nud pose une question. Le ls gauche e e correspond ` une rponse ngative, le ls droit ` une rponse positive (gure 14). a e e a e

100

CHAPITRE V. ARBRES

a1 > a2 a2 > a3 (1, 2, 3) a1 > a3 a1 > a3 (2, 1, 3) a2 > a3 (3, 2, 1)

(1, 3, 2)

(3, 1, 2)

(2, 3, 1)

Fig. 14 Exemple darbre de dcision pour le tri. e Les feuilles reprsentent les permutations des lments ` eectuer pour obtenir la squence e ee a e trie. Le nombre de comparaisons ` eectuer pour dterminer cette permutation est gale ` la e a e e a longueur du chemin de la racine ` la feuille. a Nous prouvons ici : Thor`me 6 Tout algorithme de tri par comparaison eectue (n log n) comparaisons dans le e e pire des cas pour trier une suite de n lments. ee Preuve. Tout arbre de dcision pour trier n lments a n! feuilles, reprsentant toutes les pere ee e mutations possibles. La hauteur de larbre est donc minore par log(n!). Or log(n!) = O(n log n) e par la formule de Stirling. Deux prcisions pour clore cette parenth`se sur les tris. Tout dabord, le rsultat prcdent e e e e e nest plus garanti si lon change de mod`le. Supposons par exemple que lon veuille classer les e notes (des entiers entre 0 et 20) provenant dun paquet de 400 copies. La faon la plus simple c et la plus ecace consiste ` utiliser un tableau T de taille 21, dont chaque entre T[i] sert a e a ` compter les notes gales ` i. Il sut alors de lire les notes une par une et dincrmenter e a e le compteur correspondant. Une fois ce travail accompli, le tri est termin : il y a T[0] notes e gales ` 0, suivi de T[1] notes gales ` 1, etc. Cet algorithme est manifestement linaire et e a e a e ne fait aucune comparaison ! Pourtant, il ne contredit pas notre rsultat. Nous avons en eet e utilis implicitement une information supplmentaire : toutes les valeurs ` trier appartiennent ` e e a a lintervalle [0, 20]. Cet exemple montre quil faut bien rchir aux conditions particuli`res avant e e e de choisir un algorithme. Seconde remarque, on constate exprimentalement que lalgorithme de tri rapide (QuickSort), e dont la complexit dans le pire des cas est en O(n2 ), est le plus ecace en pratique. Comment e est-ce possible ? Tout simplement parce que notre rsultat ne concerne que la complexit dans e e le pire des cas. Or QuickSort est un algorithme en O(n log n) en moyenne.

4
4.1

Arbres de syntaxe abstraite


Les expressions sont des arbres

Considrons une dnition des expressions arithmtiques avec un il neuf. Une expression e e e arithmtique e est : e un entier, ou bien une opration e1 op e2 , o` e1 et e2 sont des expressions arithmtiques et op est un e u e oprateur (+, -, * et /). e

4. ARBRES DE SYNTAXE ABSTRAITE

101

Lil neuf ne voit pas cette dnition comme celle de lcriture usuelle (notation inxe) des e e expressions, et dailleurs il manque les parenth`ses. Il voit une dnition inductive, lensemble e e des expressions est solution de cette quation rcursive : e e E = Z (E, +, E) (E, -, E) (E, *, E) (E, /, E) Cette dnition inductive est une dnition darbre, les expressions sont des feuilles qui contiennent e e un entier ou des nuds internes ` deux ls. Voir une expression comme un arbre vite toutes les a e ambigu es de la notation inxe. Par exemple, les deux arbres de la gure 15 disent clairement t quels sont les arguments des oprations + et * dans les deux cas. Alors quen notation inxe, e Fig. 15 Deux arbres de syntaxe abstraite + 1 2 * 3 1 + 2 * 3

pour bien se faire comprendre, il faut crire 1+(2*3) et (1+2)*3. e D`s quun programme doit faire des choses un tant soit peu compliques avec les expressions e e arithmtiques, il faut reprsenter ces expressions par des arbres de syntaxe abstraite. Le terme e e abstraite se justie par opposition ` la syntaxe concr`te qui est lcriture des expressions, a e e cest-`-dire ici la notation inxe. La production des arbres de syntaxes abstraite ` partir de la a a syntaxe concr`te est lanalyse grammaticale (parsing), une question cruciale qui est tudie dans e e e le cours suivant INF 431.

4.2

Implmentation des arbres de syntaxe abstraite e

Ecrivons une classe Exp des cellules darbre des expressions. Nous devons principalemnt distinguer cinq sortes de nuds. Les entiers, qui sont des feuilles, et les quatre oprations, qui e ont deux ls. La technique dimplmentation la plus simple est de raliser tous ces nuds par e e des objets dune seule classe Exp qui ont tous les champs ncessaires, plus un champ tag qui e indique la nature du nud.1 Le champs tag contient un entier cens tre lune de cinq constantes ee conventionnelles. class Exp { final static int INT=0, ADD=1, SUB=2, MUL=3, DIV=4 ; int tag ; // Utilis si tag == INT e int asInt ; // Utiliss si tag {ADD, SUB, MUL, DIV} e Exp e1, e2 ; Exp(int i) { tag = INT ; asInt = i ; } Exp(Exp e1, int op, Exp e2) { tag = op ; this.e1 = e1 ; this.e2 = e2 ; } }
1

Une technique plus lgante ` base dhritage des objets est possible. ee a e

102 Ainsi pour construire larbre de gauche de la gure 15, on crit : e new Exp (new Exp(1), ADD, new Exp (new Exp(2), MUL, new Exp(3)))

CHAPITRE V. ARBRES

Cest non seulement assez lourd, mais aussi source derreurs. On atteint ici la limite de ce quautorise la surcharge des constructeurs. Il est plus commode de dnir cinq mthodes statiques e e pour construire les divers nuds. static Exp mkInt(int i) { return new Exp (i) ; } static Exp add(Exp e1, Exp e2) { return new Exp (e1, ADD, e2) ; } . . . static Exp div(Exp e1, Exp e2) { return new Exp (e1, DIV, e2) ; } Et lexpression dj` vue, se construit par : ea add(mkInt(1), mul(mkInt(2), mkInt(3))) Ce qui est plus concis, sinon plus clair. Un exemple dopration complique sur les expressions arithmtiques est le calcul de e e e leur valeur. Lopration nest complique que si nous essayons de leectuer directement sur les e e notations inxes, car sur un arbre Exp cest tr`s facile. e static int calc(Exp e) { switch (e.tag) { case INT: return e.asInt ; case ADD: return calc(e.e1) + calc(e.e2) ; case SUB: return calc(e.e1) - calc(e.e2) ; case MUL: return calc(e.e1) * calc(e.e2) ; case DIV: return calc(e.e1) / calc(e.e2) ; } throw new Error ("calc : arbre Exp incorrect") ; } Linstruction throw nale est ncessaire, car le compilateur na pas de moyen de savoir que le e champ tag contient obligatoirement lune des cinq constantes conventionnelles. En son absence, le programme est rejet par le compilateur. Pour satisfaire le compilateur, on aurait aussi pu e renvoyer une valeur bidon par return 0, mais cest nettement moins conseill. Une erreur e est une erreur, en cas darbre incorrect, mieux vaut tout arrter que de faire semblant de rien. e Dans cet exemple typique, il faut surtout remarquer le lien tr`s fort entre la dnition e e inductive de larbre et la structure rcursive de la mthode. La programmation sur les arbres de e e syntaxe abstraite est naturellement rcursive. e

4.3

Traduction de la notation postxe vers la notation inxe

Nous avons dj` trait cette question de faon incompl`te, en ne produisant que des notations ea e c e inxes compl`tement parenthses (exercice III.2). Nous pouvons maintenant faire mieux. e ee Lide est dabord dinterprter la notation postxe comme un arbre, puis dacher cet arbre, e e en tenant compte des r`gles usuelles qui permettent de ne pas mettre toutes les parenth`ses. e e Pour la premi`re opration il ne faut se poser aucune question, nous reprenons le calcul des e e expressions donnes en notation postxe (voir III.1.2), en construisant un arbre au lieu de e calculer une valeur. Nous avons donc besoin dune pile darbres, ce qui est facile avec la classe des piles de la biblioth`que (voir III.2.3). e

4. ARBRES DE SYNTAXE ABSTRAITE static Exp postfixToExp(String [] arg) { Stack<Exp> stack = new Stack<Exp> () ; for (int k = 0 ; k < arg.length ; k++) { Exp e1, e2 ; String cmd = arg[k] ; i f (cmd.equals("+")) { e2 = stack.pop() ; e1 = stack.pop() ; stack.push(add(e1,e2)) ; } else i f (cmd.equals("-")) { e2 = stack.pop() ; e1 = stack.pop() ; stack.push(sub(e1,e2)) ; } else i f (cmd.equals("*")) { e2 = stack.pop() ; e1 = stack.pop() ; stack.push(mul(e1,e2)) ; } else i f (cmd.equals("/")) { e2 = stack.pop() ; e1 = stack.pop() ; stack.push(div(e1,e2)) ; } else { stack.push(mkInt(Integer.parseInt(arg[k]))) ; } } return stack.pop() ; }

103

e Examinons la question dacher un arbre Exp sous forme inxe sans abuser des parenth`ses. Tout dabord, les parenth`ses autour dun entier ne sont jamais utiles. Ensuite, on distingue deux e classes doprateurs, les additifs (+ et -) et les multiplicatifs (* et /), les oprateurs dune classe e e donne ont le mme comportement vis ` vis du parenthsage. Il y a cinq positions possibles : au e e a e sommet de larbre, et ` gauche ou ` droite dun oprateur additif ou multiplicatif. On examine a a e ensuite lventuel parenthsage dun oprateur. e e e Lapplication des oprateurs additifs doit tre parenthse quand elle appara comme see e ee t cond argument dun oprateur additif (1-2+3 sinterpr`te comme (1-2)+3, il faut donc pae e renthser 1-(2+3)), ou comme argument dun oprateur multiplicatif (considrer (1+2)*3 e e e et 1*(2+3)). Lapplication des oprateurs multiplicatifs doit tre parenthse ` droite des oprateurs e e ee a e multiplicatifs (mme raisonnement que pour les additifs). e Ceci nous conduit ` regrouper les positions possible en trois classes a (1) Sommet de larbre et ` gauche des additifs : ne rien parenthser. a e ` droite des additifs et ` gauche des multiplicatifs : ne parenthser que les additifs. (2) A a e ` (3) A droite des multiplicatifs : parenthser tous les oprateurs. e e On identie les trois classes par 1, 2 et 3. On voit alors que les additifs sont ` parenthser a e pour les classes strictement suprieures ` 1, et les multiplicatifs pour les classes strictement e a suprieures ` 2. Ce qui conduit directement ` la mthode suivante qui prend en dernier argument e a a e un entier lvl qui rend compte de la position de larbre e ` acher dans la sortie out. a static void expToInfix(PrintWriter out, Exp e, int lvl) { switch (e.tag) { case INT: out.print(e.asInt) ; return ; case ADD: case SUB: i f (lvl > 1) out.print(() ; expToInfix(out, e.e1, 1) ; out.print(e.tag == ADD ? + : -) ;

104

CHAPITRE V. ARBRES expToInfix(out, e.e2, 2) ; i f (lvl > 1) out.print()) ; return ; case MUL: case DIV: i f (lvl > 2) out.print(() ; expToInfix(out, e.e1, 2) ; out.print(e.tag == MUL ? * : /) ; expToInfix(out, e.e2, 3) ; i f (lvl > 2) out.print()) ; return ; } throw new Error ("expToInfix : arbre Exp incorrect") ;

} La mthode expToInfix mlange rcursion et achage. Cela ne pose pas de dicult partie e e e culi`re : pour acher une opration il faut dabord acher le premier argument (rcursion) puis e e e loprateur et enn le second argument (rcursion encore). e e La sortie est un PrintWriter qui poss`de une mthode print exactement comme System.out e e mais est bien plus ecace (voir A.5.5.1). Le code utilise une particularit de linstruction switch : e on peut grouper les cas (ici des additifs et des multiplicatifs). Pour acher loprateur, on a ree cours ` lexpression conditionnelle (voir A.7.2). Par exemple e.tag == ADD ? + : - vaut a + si e.tag est gal ` ADD et - autrement et ici autrement signie ncessairement e a e que e.tag est gal ` SUB puisque nous sommes dans un cas regroup du switch ne concernant e a e que ADD et SUB. Voici nalement la mthode main de la classe Exp qui appelle lachage inxe sur larbre e construit en lisant la notation postxe public static void main (String [] arg) { PrintWriter out = new PrintWriter (System.out) ; Exp e = postfixToExp(arg) ; expToInfix(out, e, 1) ; out.println() ; out.flush() ; } Les PrintWriter sont bueriss, il faut vider le tampon par out.flush() avant de nir, e voir A.5.4. Reprenons lexemple de la gure III.4. % java Exp 6 3 2 - 1 + / 9 6 - * 6/(3-2+1)*(9-6) Ce qui est meilleur que lachage ((6/((3-2)+1))*(9-6)) de lexercice III.2.

Files de priorit e

Nous avons dj` rencontr les les dattente. Les les de priorit sont des les dattente o` les ea e e u lments ont un niveau de priorit. Le passage devant le guichet, ou le traitement de llment, ee e ee se fait en fonction de son niveau de priorit. Limplantation dune le de priorit est un exemple e e dutilisation darbre, et cest pourquoi elle trouve naturellement sa place ici. De mani`re plus formelle, une le de priorit est un type abstrait de donnes oprant sur un e e e e ensemble ordonn, et muni des oprations suivantes : e e trouver le plus grand lment ee insrer un lment e ee retirer le plus grand lment ee

5. FILES DE PRIORITE

105

Bien sr, on peut remplacer le plus grand lment par le plus petit lment en prenant u ee ee lordre oppos. Plusieurs implantations dune le de priorit sont envisageables : par tableau ou e e par liste, ordonns ou non. Nous allons utiliser des tas. La table 1 prsente la complexit des e e e oprations des les de priorits selon la structure de donnes choisie. e e e Implantation Tableau non ordonn e Liste non ordonne e Tableau ordonn e Liste ordonne e Tas
a

Trouver max O(n) O(n) O(1) O(1) O(1)

Insrer e O(1) O(1) O(n) O(n) O(log n)

Retirer max O(n) O(1)a O(1) O(1) O(log n)

Dans cette table, le cot de la suppression dans une liste non ordonne est calcul en u e e supposant llment dj` trouv. ee ea e

Tab. 1 Complexit des implantations de les de priorit. e e

5.1

Tas

Un arbre binaire est tass si son code est un segment initial pour lordre des mots croiss. e e En dautres termes, dans un tel arbre, tous les niveaux sont enti`rement remplis ` lexception e a peut-tre du dernier niveau, et ce dernier niveau est rempli ` gauche . La gure 16 montre e a un arbre tass. e Proposition 7 La hauteur dun arbre tass ` n nuds est log2 n. ea

23 15 12 4 8 2 Fig. 16 Un arbre tass. e Un tas (en anglais heap ) est un arbre binaire tass tel que le contenu de chaque nud e soit suprieur ou gal ` celui de ses ls. Ceci entra par transitivit, que le contenu de chaque e e a ne, e nud est suprieur ou gal ` celui de ses descendants. e e a Larbre de la gure 16 est un tas. 5 6 7 1

5.2

Implantation dun tas

Un tas simplante facilement ` laide dun simple tableau. Les nuds dun arbre tass sont a e numrots en largeur, de gauche ` droite. Ces numros sont des indices dans un tableau (cf e e a e gure 17).

106 0 23 1 15 3 12 7 4 8 8 9 2 4 5 5 6 2 7 6 1

CHAPITRE V. ARBRES

Fig. 17 Un arbre tass, avec la numrotation de ses nuds. e e Llment dindice i du tableau est le contenu du nud de numro i. Dans notre exemple, le ee e tableau est : i ai 0 23 1 15 2 7 3 12 4 5 5 6 6 1 7 4 8 8 9 2

Le fait que larbre soit tass conduit ` un calcul tr`s simple des relations de liation dans larbre e a e (n est le nombre de ses nuds) : racine parent du nud i ls gauche du nud i ls droit du nud i nud i est une feuille nud i a un ls droit : : : : : : nud 0 nud (i 1)/2 nud 2i + 1 nud 2i + 2 2i + 1 n 2i + 2 < n

Linsertion dun nouvel lment v se fait en deux temps : dabord, llment est ajout comme ee ee e contenu dun nouveau nud ` la n du dernier niveau de larbre, pour que larbre reste tass. a e Ensuite, le contenu de ce nud, soit v, est compar au contenu de son p`re. Tant que le contenu e e ` du p`re est plus petit que v, le contenu du p`re est descendu vers le ls. A la n, on remplace e e par v le dernier contenu abaiss (voir gure 18). e 0 23 1 15 3 12 7 4 8 8 9 2 4 5 10 21 5 6 2 7 6 1

Fig. 18 Un tas, avec remonte de la valeur 21 apr`s insertion. e e La suppression se fait de mani`re similaire. Dabord, le contenu du nud le plus ` droite du e a dernier niveau est transfr vers la racine, et ce nud est supprim. Ceci garantit que larbre ee e reste tass. Ensuite, le contenu v de la racine est compar ` la plus grande des valeurs de ses ls e ea

5. FILES DE PRIORITE

107

(sil en a). Si cette valeur est suprieure ` v, elle est remonte et remplace le contenu du p`re. e a e e On continue ensuite avec le ls. Par exemple, la suppression de 16 dans larbre de gauche de la gure 19 conduit dabord ` larbre de droite de cette gure, et enn au tas de la gure 20 a 0 16 1 15 3 8 7 2 8 4 9 7 4 14 10 10 5 9 2 11 6 3 7 2 3 8 8 4 9 7 1 15 4 14 5 9 0 10 2 11 6 3

Fig. 19 Un tas, et la circulation des valeurs pendant la suppression de 16. 0 15 1 14 3 8 7 2 8 4 9 7 4 10 5 9 2 11 6 3

Fig. 20 Le tas de la gure 19 apr`s suppression de 16. e La complexit de chacune de ces oprations est majore par la hauteur de larbre qui est, e e e elle, logarithmique en la taille. Un tas est naturellement prsent comme une classe, fournissant les trois mthodes maxie e e mum(), inserer(), supprimer(). On range les donnes dans un tableau interne. Un constructeur e permet de faire linitialisation ncessaire. Voici le squelette : e class Tas { int[] a; int nTas = 0; Tas(int n) { nTas = 0; a = new int[n]; } int maximum() { return a[0]; }

108 void ajouter(int v) {...} void supprimer() {...}

CHAPITRE V. ARBRES

Avant de donner limplantation nale, voici une premi`re version ` laide de mthodes qui e a e re`tent les oprations de base. e e void ajouter(int v) { int i = nTas; ++nTas; while (!estRacine(i) && cle(parent(i)) < v) { cle(i) = cle(parent(i)); i = parent(i); } cle(i) = v; } De mme pour la suppression : e void supprimer() { --nTas; cle(0) = cle(nTas); int v = cle(0); int i = 0; while (!estFeuille(i)) { int j = filsG(i); if (existeFilsD(i) && cle(filsD(i)) > cle(filsG(i))) j = filsD(i); if (v >= cle(j)) break; cle(i) = cle(j); i = j; } cle(i) = v; } Il ne reste plus qu` remplacer ce pseudo-code par les instructions oprant directement sur le a e tableau. void ajouter(int v) { int i = nTas; ++nTas; while (i > 0 && a[(i-1)/2] <= v) { a[i] = a[(i-1)/2]; i = (i-1)/2; } a[i] = v; } On notera que, puisque la hauteur dun tas ` n nuds est log2 n, le nombre de comparaisons a utilise par la mthode ajouter est en O(log n). e e

5. FILES DE PRIORITE void supprimer() { int v = a[0] = a[--nTas]; int i = 0; while (2*i + 1 < nTas) { int j = 2*i + 1; if (j +1 < nTas && a[j+1] > a[j]) ++j; if (v >= a[j]) break; a[i] = a[j]; i = j; } a[i] = v; } }

109

L` encore, la complexit de la mthode supprimer est en O(log n). a e e On peut se servir dun tas pour trier : on ins`re les lments ` trier dans le tas, puis on les e ee a extrait un par un. Ceci donne une mthode de tri appele tri par tas ( heapsort en anglais). e e static int[] triParTas(int[] a) { int n = a.length; Tas t = new Tas(n); for (int i = 0; i < n; i++) t.ajouter(a[i]); for (int i = n - 1; i >= 0; --i) { int v = t.maximum(); t.supprimer(); a[i] = v; } return a; } La complexit de ce tri est, dans le pire des cas, en O(n log n). En eet, on appelle n fois chacune e des mthodes ajouter et supprimer. e

5.3

Arbres de slection e

Une variante des arbres tasss sert ` la fusion de listes ordonnes. On est en prsence de k e a e e suites de nombres ordonnes de faon dcroissante (ou croissante), et on veut les fusionner en e c e une seule suite croissante. Cette situation se prsente par exemple lorsquon veut fusionner des e donnes provenant de capteurs dirents. e e ` Un algorithme na op`re de mani`re la suivante. A chaque tape, on consid`re le premier f e e e e lment de chacune des k listes (cest le plus grand dans chaque liste), puis on cherche le maxiee mum de ces nombres. Ce nombre est insr dans la liste rsultat, et supprim de la liste dont il ee e e provient. La recherche du maximum de k lments cote k 1 comparaisons. Si la somme des ee u longueurs des k listes de donnes est n, lalgorithme na est donc de complexit O(nk). e f e Il semble plausible que lon puisse gagner du temps en mmorisant les comparaisons que e lon a faites lors du calcul dun maximum. En eet, lorsque lon calcule le maximum suivant,

110

CHAPITRE V. ARBRES

seule une donne sur les k donnes compares a chang. Lordre des k 1 autres lments reste le e e e e ee mme, et on peut conomiser des comparaisons si lon conna cet ordre au moins partiellement. e e t La solution que nous proposons est base sur un tas qui mmorise partiellement lordre. On e e verra que lon peut eectuer la fusion en temps O(k + n log k). Le premier terme correspond au prtraitement , cest-`-dire ` la mise en place de la structure particuli`re. e a a e Larbre de slection est un arbre tass ` k feuilles. Chaque feuille est en fait une liste, lune e ea des k listes ` fusionner (voir gure 21). La hauteur de larbre est log k. Chaque nud de larbre a

98 49 49 18 98 98 89

38 32 25 12 9

49 45 23 21 11 6

18 12 9 1

9 8 4 3 2

98 85 78 65 52 36

49 35 31 25 21 13

58 53 42 26 10

89 67 55 44 32 13

Fig. 21 Un arbre de slection pour 8 listes. e contient, comme cl, le maximum des cls de ses deux ls (ou le plus grand lment dune liste). e e ee En particulier, le maximum des lments en tte des k listes (en gris sur la gure 21) se trouve ee e e log k fois dans larbre, sur les nuds dun chemin menant ` la racine. Lextraction dun plus a grand lment se fait donc en temps constant. Cette extraction est suivie dun mise-`-jour : ee a En descendant le chemin dont les cls portent le maximum, on aboutit ` la liste dont la valeur e a de tte est ce maximum. Llment est remplac, dans la liste, par son suivant. Ensuite, on e ee e remonte vers la racine en recalculant, pour chaque nud rencontr, le valeur de la cl, avec la e e ` la racine, on trouve le nouveau maximum (voir gure 22). nouvelle valeur du ls mis-`-jour. A a La mise-`-jour se fait donc en O(log k) oprations. La mise en place initiale de larbre est en a e temps k.

6
6.1

Codage de Human
Compression des donnes e

La compression des donnes est un probl`me algorithmique aussi important que le tri. On e e distingue la compression sans perte, o` les donnes dcompresses sont identiques aux donnes u e e e e de dpart, et la compression avec perte. La compression sans perte est importante par exemple e dans la compression de textes, de donnes scientiques ou du code compil de programmes. En e e revanche, pour la compression dimages et de sons, une perte peut tre accepte si elle nest pas e e perceptible, ou si elle est automatiquement corrige par le rcepteur. e e Parmi les algorithmes de compression sans perte, on distingue plusieurs catgories : les algoe rithmes statistiques codent les caract`res frquents par des codes courts, et les caract`res rares e e e par des codes longs ; les algorithmes bass sur les dictionnaires enregistrent toutes les cha e nes de

6. CODAGE DE HUFFMAN

111

89 49 49 18 85 89 89

38 32 25 12 9

49 45 23 21 11 6

18 12 9 1

9 8 4 3 2

85 78 65 52 36

49 35 31 25 21 13

58 53 42 26 10

89 67 55 44 32 13

Fig. 22 Apr`s extraction du plus grand lment et recalcul. e ee caract`res trouves dans une table, et si une cha appara une deuxi`me fois, elle est remplace e e ne t e e par son indice dans la table. Lalgorithme de Human est un algorithme statistique. Lalgorithme de Ziv-Lempel est bas sur un dictionnaire. Enn, il existe des algorithmes numriques . Le e e codage arithmtique remplace un texte par un nombre rel entre 0 et 1 dont les chires en e e criture binaire peuvent tre calculs au fur et ` mesure du codage. Le codage arithmtique e e e a e repose aussi sur la frquence des lettres. Ces algorithmes seront tudis dans le cours 431 ou en e e e majeure.

6.2

Algorithme de Human

Lalgorithme de Human est un algorithme statistique. Les caract`res du texte clair sont e cods par des cha e nes de bits. Le choix de ces codes se fait dune part en fonction des frquences e dapparition de chaque lettre, de sorte que les lettres frquentes aient des codes courts, mais e aussi de faon ` rendre le dcodage facile. Pour cela, on choisit un code prxe, au sens dcrit c a e e e ci-dessous. La version du codage de Human que nous dtaillons dans cette section est le codage e dit statique : les frquences nvoluent pas au cours de la compression, et le code reste xe. e e Cest ce qui se passe dans les modems, ou dans les fax. Une version plus sophistique est le codage dit adaptatif , prsent bri`vement dans la e e e e section 6.3. Dans ce cas, les frquences sont mises ` jour apr`s chaque compression de caract`re, e a e e pour chaque fois optimiser le codage. 6.2.1 Codes prxes et arbres e

Un ensemble P de mots non vides est un code prxe si aucun des mots de P nest prxe e e propre dun autre mot de P . Par exemple, lensemble {0, 100, 101, 111, 1100, 1101} est un code prxe. Un code prxe P e e est complet si tout mot est prxe dun produit de mots de P . Le code de lexemple ci-dessus e est complet. Pour illustrer ce fait, prenons le mot 1011010101011110110011 : il est prxe du e produit 101 101 0 101 0 111 10 1100 111 Lintrt de ces dnitions vient des propositions suivantes. ee e

112

CHAPITRE V. ARBRES

Proposition 8 Un ensemble ni de mots sur {0, 1} est un code prxe si et seulement sil est e le code des feuilles dun arbre binaire. Rappelons quun arbre binaire est dit complet si tous ses nuds internes ont arit 2. e Proposition 9 Un ensemble ni de mots sur {0, 1} est un code prxe complet si et seulement e sil est le code des feuilles dun arbre binaire complet. La gure 23 reprend larbre de la gure 12. Le code des feuilles est prxe, mais il nest pas e complet.

0 0 0 1 0

1 1 1 0

Fig. 23 Le code des feuilles est {00, 010, 101, 110}. En revanche, le code des feuilles de larbre de la gure 24 est complet.

0 0 a 0 b 1 1 c 0 d

1 1 r

Fig. 24 Le code des feuilles est {00, 010, 011, 10, 11}. Le codage dun texte par un code prxe (complet) se fait de la mani`re suivante : ` chaque e e a lettre est associe une feuille de larbre. Le code de la lettre est le code de la feuille, cest-`-dire e a ltiquette du chemin de la racine ` la feuille. Dans le cas de larbre de la gure 24, on associe e a les lettres aux feuilles de la gauche vers la droite, ce qui donne le tableau de codage suivant : a b c d r 00 010 011 10 11

Le codage consiste simplement ` remplacer chaque lettre par son code. Ainsi, abracadabra devient a 000101100011001000010110. Le fait que le code soit prxe permet un dcodage instantan : il e e e sut dentrer la cha caract`re par caract`re dans larbre, et de se laisser guider par les ne e e tiquettes. Lorsque lon est dans une feuille, on y trouve le caract`re correspondant, et on e e recommence ` la racine. Quand le code est complet, on est sr de pouvoir toujours dcoder un a u e message, ventuellement ` un reste pr`s qui est un prxe dun mot du code. e a e e

6. CODAGE DE HUFFMAN

113

Le probl`me qui se pose est de minimiser la taille du texte cod. Avec le code donn dans le e e e tableau ci-dessus, le texte abracadabra, une fois cod, est compos de 25 bits. Si lon choisit un e e autre codage, comme par exemple a 0 b 10 c 1101 d 1100 r 111

on observe que le mme mot se code par 01011101101011000101110 et donc avec 23 bits. Bien e sr, la taille du rsultat ne dpend que de la frquence dapparition de chaque lettre dans le u e e e texte source. Ici, il y a 5 lettres a, 2 lettres b et r, et 1 fois la lettre c et d. Construction de larbre

6.2.2

Lalgorithme de Human construit un arbre binaire complet qui donne un code optimal, en ce sens que la taille du code produit est minimale parmi la taille de tous les codes produits ` a laide de codes prxes complets. e Initialisation On construit une fort darbres binaires forms chacun dune seul feuille. Chaque e e feuille correspond ` une lettre du texte, et a pour valeur la frquence dapparition de la a e lettre dans le texte. Itration On fusionne deux des arbres dont la frquence est minimale. Le nouvel arbre a pour e e frquence la somme des frquences de ses deux sous-arbres. e e Arrt On termine quand il ne reste plus quun seul arbre qui est larbre rsultat. e e La frquence dun arbre est, bien sr, la somme des frquences de ses feuilles. Voici une e u e illustration de cet algorithme sur le mot abracadabra. La premi`re tape conduit ` la cration e e a e des 5 arbres (feuilles) de la gure 25. Les feuilles des lettres c et d sont fusionnes (gure 26), e puis cet arbre avec la feuille b (gure 27), etc. Le rsultat est reprsent dans la gure 29. e e e 5 a 2 b 2 r 1 c 1 d

Fig. 25 Les 5 arbres rduits chacun ` une feuille. e a 5 a 2 b 2 r 2 1 d 1 c

Fig. 26 Les feuilles des lettres c et d sont fusionnes. e 5 a 2 b 1 d 4 2 1 c 2 r

Fig. 27 Larbre est fusionn avec la feuille de r. e

114 5 a 2 b 2 1 d 1 c 6 4 2 r

CHAPITRE V. ARBRES

Fig. 28 La feuille de b est fusionne avec larbre. e

11 5 a 6 2 b 1 d 4 2 1 c 2 r

Fig. 29 Larbre apr`s la derni`re fusion. e e Notons quil y a beaucoup de choix dans lalgorithme : dune part, on peut choisir lequel des deux arbres devient sous-arbre gauche ou droit. Ensuite, les deux sous-arbres de frquence minie male ne sont peut-tre pas uniques, et l` encore, il y a des choix pour la fusion. On peut prouver e a que cet algorithme donne un code optimal. Il en existe de nombreuses variantes. Lune delle consiste ` grouper les lettres par deux (digrammes) ou mme par bloc plus grands, notamment a e sil sagit de donnes binaires. Les techniques de compression avec dictionnaire (compression de e Ziv-Lempel) ou le codage arithmtique donnent en gnral de meilleurs taux de compression. e e e 6.2.3 Choix de la reprsentation des donnes e e

Si le nombre de lettres gurant dans le texte est n, larbre de Human est de taille 2n 1. On le reprsente ici par un tableau pere, o`, pour x 0 et y > 0 pere[x] = y si x est ls droit de e u y et pere[x] = -y si x est ls gauche de y. Chaque caract`re c a une frquence dapparition dans e e le texte, note freq[c]. Seuls les caract`res qui gurent dans le texte, i.e. de frquence non nulle, e e e vont tre reprsents dans larbre. e e e La cration de larbre est contrle par un tas (mais un tas-min , cest-`-dire un tas avec e oe a extraction du minimum). La cl de la slection est la frquence des lettres et la frquence cumule e e e e e dans les arbres. Une autre mthode rencontre est lemploi de deux les. e e Cette reprsentation des donnes est bien adapte au probl`me considr, mais est assez e e e e ee loigne des reprsentations darbres que nous verrons dans le chapitre suivant. Raliser une e e e e implantation ` laide de cette deuxi`me reprsentation est un bon exercice. a e e 6.2.4 Implantation

La classe Huffman a deux donnes statiques : le tableau pere pour reprsenter larbre, et un e e tableau freq qui contient les frquences des lettres dans le texte, et aussi les frquences cumules e e e

6. CODAGE DE HUFFMAN

115

dans les arbres. On part du principe que les 26 lettres de lalphabet peuvent gurer dans le texte (dans la pratique, on prendra plutt un alphabet de 128 ou de 256 caract`res). o e class Huffman { final static int N = 26, M = 2*N - 1; // nombre de caract`res e static int[] pere = new int[M]; static int[] freq = new int[M]; public static void main(String[] args) { String s = args[0]; calculFrequences(s); creerArbre(); String[] table = faireTable(); afficherCode(s,table); System.out.println(); } } La mthode main dcrit lopration : on calcule les frquences des lettres, et le nombre de e e e e lettres de frquence non nulle. On cre ensuite un tas de taille approprie, avec le tableau des e e e frquences comme cls. Lopration principale est creerArbre() qui cre larbre de Human. La e e e e table de codage est ensuite calcule et applique ` la cha ` compresser. e e a ne a Voyons les diverses tapes. Le calcul des frquences et du nombre de lettres de frquence non e e e nulle ne pose pas de probl`me. On notera toutefois lutilisation de la mthode charAt de la classe e e String, qui donne lunicode du caract`re en position i. Comme on a suppos que lalphabet tait e e e az et que les unicodes de ces caract`res sont conscutifs, lexpression s.charAt(i) - a donne e e bien le rang du i-`me caract`re dans lalphabet. Il faudrait bien sr modier cette mthode si e e u e on prenait un alphabet ` 256 caract`res. a e static void calculFrequences(String s) { for (int i = 0; i < s.length(); i++) freq[s.charAt(i) - a]++; } static int nombreLettres() { int n = 0; for (int i = 0; i < N; i++) if (freq[i] > 0) n++; return n; } La mthode de cration darbre utilise tr`s exactement lalgorithme expos plus haut : dabord, e e e e les lettres sont insres dans le tas ; ensuite, les deux lments de frquence minimale sont ee ee e extraits, et un nouvel arbre, dont la frquence est la somme des frquences des deux arbres e e extraits, est insr dans le tas. Comme il y a n feuilles dans larbre, il y a n 1 crations de ee e nuds internes. static void creerArbre() { int n = nombreLettres();

116 Tas tas = new Tas(2*n-1, freq); for (int i = 0; i < N; ++i) if (freq[i] >0) tas.ajouter(i); for (int i = N; i < N+n-1; ++i) { int x = tas.minimum(); tas.supprimer(); int y = tas.minimum(); tas.supprimer(); freq[i] = freq[x] + freq[y]; pere[x] = -i; pere[y] = i; tas.ajouter(i); } } Le calcul de la table de codage se fait par un parcours darbre :

CHAPITRE V. ARBRES

static String code(int i) { if (pere[i] == 0) return ""; return code(Math.abs(pere[i])) + ((pere[i] < 0) ? "0" : "1"); } static String[] faireTable() { String[] table = new String[N]; for (int i = 0; i < N; i++) if (freq[i] >0) table[i] = code(i); return table; } Il reste ` examiner la ralisation du tas-min . Il y a deux dirences avec les tas dj` vus : a e e ea dune part, le maximum est remplac par le minimum (ce qui est ngligeable), et dautre part, e e ce ne sont pas les valeurs des lments eux-mmes (les lettres du texte) qui sont utilises comme ee e e comparaison, mais une valeur qui leur est associe (la frquence de la lettre). Les mthodes des e e e tas dj` vues se rcrivent tr`s facilement dans ce cadre. ea e e class Tas { int[] a; // contient les caract`res e int nTas = 0; int[] freq; // contient les frquences des caract`res e e Tas(int taille, int[] freq) { this.freq = freq; nTas = 0; a = new int[taille]; } int minimum() {

6. CODAGE DE HUFFMAN return a[0]; } void ajouter(int v) { int i = nTas; ++nTas; while (i > 0 && freq[a[(i-1)/2]] >= freq[v]) { a[i] = a[(i-1)/2]; i = (i-1)/2; } a[i] = v; } void supprimer() { int v = a[0] = a[--nTas]; int i = 0; while (2*i + 1 < nTas) { int j = 2*i + 1; if (j +1 < nTas && freq[a[j+1]] < freq[a[j]]) ++j; if (freq[v] <= freq[a[j]]) break; a[i] = a[j]; i = j; } a[i] = v; } }

117

Une fois la table du code calcule, encore faut-il la transmettre avec le texte comprim, pour e e que le destinaire puisse dcompresser le message. Transmettre la table telle quelle est redondant e puisque lon transmet tous les chemins de la racine vers les feuilles. Il est plus conomique de faire e un parcours prxe de larbre, avec la valeur littrale des lettres reprsentes aux feuilles. Par e e e e exemple, pour larbre de la gure 29, on obtient la reprsentation 01[a]01[b]001[d]1[c]1[r]. e Une mthode plus simple est de transmettre la suite de frquence, quitte au destinataire de e e reconstruire le code. Cette deuxi`me mthode admet un ranement (qui montre jusquo` peut e e u aller la recherche de lconomie de place) qui consiste ` non pas transmettre les valeurs exactes e a des frquences, mais une squence de frquences ctives (` valeurs numriques plus petites, donc e e e a e plus courtes ` transmettre) qui donne le mme code de Human. Par exemple, les deux arbres a e de la gure 30 ont des frquences direntes, mais le mme code. Il est bien plus conomique de e e e e transmettre la suite de nombres 5, 1, 2, 1, 3 que la suite initiale 43, 13, 12, 14, 41.

6.3

Algorithme de Human adaptatif

Les inconvnients de la mthode de compression de Human sont connus : e e Il faut lire le texte enti`rement avant de lancer la compression. e Il faut aussi transmettre le code trouv. e Une version adaptative de lalgorithme de Human corrige ces dfauts. Le principe est le suivant : e

118 123 80 41 e 25 13 d 12 b 14 c 1 d 1 b 39 43 a 3 e 2 2 c 4 7 12 5 a

CHAPITRE V. ARBRES

Fig. 30 Deux suites de frquences qui produisent le mme arbre. e e Au dpart, toutes les lettres ont mme frquence (nulle) et le code est uniforme. e e e Pour chaque lettre x, le code de x est envoy, puis la frquence de la lettre est augmente e e e et larbre de Human est recalcul. e Evidemment, le code dune lettre change en cours de transmission : quand la frquence (relative) e dune lettre augmente, la longueur de son code diminue. Le code dune lettre sadapte ` sa a frquence. Le destinataire du message compress mime le codage de lexpditeur : il maintient e e e

32 11

9 11 a 7 10

21 10

11 8

4 5

3 5 c

6 6 e

5 5 f

2 3 b

1 2 d

Fig. 31 Un arbre de Human avec un parcours compatible. un arbre de Human quil met ` jour comme lexpditeur, et il sait donc ` tout moment quel a e a est le code dune lettre. Les deux inconvnients du codage de Human (statique) sont corrigs par cette version. Il e e nest pas ncessaire de calculer globalement les frquences des lettres du texte puisquelles sont e e mises ` jour ` chaque pas. Il nest pas non plus ncessaire de transmettre la table de codage a a e puisquelle est recalcule par le destinataire. e Il existe par ailleurs une version simplie du codage de Human o` les frquences utilises e u e e pour la construction de larbre ne sont pas les frquences des lettres du texte ` comprimer, mais e a les frquences moyennes rencontres dans les textes de la langue considre. Bien entendu, le e e ee taux de compression sen ressent si la distribution des lettres dans le texte ` comprimer scarte a e de cette moyenne. Cest dans ce cas que lalgorithme adaptatif sav`re particuli`rement utile. e e

6. CODAGE DE HUFFMAN

119

Notons enn que le principe de lalgorithme adaptatif sapplique ` tous les algorithmes a de compression statistiques. Il existe, par exemple, une version adaptative de lalgorithme de compression arithmtique. e Lalgorithme adaptatif op`re, comme lalgorithme statique, sur un arbre. Aux feuilles de e larbre se trouvent les lettres, avec leurs frquences. Aux nuds de larbre sont stockes les e e frquences cumules, cest-`-dire la somme des frquences des feuilles du sous-arbre dont le e e a e nud est la racine (voir gure 31). Larbre de Human est de plus muni dune liste de parcours (un ordre total) qui a les deux proprits suivantes : ee le parcours est compatible avec les frquences (les frquences sont croissantes dans lordre e e du parcours), deux nuds fr`res sont toujours conscutifs. e e On dmontre que, dans un arbre de Human, on peut toujours trouver un ordre de parcours e possdant ces proprits. Dans la gure 31, les nuds sont numrots dans un tel ordre. Larbre e ee e e 32 11 32 11

9 11 a 7 10

21 10

9 11 a 11 8 7 10

21 10

11 8

4 5

3 5 c

6 6 e

5 5 f

4 5 f

3 5 c

6 6 e 2 3 b

6 5

2 3 b

1 3 d

1 3 d

Fig. 32 Apr`s incrmentation de d (` gauche), et apr`s permutation des nuds (` droite). e e a e a de Human volue avec chaque lettre code, de la mani`re suivante. Soit x le caract`re lu dans e e e e le texte clair. Il correspond ` une feuille de larbre. Le code correspondant est envoy, puis les a e deux oprations suivantes sont ralises. e e e Partant de la feuille du caract`re, sa frquence est incrmente, et on remonte vers la e e e e racine en incrmentant les frquences dans les nuds rencontrs. e e e Avant lincrmentation, chaque nud est permut avec le dernier nud (celui de plus e e grand numro) de mme frquence dans lordre de parcours. e e e La deuxi`me opration garantit que lordre reste compatible avec les frquences. Supposons que e e e la lettre suivante du texte clair ` coder, dans larbre de la gure 31, soit une lettre d. Le code a mis est alors 1001. La frquence de la feuille d est incrmente. Le p`re de la feuille d a pour e e e e e frquence 5. Son numro dordre est 4, et le plus grand nud de frquence 5 a numro dordre e e e e 5. Les deux nuds sont changs (gure 32). De mme, avant dincrmenter le nud de numro e e e e e 8, il est chang avec le nud de numro 9, de mme frquence (gure 33). Ensuite, la racine e e e e e est incrmente. e e Pour terminer, voici une table de quelques statistiques concernant lalgorithme de Human. Ces statistiques ont t obtenues ` une poque o` il y avait encore peu de donnes disponibles ee a e u e sous format lectronique, et les contes de Grimm en faisaient partie... Comme on le voit, le taux e de compression obtenu par les deux mthodes est sensiblement gal, car les donnes sont assez e e e

120 32 11

CHAPITRE V. ARBRES 33 11

9 11 a 7 10

21 10

8 12

21 10

11 8

6 6 e

6 5

7 10

11 9 a

4 5 f

3 5 c

6 6 e 2 3 b

6 5

2 3 b

1 3 d

5 4 f

5 3 c

1 3 d

Fig. 33 Avant permutation des nuds 8 et 9 (` gauche) , et arbre nal (` droite). a a homog`nes. e Taille (bits) Human Taille code Total Adaptatif Contes de Grimm 700 000 439 613 522 440135 440164 Texte technique 700 000 518 361 954 519315 519561

Si on utilise un codage par digrammes (groupe de deux lettres), les statistiques sont les suivantes : Contes de Grimm 700 000 383 264 10 880 394 144 393 969 Texte technique 700 000 442 564 31 488 474 052 472 534

Taille (bits) Human Taille code Total Adaptatif

On voit que le taux de compression est lg`rement meilleur. e e

Chapitre VI

Arbres binaires
Dans ce chapitre, nous traitons dabord les arbres binaires de recherche, puis les arbres quilibrs. e e

Implantation des arbres binaires

Un arbre binaire qui nest pas vide est form dun nud, sa racine, et de deux sous-arbres e binaires, lun appel le ls gauche, lautre le ls droit. Nous nous intressons aux arbres contenant e e des informations. Chaque nud porte une information, appele son contenu. Un arbre non vide e est donc enti`rement dcrit par le triplet (ls gauche, contenu de la racine, ls droit). Cette e e dnition rcursive se traduit en une spcication de programmation. Il sut de prciser la e e e e nature du contenu dun nud. Pour simplier, nous supposons que le contenu est un entier. On obtient alors la dnition. e class Arbre { int contenu; Arbre filsG, filsD; Arbre(Arbre g, int c, Arbre d) { filsG = g; contenu = c; filsD = d; } } Larbre vide est, comme dhabitude, reprsent par null. Un arbre rduit ` une feuille, de contenu e e e a x, est cr par ee new Arbre(null, x, null) Larbre de la gure 1 est cr par ee new Arbre( new Arbre( new Arbre(null, 3, null), 5, new Arbre( new Arbre( new Arbre(null, 6, null), 8, 121

122 null) 12, new Arbre(null, 13, null))), 20, new Arbre( new Arbre(null, 21, null), 25, new Arbre(null, 28, null))) 20 5 3 8 6 12 13 21 25

CHAPITRE VI. ARBRES BINAIRES

28

Fig. 1 Un arbre binaire portant des informations aux nuds. Avant de poursuivre, reprenons le schma dj` utilis pour les listes. Quatre fonctions cae ea e ractrisent les arbres : composer, cle, lsGauche, lsDroit. Elles simplantent facilement : e static Arbre composer(Arbre g, int c, Arbre d) { return new Arbre(g, c, d); } static int cle(Arbre a) { return a.contenu; } static Arbre filsGauche(Arbre a) { return a.filsG; } static Arbre filsDroit(Arbre a) { return a.filsD; } Les quatre fonctions sont lies par les quations suivantes : e e cle(composer(g, c, d)) = c lsGauche(composer(g, c, d)) = g lsDroit(composer(g, c, d)) = d composer(lsGauche(a), cle(a), lsDroit(a)) = a; (a = null)

Comme pour les listes, ces quatre fonctions sont ` la base doprations non destructives. a e

1. IMPLANTATION DES ARBRES BINAIRES

123

La dnition rcursive des arbres binaires conduit naturellement ` une programmation e e a rcursive, comme pour les listes. Voici quelques exemples : la taille dun arbre, cest-`-dire le e a nombre t(a) de ses nuds, sobtient par la formule t(a) = 0 si a est vide 1 + t(ag ) + t(ad ) sinon.

o` sont nots ag et ad les sous-arbres gauche et droit de a. Do` la mthode u e u e static int { if (a == return return 1 } taille(Arbre a) null) 0; + taille(a.filsG) + taille(a.filsD);

Des formules semblables donnent le nombre de feuilles ou la hauteur dun arbre. Nous illustrons ce style de programmation par les parcours darbre dnis page 98. Les trois parcours en e profondeur scrivent : e static void parcoursPrfixe(Arbre a) e { if (a == null) return; System.out.print(a.contenu + " "); parcoursPrfixe(a.filsG); e parcoursPrfixe(a.filsD); e } static void parcoursInfixe(Arbre a) { if (a == null) return; parcoursInfixe(a.filsG); System.out.print(a.contenu + " "); parcoursInfixe(a.filsD); } static void parcoursSuffixe(Arbre a) { if (a == null) return; parcoursSuffixe(a.filsG); parcoursSuffixe(a.filsD); System.out.print(a.contenu + " "); } Le parcours en largeur dun arbre binaire scrit simplement avec une le. Le parcours prxe e e scrit lui aussi simplement de mani`re itrative, avec une pile. Nous reprenons les classes Pile e e e et File du chapitre I, sauf que ce sont, cette fois-ci, des piles ou des les darbres. On crit alors e static void parcoursPrfixeI(Arbre a) e { if (a == null) return;

124 Pile p = new Pile(); p.ajouter(a); while (!p.estVide()) { a = p.valeur(); p.supprimer(); System.out.print(a.contenu + " "); if (a.filsD != null) p.ajouter(a.filsD); if (a.filsG != null) p.ajouter(a.filsG); } } static void parcoursLargeurI(Arbre a) { if (a == null) return; File f = new File(); f.ajouter(a); while (!f.estVide()) { a = f.valeur(); f.supprimer(); System.out.print(a.contenu + " "); if (a.filsG != null) f.ajouter(a.filsG); if (a.filsD != null) f.ajouter(a.filsD); } }

CHAPITRE VI. ARBRES BINAIRES

1.1

Implantation des arbres ordonns par arbres binaires e

Rappelons quun arbre est ordonn si la suite des ls de chaque nud est ordonne. Il est donc e e naturel de reprsenter les ls dans une liste cha ee. Un nud contient aussi la rfrence ` son e n ee a ls a e, cest-`-dire ` la tte de la liste de ses ls. Ainsi, chaque nud contient deux rfrences, n a a e ee celle ` son ls a e, et celle ` son fr`re cadet. En dautres termes, la structure des arbres binaires a n a e convient parfaitement, sous rserve de rebaptiser lsAine le champ lsG et frereCadet le champ e lsD. class ArbreOrdonne { int contenu; Arbre filsAine, frereCadet; Arbre(Arbre g, int c, Arbre d) { filsAine = g; contenu = c; frereCadet = d; } }

2. ARBRES BINAIRES DE RECHERCHE Cette reprsentation est aussi appele ls gauche fr`re droit (voir gure 2). e e e

125

1 2 2 3 4 5 6 10 10 7 8 3

Fig. 2 Reprsentation dun arbre ordonn par un arbre binaire. e e Noter que la racine de larbre binaire na pas de ls droit. En fait, cette reprsentation stend e e a ` la reprsentation, par un seul arbre binaire, dune fort ordonne darbres ordonns. e e e e

Arbres binaires de recherche

Les arbres binaires servent ` grer des informations. Chaque nud contient une donne prise a e e dans un certain ensemble. Nous supposons dans cette section que cet ensemble est totalement ordonn. Ceci est le cas par exemple pour les entiers et pour les mots. e Un arbre binaire a est un arbre binaire de recherche si, pour tout nud s de a, les contenus des nuds du sous-arbre gauche de s sont strictement infrieurs au contenu de s, et que les e contenus des nuds du sous-arbre droit de s sont strictement suprieurs au contenu de s (cf. e gure 3). 15 12 8 10 13 14 16 17 20 21

Fig. 3 Un arbre binaire de recherche. Une petite mise en garde : contrairement ` ce que lanalogie avec les tas pourrait laisser a croire, il ne sut pas de supposer que, pour tout nud s de larbre, le contenu du ls gauche de s soit strictement infrieur au contenu de s, et que le contenu du ls droit de s soit suprieur e e au contenu de s. Ainsi, dans larbre de la gure 3, si on change la valeur 13 en 11, on na plus un arbre de recherche... Une consquence directe de la dnition est la r`gle suivante : e e e R`gle. Dans un arbre binaire de recherche, le parcours inxe fournit les contenus des nuds en e ordre croissant.

126

CHAPITRE VI. ARBRES BINAIRES

Une seconde r`gle permet de dterminer dans certains cas le nud qui prc`de un nud e e e e donn dans le parcours inxe. e R`gle. Si un nud poss`de un ls gauche, son prdcesseur dans le parcours inxe est le nud e e e e le plus ` droite dans son sous-arbre gauche. Ce nud nest pas ncessairement une feuille, mais a e il na pas de ls droit. Ainsi, dans larbre de la gure 3, la racine poss`de un ls gauche, et son prdcesseur est le e e e nud de contenu 14, qui na pas de ls droit... Les arbres binaires de recherche fournissent, comme on le verra, une implantation souvent ecace dun type abstrait de donnes, appel dictionnaire, qui op`re sur un ensemble totalement e e e ordonn ` laide des oprations suivantes : ea e rechercher un lment ee insrer un lment e ee supprimer un lment ee Le dictionnaire est simplement une table dassociations (chapitre IV) dont les informations sont inexistantes. Plusieurs implantations dun dictionnaire sont envisageables : par tableau, par liste ordonns ou non, par tas, et par arbre binaire de recherche. La table 1 rassemble la complexit e e des oprations de dictionnaire selon la structure de donnes choisie. e e Implantation Tableau non ordonn e Liste non ordonne e Tableau ordonn e Liste ordonne e Tas Arbre de recherche
a b

Rechercher O(n) O(n) O(log n) O(n) O(n) O(h)

Insrer e O(1)a O(1)a O(n) O(n) O(log n) O(h)

Supprimer O(n) O(1)b O(n) O(1)b O(n) O(h)

Ces cots supposent que lon sait que llment insr nest pas dans le dictionnaire. u ee e e Ces cots supposent llment supprim dj` trouv, ainsi que des oprations destrucu ee e ea e e tives.

Tab. 1 Complexit des implantations de dictionnaires e Lentier h dsigne la hauteur de larbre. On voit que lorsque larbre est bien quilibr, ceste e e a `-dire lorsque la hauteur est proche du logarithme de la taille, les oprations sont ralisables de e e mani`re particuli`rement ecace. e e

2.1

Recherche dune cl e

Nous commenons limplantation des oprations de dictionnaire sur les arbres binaires de c e recherche par lopration la plus simple, la recherche. Plutt que dcrire une mthode boolenne e o e e e qui teste la prsence dun lment dans larbre, nous crivons une mthode qui retourne larbre e ee e e dont la racine porte llment cherch sil gure dans larbre, et null sinon. Comme toujours, ee e il y a le choix entre une mthode rcursive, calque sur la dnition rcursive des arbres, et e e e e e une mthode itrative, cheminant dans larbre. Nous prsentons les deux, en commenant par e e e c la mthode rcursive. Pour chercher si un lment x gure dans un arbre A, on commence par e e ee comparer x au contenu c de la racine de A. Sil y a galit, on a trouv la rponse ; sinon il y a e e e e deux cas selon que x < c et x > c. Si x < c, alors x gure peut-tre dans le sous-arbre gauche Ag e de A, mais certainement pas dans le sous-arbre droit Ad . On limine ainsi de la recherche tous e les nuds du sous-arbre droit. Cette mthode nest pas sans rappeler la recherche dichotomique. e La mthode scrit rcursivement comme suit : e e e

2. ARBRES BINAIRES DE RECHERCHE static Arbre chercher(int x, Arbre a) { if (a == null || x == a.contenu) return a; if (x < a.contenu) return chercher(x, a.filsG); return chercher(x, a.filsD); }

127

Cette mthode retourne null si larbre a ne contient pas x. Ceci inclut le cas o` larbre est vide. e u Voici la mthode itrative. e e static chercherI(int x, Arbre a) { while(a != null && x != a.contenu) if (x < a.contenu) a = a.filsG; else a = a.filsD; return a; } On voit que la condition de continuation dans la mthode itrative chercherI est la ngation de e e e la condition darrt de la mthode rcursive, ce qui est logique. e e e

2.2

Insertion dune cl e

Ladjonction dun nouvel lment ` un arbre modie larbre. Nous sommes confronts au ee a e mme choix que pour les listes : soit on construit une nouvelle version de larbre (version non e destructive), soit on modie larbre existant (version destructive). Nous prsentons une mthode e e rcursive dans les deux versions. Dans les deux cas, si lentier gure dj` dans larbre, on ne e ea lajoute pas une deuxi`me fois. Voici la version destructive. e static Arbre inserer(int x, Arbre a) { if (a == null) return new Arbre(null, x, null); if (x < a.contenu) a.filsG = inserer(x, a.filsG); else if (x > a.contenu) a.filsD = inserer(x, a.filsD); return a; } Voici la version non destructive. static Arbre inserer(int x, Arbre a) { if (a == null) return new Arbre(null, x, null); if (x < a.contenu) { Arbre b = inserer(x, a.filsG); return new Arbre(b, a.contenu, a.filsD); }

128

CHAPITRE VI. ARBRES BINAIRES else if (x > a.contenu) { Arbre b = inserer(x, a.filsD); return new Arbre(a.filsG, a.contenu, b); } return a; }

2.3

Suppression dune cl e

La suppression dune cl dans un arbre est une opration plus complexe. Elle saccompagne e e de la suppression dun nud. Comme on le verra, ce nest pas toujours le nud qui porte la cl ` supprimer qui sera enlev. Soit s le nud qui porte la cl x ` supprimer. Trois cas sont ` ea e e a a considrer selon le nombre de ls du nud x : e si le nud s est une feuille, alors on llimine ; e si le nud s poss`de un seul ls, on limine s et on remonte ce ls. e e si le nud s poss`de deux ls, on cherche le prdcesseur t de s. Celui-ci na pas de ls e e e droit. On remplace le contenu de s par le contenu de t, et on limine t. e La suppression de la feuille qui porte la cl 13 est illustre dans la gure 4 e e

15 12 8 10 13 14 16 17 20 21 8 10 12 14

15 20 16 17 21

Fig. 4 Suppression de la cl 13 par limination dune feuille. e e La gure 5 illustre la remonte : le nud s qui porte la cl 16 na quun seul enfant. Cet e e enfant devient lenfant du p`re de s, le nud de cl 20. e e 15 12 8 10 13 17 14 16 18 19 20 21 8 10 13 12 14 17 18 19 15 20 21

Fig. 5 Suppression de la cl 16 par remonte du ls. e e Le cas dun nud ` deux ls est illustr dans la gure 6. La cl ` supprimer se trouve ` la racine a e ea a de larbre. On ne supprime pas le nud, mais seulement sa cl, en remplaant la cl par une e c e

2. ARBRES BINAIRES DE RECHERCHE

129

autre cl. Pour conserver lordre sur les cls, il ny a que deux choix : la cl du prdcesseur dans e e e e e lordre inxe, ou la cl du successeur. Nous choisissons la premi`re solution. Ainsi, la cl 14 est e e e mise ` la racine de larbre. Nous sommes alors ramens au probl`me de la suppression du nud a e e du prdcesseur et de sa cl. Comme le prdcesseur est le nud le plus ` droite du sous-arbre e e e e e a gauche, il na pas de ls droit, donc il a zro ou un ls, et sa suppression est couverte par les e deux premiers cas. 15 12 8 10 13 14 16 17 20 21 8 10 12 13 16 17 14 20 21

Fig. 6 Suppression de la cl 15 par substitution de la cl 14 et suppression de ce nud. e e Trois mthodes coop`rent pour la suppression. La premi`re, suppression, recherche le nud e e e portant la cl ` supprimer ; la deuxi`me, suppressionRacine, eectue la suppression selon les cas ea e numrs ci-dessus. La troisi`me, dernierDescendant est une mthode auxiliaire ; elle calcule le e ee e e prcdesseur dun nud qui a un ls gauche. e e static Arbre supprimer(int x, Arbre a) { if (a == null) return a; if (x == a.contenu) return supprimerRacine(a); if (x < a.contenu) a.filsG = supprimer(x, a.filsG); else a.filsD = supprimer(x, a.filsD); return a; } La mthode suivante supprime la cl de la racine de larbre. e e static Arbre supprimerRacine(Arbre a) { if (a.filsG == null) return a.filsD; if (a.filsD == null) return a.filsG; Arbre f = dernierDescendant(a.filsG); a.contenu = f.contenu; a.filsG = supprimer(f.contenu, a.filsG); } La derni`re mthode est toute simple : e e static Arbre dernierDescendant(Arbre a) { if (a.filsD == null)

130 return a; return dernierDescendant(a.filsD); }

CHAPITRE VI. ARBRES BINAIRES

La rcursivit croise entre les mthodes supprimer et supprimerRacine est droutante au premier e e e e e abord. En fait, lappel ` supprimer ` la derni`re ligne de supprimerRacine conduit au nud a a e prdcesseur de la racine de larbre, appel f. Comme ce nud na pas deux ls, il nappelle pas e e e une deuxi`me fois la mthode supprimerRacine... e e Il est intressant de voir une ralisation itrative de la suppression. Elle dmonte enti`rement e e e e e la mcanique de lalgorithme. En fait, chacune des trois mthodes peut sparment tre crite e e e e e e de faon rcursive. c e static Arbre supprimer(int x, Arbre a) { Arbre b = a; while (a != null && x != a.contenu) if (x < a.contenu) a = a.filsG; else a = a.filsD; if (a != null) a = supprimerRacine(a); return b; } Voici la deuxi`me. e static Arbre supprimerRacine(Arbre a) { if (a.filsG == null) return a.filsD; if (a.filsD == null) return a.filsG; Arbre b = a.filsG; if (b.filsD == null) { // cas (i) a.contenu = b.contenu; a.filsG = b.filsG; } else { // cas (ii) Arbre p = avantDernierDescendant(b); Arbre f = p.filsD; a.contenu = f.contenu; p.filsD = f.filsG; } return a; } Et voici le calcul de lavant-dernier descendant : static Arbre avantDernierDescendant(Arbre a) { while (a.filsD.filsD != null) a = a.filsD;

2. ARBRES BINAIRES DE RECHERCHE return a; }

131

Dcrivons plus prcisment le fonctionnement de la mthode supprimerRacine. La premi`re partie e e e e e permet de se ramener au cas o` la racine de larbre a a deux ls. On note b le ls gauche de a, u et pour dterminer le prdcesseur de la racine de a, on cherche le nud le plus ` droite dans e e e a larbre b. Deux cas peuvent se produire : (i) la racine de b na pas de ls droit, ou (ii) la racine de b a un ls droit. Les deux cas sont illustrs sur les gures 7 et 8. e a 21 b 12 8 10 22 23 24 25 8 10 22 23 12 24 25

Fig. 7 Suppression de la racine, version itrative, cas (i). e a 21 b 12 8 10 13 14 p 16 f 20 18 22 23 24 25 8 10 13 b 12 14 p 16 18 22 23

20 24 25

Fig. 8 Suppression de la racine, version itrative, cas (ii). e Dans le premier cas, la cl de la racine de b est transfre ` la racine de a, et b est remplace e ee a e par son sous-arbre gauche (qui peut dailleurs tre vide). Dans le deuxi`me cas, on cherche e e lavant-dernier descendant, not p, de b sur la branche droite de b, au moyen de la mthode e e avantDernierDescendant. Cela peut tre b lui-mme, ou un de ses descendants (notons que dans e e le cas (i), lavant-dernier descendant nexiste pas, ce qui explique le traitement spar opr dans e e ee ce cas). Le sous-arbre droit f de p nest pas vide par dnition. La cl de f est transfre ` la e e ee a racine de a, et f est remplac par son sous-arbre gauche ce qui fait dispara la racine de f . e tre

2.4

Hauteur moyenne

Il est facile de constater, sur nos implantations, que la recherche, linsertion et la suppression dans un arbre binaire de recherche se font en complexit O(h), o` h est la hauteur de larbre. e u

132

CHAPITRE VI. ARBRES BINAIRES

Le cas le pire, pour un arbre ` n nuds, est O(n). En ce qui concerne la hauteur moyenne, a deux cas sont ` considrer. La premi`re des propositions sapplique aux arbres, la deuxi`me aux a e e e permutations. Proposition 10 Lorsque tous les arbres binaires ` n nuds sont quiprobables, la hauteur a e moyenne dun arbre binaire ` n nuds est en O( n). a Proposition 11 Lorsque toutes les permutations de {1, . . . , n} sont quiprobables, la hauteur e moyenne dun arbre binaire de recherche obtenu par insertion des entiers dune permutation dans un arbre initialement vide est O(n log n). La dirence provient du fait que plusieurs permutations peuvent donner le mme arbre. Par e e exemple les permutations 2, 1, 3 et 2, 3, 1 produisent toutes les deux larbre de la gure 9.

2 1 3

Fig. 9 Larbre produit par la suite dinsertions 2, 1, 3, ou par la suite 2, 3, 1.

Arbres quilibrs e e

Comme nous lavons dj` constat, les cots de la recherche, de linsertion et de la suppression ea e u dans un arbre binaire de recherche sont de complexit O(h), o` h est la hauteur de larbre. Le cas e u le pire, pour un arbre ` n nuds, est O(n). Ce cas est atteint par des arbres tr`s dsquilibrs, a e ee e ou liformes . Pour viter que les arbres puissent prendre ces formes, on utilise des oprations e e ` plus ou moins simples, mais peu coteuses en temps, de transformation darbres. A laide de ces u transformations on tend ` rendre larbre le plus rgulier possible dans un sens qui est mesur a e e par un param`tre dpendant en gnral de la hauteur. Une famille darbres satisfaisant une e e e e condition de rgularit est appele une famille darbres quilibrs. Plusieurs esp`ces de tels e e e e e e arbres ont t dvelopps, notamment les arbres AVL, les arbres 2-3, les arbres rouge et noir, ee e e ainsi quune myriade de variantes. Dans les langages comme Java ou C++, des modules de gestion densembles sont prprogramms. Lorsquun ordre total existe sur les lments de ces e e ee ensembles, ils sont en gnral grs, en interne, par des arbres rouge et noir. e e ee

3.1

Arbres AVL

La famille des arbres AVL est nomme ainsi dapr`s leurs inventeurs, Adelson-Velskii et e e Landis, qui les ont prsents en 1962. Au risque de para vieillots, nous dcrivons ces arbres e e tre e plus en dtail parce que leur programmation peut tre mene jusquau bout, et parce que les e e e principes utiliss dans leur gestion se retrouvent dans dautres familles plus complexes. e Un arbre binaire est un arbre AVL si, pour tout nud de larbre, les hauteurs des sous-arbres gauche et droit di`rent dau plus 1. e Rappelons quune feuille est un arbre de hauteur 0, et que larbre vide a la hauteur 1. Larbre vide, et larbre rduit ` une feuille, sont des arbres AVL. e a

3. ARBRES EQUILIBRES 4 3 1 0 0 2 1 0 Fig. 10 Un arbre AVL, avec les hauteurs aux nuds. 1 0 2 0

133

Larbre de la gure 10 porte, dans chaque nud, la hauteur de son sous-arbre. Un autre exemple est fourni par les arbres de Fibonacci, qui sont des arbres binaires An tels que les sous-arbres gauche et droit de An sont respectivement An1 et An2 . Les premiers arbres de Fibonacci sont donns dans la gure 11. e

Fig. 11 Les premiers arbres de Fibonacci. Lintrt des arbres AVL rsulte du fait que leur hauteur est toujours logarithmique en ee e fonction de la taille de larbre. En dautres termes, la recherche, linsertion et la suppression (sous rserve dun ventuel rquilibrage) se font en temps logarithmique. Plus prcisment, on e e ee e e a la proprit que voici. ee Proposition 12 Soit A un arbre AVL ayant n nuds et de hauteur h. Alors log2 (1 + n) avec = 1/ log2 ((1 + 5)/2) 1.44. 1 + h. Soit N (h) le nombre minimum 1+h log2 (2 + n)

Preuve. On a toujours n 2h+1 1, donc log2 (1 + n) de nuds dun arbre AVL de hauteur h. Alors

N (h) = 1 + N (h 1) + N (h 2) car un arbre minimal aura un sous-arbre de hauteur h 1 et lautre sous-arbre de hauteur seulement h2. La suite F (h) = 1+N (h) vrie F (0) = 2, F (1) = 3, F (h+2) = F (h+1)+F (h) e pour h 0, donc 1 F (h) = (h+3 (h+3) ) 5 1 e o` = (1 + 5)/2. Il en rsulte que 1 + n F (h) > 5 (h+3 1), soit en passant au logarithme u en base , h + 3 < log ( 5(2 + n)) < log2 (2 + n)/ log2 + 2. Par exemple, pour un arbre AVL qui a 100000 nuds, la hauteur est comprise entre 17 et 25. Cest le nombre doprations quil faut donc pour rechercher, insrer ou supprimer une donne e e e dans un tel arbre.

134 3.1.1 Rotations

CHAPITRE VI. ARBRES BINAIRES

Nous introduisons maintenant une opration sur les arbres appele rotation. Les rotations e e sont illustres sur la gure 12. Soit A = (B, q, W ) un arbre binaire tel que B = (U, p, V ). La e rotation gauche est lopration e ((U, p, V ), q, W ) (U, p, (V, q, W )) et la rotation droite est lopration inverse. Les rotations gauche (droite) ne sont donc dnies e e que pour les arbres binaires non vides dont le sous-arbre gauche (resp. droit) nest pas vide.

q p W U V rotation droite U

p q

rotation gauche

Fig. 12 Rotation. Remarquons en passant que pour larbre dune expression arithmtique, si les symboles e dopration p et q sont les mmes, les rotations expriment que lopration est associative. e e e Les rotations ont la proprit de pouvoir tre implantes en temps constant (voir ci-dessous), ee e e et de prserver lordre inxe. En dautres termes, si A est un arbre binaire de recherche, tout e arbre obtenu ` partir de A par une suite de rotations gauche ou droite dun sous-arbre de A a reste un arbre binaire de recherche. En revanche, comme le montre la gure 13, la proprit AVL ee nest pas conserve par rotation. e q p U V W V W rotation droite U p q

Fig. 13 Les rotations ne prservent pas la proprit AVL. e ee Pour remdier ` cela, on consid`re une double rotation qui est en fait compose de deux e a e e rotations. La gure 14 dcrit une double rotation droite, et montre comment elle est compose e e dune rotation gauche du sous-arbre gauche suivie dun rotation droite. Plus prcisment, soit e e A = ((U, p, (V, q, W )), r, X) un arbre dont le sous-arbre gauche poss`de un sous-arbre droit. La e double rotation droite est lopration e A = ((U, p, (V, q, W )), r, X) A = ((U, p, V ), q, (W, r, X)) Vrions quelle est bien gale ` la composition de deux rotations. Dabord, une rotation gauche e e a de B = (U, p, (V, q, W )) donne B = ((U, p, V ), q, W ), et larbre A = (B, r, X) devient A = (B , r, X) ; la rotation droite de A donne en eet A . On voit quune double rotation droite

3. ARBRES EQUILIBRES

135

diminue la hauteur relative des sous-arbres V et W , et augmente celle de X. La double rotation gauche est dnie de mani`re symtrique. e e e r p q U V W r q rotation gauche U p W V X rotation droite X double rotation droite U V W X p q q

Fig. 14 Rotations doubles. 3.1.2 Implantation des rotations

Voici une implantation non destructive dune rotation gauche. static Arbre rotationG(Arbre a) // non destructive { Arbre b = a.filsD; Arbre c = new Arbre(a.filsG, a.contenu, b.filsG); return new Arbre(c, b.contenu, b.filsD); } La fonction suppose que le sous-arbre gauche, not b, nest pas vide. La rotation gauche dese tructive est aussi simple ` crire. ae static Arbre rotationG(Arbre a) { Arbre b = a.filsD; a.filsD = b.filsG; b.filsG = a; return b; } // destructive

Les double rotations scrivent par composition. e 3.1.3 Insertion et suppression dans un arbre AVL

Linsertion et la suppression dans un arbre AVL peuvent transformer larbre en un arbre qui ne satisfait plus la contrainte sur les hauteurs. Dans la gure 15, un nud portant ltiquette e 50 est insr dans larbre de gauche. Apr`s insertion, on obtient larbre du milieu qui nest plus ee e AVL. Une double rotation autour de la racine sut ` rquilibrer larbre. a ee

136 71 44 37 61 83 insertion de 50 37 44 61 50 71 83

CHAPITRE VI. ARBRES BINAIRES 61 double rotation 37 44 50 71 83

Fig. 15 Insertion suivie dune double rotation. Cette proprit est gnrale. Apr`s une insertion (respectivement une suppression), il sut ee e e e de rquilibrer larbre par des rotations ou double rotations le long du chemin qui conduit ` la ee a feuille o` linsertion (respectivement la suppression) a eu lieu. Lalgorithme est le suivant : u Algorithme. Soit A un arbre, G et D ses sous-arbres gauche et droit. On suppose que |h(G) h(D)| = 2. Si h(G) h(D) = 2, on fait une rotation droite, mais prcde dune rotation gauche e e e de G si h(g) < h(d) (on note g et d les sous-arbres gauche et droit de G). Si h(G) h(D) = 2 on op`re de faon symtrique. e c e On peut montrer en exercice quil sut dune seule rotation ou double rotation pour rquiee librer un arbre AVL apr`s une insertion. Cette proprit nest plus vraie pour une suppression. e ee 3.1.4 Implantation : la classe Avl

Pour limplantation, nous munissons chaque nud dun champ supplmentaire qui contient e la hauteur de larbre dont il est racine. Pour une feuille par exemple, ce champ a la valeur 0. Pour larbre vide, qui est reprsent par null et qui nest donc pas un objet, la hauteur vaut 1. e e La mthode H sert ` simplier lacc`s ` la hauteur dun arbre. e a e a class { int int Avl Avl contenu; hauteur; filsG, filsD;

Avl(Avl g, int c, Avl d) { filsG = g; contenu = c; filsD = d; hauteur = 1 + Math.max(H(g),H(d)); } static int H(Avl a) { return (a == null) ? -1 : a.hauteur; } static void calculerHauteur(Avl a) { a.hauteur = 1 + Math.max(H(a.filsG), H(a.filsD)); } ... }

3. ARBRES EQUILIBRES

137

La mthode calculerHauteur recalcule la hauteur dun arbre ` partir des hauteurs de ses souse a arbres. Lusage de H permet de traiter de mani`re unie le cas o` lun de ses sous-arbres e e u serait larbre vide. Les rotations sont reprises de la section prcdente. On utilise la version non e e destructive qui rvalue la hauteur. Ces mthodes et les suivantes font toutes partie de la classe ee e Avl. static Avl rotationG(Avl a) { Avl b = a.filsD; Avl c = new Avl(a.filsG, a.contenu, b.filsG); return new Avl(c, b.contenu, b.filsD); } La mthode principale implante lalgorithme de rquilibrage expos plus haut. e ee e static Avl equilibrer(Avl a) { a.hauteur = 1 + Math.max(H(a.filsG), H(a.filsD)); if(H(a.filsG) - H(a.filsD) == 2) { if (H(a.filsG.filsG) <= H(a.filsG.filsD)) a.filsG = rotationG(a.filsG); return rotationD(a); } //else version symtrique e if (H(a.filsG) - H(a.filsD) == -2) { if (H(a.filsD.filsD) <= H(a.filsD.filsG)) a.filsD = rotationD(a.filsD); return rotationG(a); } return a; } Il reste ` crire les mthodes dinsertion et de suppression, en prenant soin de rquilibrer a e e ee larbre ` chaque tape. On reprend simplement les mthodes dj` crites pour un arbre binaire a e e eae de recherche gnral. Pour linsertion, on obtient e e static Avl inserer(int x, Avl a) { if (a == null) return new Avl(null, x, null); if (x < a.contenu) a.filsG = inserer(x, a.filsG); else if (x > a.contenu) a.filsD = inserer(x, a.filsD); return equilibrer(a); //seul changement } La suppression scrit comme suit e static Avl { if (a == return if (x == supprimer(int x, Avl a) null) a; a.contenu)

138

CHAPITRE VI. ARBRES BINAIRES return supprimerRacine(a); if (x < a.contenu) a.filsG = supprimer(x, a.filsG); else a.filsD = supprimer(x, a.filsD); return equilibrer(a); // seul changement } static Avl supprimerRacine(Avl a) { if (a.filsG == null && a.filsD == null) return null; if (a.filsG == null) return equilibrer(a.filsD); if (a.filsD == null) return equilibrer(a.filsG); Avl b = dernierDescendant(a.filsG); a.contenu = b.contenu; a.filsG = supprimer(a.contenu, a.filsG); return equilibrer(a); // seul changement } static Avl dernierDescendant(Avl a) // inchange e { if (a.filsD == null) return a; return dernierDescendant(a.filsD); }

3.2

B-arbres et arbres a-b

Dans cette section, nous dcrivons de mani`re succinte les arbres a-b. Il sagit dune des e e variantes darbres quilibrs qui ont la proprit que toutes leurs feuilles sont au mme niveau, e e ee e les nuds internes pouvant avoir un nombre variable de ls (ici entre a et b). Dans cette catgorie e darbres, on trouve aussi les B-arbres et en particulier les arbres 2-3-4. Les arbres rouge et noir (ou bicolores) sont semblables. Lintrt des arbres quilibrs est quils permettent des modications en temps logarithmique. ee e e Lorsque lon manipule de tr`s grands volumes de donnes, il survient un autre probl`me, ` savoir e e e a lacc`s proprement dit aux donnes. En eet, les donnes ne tiennent pas en mmoire vive, et les e e e e donnes sont donc accessibles seulement sur la mmoire de masse, un disque en gnral. Or, un e e e e seul acc`s disque peut prendre, en moyenne, environ autant de temps que 200 000 instructions. e Les B-arbres ou les arbres a-b servent, dans ce contexte, ` minimiser les acc`s au disque. a e Un disque est divis en pages (par exemple de taille 512, 2048, 4092 ou 8192 octets). La e page est lunit de transfert entre mmoire centrale et disque. Il est donc rentable de grouper e e les donnes par blocs, et de les manipuler de concert. e Les donnes sont en gnral repres par des cls, qui sont ranges dans un arbre. Si chaque e e e ee e e acc`s ` un nud requiert un acc`s disque, on a intrt ` avoir des nuds dont le nombre de ls e a e ee a est voisin de la taille dune page. De plus, la hauteur dun tel arbre qui mesure le nombre dacc`s disques ncessaire est alors tr`s faible. En eet, si chaque nud a de lordre de 1000 e e e ls, il sut dun arbre de hauteur 3 pour stocker un milliard de cls. e Nous considrons ici des arbres de recherche qui ne sont plus binaires, mais darit plus e e grande. Chaque nud interne dun tel arbre contient, en plus des rfrences vers ses sous-arbres, ee

3. ARBRES EQUILIBRES

139

des balises, cest-`-dire des valeurs de cl qui permettent de dterminer le sous-arbre o` se a e e u trouve linformation cherche. Plus prcisment, si un nud interne poss`de d + 1 sous-arbres e e e e A0 , . . . , Ad , alors il est muni de d balises k1 , . . . , kd telles que c0 k1 < c1 kd < cd

pour toute squence de cls (c0 , . . . , cd ), o` chaque ci est une cl du sous-arbre Ai (cf. gure 16). e e u e k1 k2 kd

cls e k1 A0

cls e ]k1 , k2 ] A1

cls e ]kd1 , kd ] Ad1

cls e > kd Ad

Fig. 16 Un nud interne muni de balises et ses sous-arbres. Il en rsulte que pour chercher une cl c, on dtermine le sous-arbre Ai appropri en e e e e dterminant lequel des intervalles ] , k1 ], ]k1 , k2 ],. . .,]kd1 , kd ], ]kd , [ contient la cl. e e 3.2.1 Arbres a-b

Soient a 2 et b 2a 1 deux entiers. Un arbre a-b est un arbre de recherche tel que les feuilles ont toutes la mme profondeur, e la racine a au moins 2 ls (sauf si larbre est rduit ` sa racine) et au plus b ls, e a les autres nuds internes ont au moins a et au plus b ls. Les arbres 2-3 sont les arbres obtenus quand a et b prennent leurs valeurs minimales : tout nud interne a alors 2 ou 3 ls. Les B-arbres sont comme les arbres a-b avec b = 2a 1 mais avec une interprtation e dirente : les informations sont aussi stockes aux nuds internes, alors que, dans les arbres e e que nous considrons, les cls aux nuds internes ne servent qu` la navigation. e e a

8 20 40 6 4 7 10 11 12 14 12 14 18 21 21 26 30 25 30 36 41 42 50

Fig. 17 Un arbre 2-4. Un nud a entre 2 et 4 ls. Larbre de la gure 17 reprsente un arbre 2-4. Les nuds internes contiennent les balises, e et les feuilles contiennent les cls. Lintrt des arbres a-b est justi par la proposition suivante. e ee e Proposition 13 Si un arbre a-b de hauteur h contient n feuilles, alors log n/ log b h < 1 + log(n/2)/ log a .

140

CHAPITRE VI. ARBRES BINAIRES

Preuve. Tout nud a au plus b ls. Il y a donc au plus bh feuilles. Tout nud autre que la racine a au moins a ls, et la racine en a au moins 2. Au total, il y a au moins 2ah1 feuilles, donc 2ah1 n bh . Il rsulte de la proposition prcdente que la hauteur dun arbre a-b ayant n feuilles est en e e e O(log n). La complexit des algorithmes dcrit ci-dessous (insertion et suppression) sera donc e e elle aussi en O(log n). 3.2.2 Insertion dans un arbre a-b

La recherche dans un arbre a-b repose sur le mme principe que celle utilise pour les arbres e e binaires de recherche : on parcourt les balises du nud courant pour dterminer le sous-arbre e dans lequel il faut poursuivre la recherche. Pour linsertion, on commence par dterminer, par e une recherche, lemplacement de la feuille o` linsertion doit avoir lieu. On ins`re alors une u e nouvelle feuille, et une balise approprie : la balise est la plus petite valeur de la cl ` insrer et e ea e de la cl ` sa droite. ea Reste le probl`me du rquilibrage qui se pose lorsque le nombre de ls dun nud dpasse e ee e le nombre autoris. Si un nud a b + 1 ls, alors il est clat en deux nuds qui se partagent e e e les ls de mani`re quitable : le premier nud reoit les (b + 1)/2 ls de gauche, le deuxi`me e e c e les ls de droite. Noter que b + 1 2a, et donc chaque nouveau nud aura au moins a ls. Les balises sont galement partages, et la balise centrale restante est transmise au nud p`re, pour e e e quil puisse ` son tour procder ` linsertion des deux ls ` la place du nud clat. Linsertion a e a a e e 12 8 6 4 7 10 11 12 14 14 15 15 18 21 20 40 21 26 30 25 30 36 41 42 50

Fig. 18 Un arbre 2-4. Un nud a entre 2 et 4 ls. de la cl 15 dans larbre 17 produit larbre de la gure 18. Cette insertion se fait par un double e clatement. Dabord, le nud aux balises 11, 12, 14 de larbre 17 est clat en deux. Mais alors, e e e la racine de larbre 17 a un nud de trop. La racine elle-mme est clate, ce qui fait augmenter e e e la hauteur de larbre. Il est clair que linsertion dune nouvelle cl peut au pire faire clater les nuds sur le e e chemin de son lieu dinsertion ` la racine et la racine elle-mme. Le cot est donc born a e u e logarithmiquement en fonction du nombre de feuilles dans larbre. 3.2.3 Suppression dans un arbre a-b

Comme dhabitude, la suppression est plus complexe. Il sagit de fusionner un nud avec un nud fr`re lorsquil na plus assez de ls, cest-`-dire si son nombre de ls descend au dessous e a de a. Mais si son fr`re (gauche ou droit) a beaucoup de ls, la fusion des deux nuds risque de e conduire ` un nud qui a trop de ls et quil faut clater. On groupe ces deux oprations sous a e e la forme dun partage (voir gure 19).

3. ARBRES EQUILIBRES

141

8 20 6 4 7 10 11 12 21 21 26 30 25 30 36 4 6 7

8 21 20 10 21 25 26 30 30 36

Fig. 19 La suppression de 12 est absorbe par un partage. e Plus prcisment, lalgorithme est le suivant : e e Supprimer la feuille, puis la balise gurant sur le chemin de la feuille ` la racine. a Si les nuds ainsi modis ont toujours a ls, larbre est encore a-b. Si un nud poss`de e e a 1 ls examiner les fr`res adjacents. e Si lun des fr`res poss`de au moins a + 1 ls, faire un partage avec ce fr`re. e e e Sinon, les fr`res adjacents ont a ls, et la fusion avec lun des deux produit un nud e ayant 2a 1 b ls. 8 20 6 6 7 10 11 13 21 21 25 6 6 7 10 8 20 21 21 25

Fig. 20 La suppression de 13 entra la fusion des deux nuds de droite. ne L` encore, le cot est major par un logarithme du nombre de feuilles. Il faut noter quil sagit a u e du comportement dans le cas le plus dfavorable. On peut en eet dmontrer e e Proposition 14 On consid`re une suite quelconque de n insertions ou suppressions dans un e arbre 2-4 initialement vide. Alors le nombre total dclatements, de partage et de fusions est au e plus 3n/2. Ceci signie donc quen moyenne, 1, 5 oprations susent pour rquilibrer larbre, et ce, e ee quel que soit sa taille ! Des rsultats analogues valent pour des valeurs de a et b plus grandes. e

142

CHAPITRE VI. ARBRES BINAIRES

Chapitre VII

Expressions rguli`res e e
1
1.1

Langages rguliers e
Les mots

On se donne un ensemble de caract`res, parfois dnomm alphabet. Lensemble des mots e e e sur , not , est lensemble des suites nies de caract`res. La longueur dun mot est le nombre e e de caract`res de ce mot, cest-`-dire la longueur de la suite qui le dnit. Un langage est un e a e , cest-`-dire un ensemble particulier de mots. sous-ensemble de a Par exemple si est lensemble des lettres usuelles, on peut dnir le langage L1 des mots e franais. Ou encore si est lensemble des chires de 0 ` 9, on peut dnir le langage L2 des c a e reprsentations en base 10 des entiers naturels. Dans le premier cas, on peut tout simplement tene ter de dnir L1 comme lensemble de tous les mots prsents dans le dictionnaire de lAcadmie e e e franaise, dans le second cas, on peut recourir ` une dnition du style un entier (dcimal) est c a e e le chire zro, ou une suite non vide de chires qui ne commence pas par zro . Les expressions e e rguli`res (ou rationnelles) sont une notation tr`s commode pour dnir des langages de mots e e e e du style de L1 et L2 ci-dessus. Dnissons tout de suite quelques notations sur les mots et les langages. Parmi les mots de e on distingue le mot vide not . Le mot vide est lunique mot de longueur zro. La concatnation e e e de deux mots m1 et m2 est le mot obtenu en mettant m1 ` la n de m2 . On note loprateur a e de concatnation, mais on omet souvent cet oprateur et on note donc souvent la concatnation e e e m1 m2 par une simple juxtaposition m1 m2 . La concatnation est une opration associative qui e e poss`de un lment neutre : le mot vide . Dans lcriture m = m1 m2 , m1 est le prxe du e ee e e mot m, tandis que m2 est le suxe du mot m. La concatnation stend naturellement aux ensembles de mots, on note L1 L2 le langage e e obtenu en concatnant tous les mots de L1 avec les mots de L2 . e L1 L2 = {m1 m2 | m1 L1 m2 L2 } Enn on note L le langage obtenu en concatnant les mots de L. e L0 = { } Ln+1 = Ln L L =
iN

Li

Cest-`-dire quun mot m de L est la concatnation de n mots (n 0) m1 , . . . mn , o` les mi a e u sont tous des mots de L.

1.2

Syntaxe des expressions rguli`res e e

Les expressions rguli`res ou motifs, notes p, sont dnies ainsi : e e e e 143

144

` CHAPITRE VII. EXPRESSIONS REGULIERES

Le mot vide est une expression rguli`re. e e Un caract`re c (un lment de lalphabet ) est une expression rguli`re. e ee e e Si p1 et p2 sont des expressions rguli`res, alors lalternative p1 | p2 est une expression e e rguli`re. e e Si p1 et p2 sont des expressions rguli`res, alors la concatnation p1 p2 est une expression e e e rguli`re. e e Si p est une expression rguli`re, alors la rptition p* est une expression rguli`re. e e e e e e On reconna ici la dnition dun arbre, et mme dun arbre de syntaxe abstraite. Il y a deux t e e sortes de feuilles, mot vide et caract`res ; deux sortes de nuds binaires, concatnation et altere e native ; et une sorte de nud unaire, la rptition. Comme dhabitude pour lever les ambigu es e e t dans lcriture linaire dun arbre, on a recours ` des priorits (` savoir, la rptition est plus e e a e a e e prioritaire que la concatnation, elle-mme plus prioritaire que lalternative) et ` des parenth`ses. e e a e Ainsi, pour = {a, b, c}, le motif ab*|ba* est ` comprendre comme ((a)(b)*)|((b)(a)*), a ou plus informatiquement comme larbre de la gure 1. Pour se souvenir des priorits adoptes e e Fig. 1 Larbre de syntaxe abstraite de lexpression ab*|ba* |

a * b b

* a

on peut noter lanalogie avec les expressions arithmtiques : les oprations de concatnation e e e et dalternative ont les mme priorits respectives que la multiplication et laddition, enn la e e rptition se comporte comme la mise ` la puissance. e e a Dans ce polycopi nous exprimons les expressions rguli`res sous la forme programme e e e et non pas dexpression mathmatiques Concr`tement, nous avons crit ab*|ba* et non pas e e e | ba . Ce parti pris entra ab ne que le motif vide ne peut plus tre donn par , si ncessaire e e e nous le reprsenterons donc par exemple comme (). e Avec un peu dhabitude, on comprend nos critures comme des arbres de syntaxe abstraites. e Cela l`ve toutes les ambigu es dentre de jeu. On doit sans doute remarquer quen ralit, nos e t e e e priorits ne l`vent pas toutes les ambigu es. En eet, on peut comprendre abc comme a(bc) ou e e t comme (ab)c, cest-`-dire comme larbre de gauche ou comme larbre de droite de la gure 2. a Au fond cette ambigu e na ici aucune importance, car lopration de concatnation des mots t e e est associative. Il en rsulte que la concatnation des motifs est galement associative. Comme e e e Fig. 2 Deux arbres possibles pour abc

a b


c a

1. LANGAGES REGULIERS nous allons le voir immdiatement, il en va de mme pour lalternative. e e

145

1.3

Smantique des expressions rguli`res e e e

Une expression arithmtique a une valeur (plus gnralement on dit aussi une smantique) : e e e e cest tout simplement lentier rsultat du calcul. De mme, une expression rguli`re a une valeur e e e e qui est ici un ensemble de mots. Il est logique que la dnition de la smantique reprenne e e la structure de la dnition des expressions rguli`res. Cest tout simplement une dnition e e e e . inductive, qui ` chaque expression rguli`re p associe un sous-ensemble [[p]] de a e e [[]] [[c]] [[p1 | p2 ]] [[p1 p2 ]] [[p*]] = = = = = {} {c} [[p1 ]] [[p2 ]] [[p1 ]] [[p2 ]] [[p]]

Cette dnition a le parfum usuel des dnitions de smantique, on a presque rien crit en fait ! e e e e Tout a dj` t dit ou presque dans la section 1.1 sur les mots. Lorsque m appartient au langage eae e dni par p, on dit aussi que le motif p ltre le mot m. Un langage qui peut tre dni comme e e e la valeur dune expression rguli`re est dit langage rgulier. e e e Exemple 1 Nous savons donc que le langage des mots du franais selon lAcadmie est rgulier, c e e puisque quil est dni par une grosse alternative de tous les mots du dictionnaire publi par e e cette institution. Les reprsentations dcimales des entiers sont galement un langage rgulier e e e e dni par : e 0|(1|2|3|4|5|6|7|8|9)(0|1|2|3|4|5|6|7|8|9)* Exercice 1 Montrer quun langage qui contient un nombre ni de mots est rgulier. e Solution. Si L = {m1 , m2 , . . . , mn } est un langage ni contenant n mots, alors L est exactement dcrit par lalternative de tous les mi . e

1.4

Filtrage

La relation de ltrage m [[p ]], galement note p m, peut tre dnie directement par les e e e e r`gles de la gure 3. La dnition de p m est faite selon le formalisme des r`gles dinfrence, e e e e Fig. 3 Dnition de la relation p ltre m e
Seq OrLeft OrRight

Empty

Char

p1

m1 p1 p2

p2 m1 m2

m2

p1 p1 | p2

m m m2

p2 p1 | p2

m m

StarEmpty

StarSeq

p*

m1 p*

p* m1 m2

qui est tr`s courant. Il permet la dnition inductive dun prdicat. Les r`gles se dcomposent e e e e e

146

` CHAPITRE VII. EXPRESSIONS REGULIERES

en axiomes (par ex. Empty) qui sont les cas de base de la dnition, et en r`gles dinfrence e e e proprement dites qui sont les cas inductifs de la dnition. Les r`gles sont ` comprendre comme e e a des implications : quand toutes les prmisses (au dessus du trait) sont vraies, alors la conclusion e (au dessous du trait) est vraie. En encha nant les r`gles on obtient une preuve eective du e prdicat, appele arbre de drivation. Voici par exemple la preuve de ab*|ba* baa. e e e a a b b ba* ab*|ba* a baa a a* a* aa baa a* a

Nous disposons donc de deux moyens dexprimer un langage rgulier, m [[p]] ou p e m. Selon les circonstances, il est plus facile demployer lun ou lautre de des moyens. Par exemple, pour montrer quun motif ltre un mot, les r`gles dinfrences sont souvent commodes. En e e revanche, pour montrer des galits de langages rguliers, il est souvent plus commode de se e e e servir des oprations sur les langages. e Exercice 2 Prouver que pour tout motif p, p* et p** dnissent le mme langage. e e Solution. Pour tout langage on a (L ) = L par dnition de lopration de rptition sur les e e e e langages. Prouver lquivalence p* m p** m est plus pnible. e e

Notations supplmentaires e

En pratique on rencontre diverses notations qui peuvent toutes tre exprimes ` laide des e e a constructions de base des expressions rguli`res. Autrement dit, lemploi de ces notations nauge e mente pas la classe des langages rguliers, mais permet simplement dcrire des expressions e e rguli`res plus compactes. e e Le motif optionnel p ? dni comme p|. e La rptition au moins une fois p+ dnie comme pp*. e e e Le joker, not ., qui est lalternative de tous les caract`res de . On a donc [[.]] = . e e Exemple 2 Ainsi une notation plus conforme ` ce que les langages informatiques acceptent en a fait dentiers dcimaux est : e (0|1|2|3|4|5|6|7|8|9)+ Cest-`-dire une suite non-vide de chires dcimaux, sans chercher ` viter dventuels zros a e a e e e initiaux. Une notation possible pour les entiers relatifs est - ?(0|1|2|3|4|5|6|7|8|9)+ Cest-`-dire un entier naturel prcd ventuellement dun signe moins. a e e ee Il existe galement toute une varit de notations pour des ensembles de caract`res. Ces motifs e ee e sont des abrviations de lalternative notes entre crochets [. . .]. Donc, au sein de ces crochets, e e on trouve une suite des classes de caract`res suivantes : e Un caract`re c se reprsente lui-mme. e e e Un intervalle c1 - c2 reprsente les caract`res dont les codes sont compris au sens large e e entre les codes de c1 et c2 .

2. NOTATIONS SUPPLEMENTAIRES

147

La notation [^. . .] se comprend comme le complmentaire des classes de caract`res donnes. Il e e e est clair que les notations de classes de caract`res peuvent sexprimer comme des alternatives e de motifs caract`res. Dans le cas du complmentaire, cela suppose un alphabet ni, ce qui est e e bien videmment le cas en pratique. e Exemple 3 Une notation compacte pour les entiers dcimaux est [0-9]+. Cela fonctionne parce e que dans tous les codages raisonnables les codes des chires sont conscutifs. De mme, puisquen e e Java par exemple (en ASCII en fait) les codes des lettres sont conscutifs, lexpression rguli`re e e e [a-zA-Z] reprsente toutes les lettres minuscules et majuscules (non-accentues), Tandis que e e [^a-zA-Z] reprsente tous les caract`res qui ne sont pas des lettres. e e Exercice 3 Donner une expression rguli`re compacte qui dcrit la notation adopte pour les e e e e ` savoir, notation dcimale (sans zro initiaux), hexadcimale (introduite par entiers par Java. A e e e 0x, les chires de 10 ` 15 tant reprsentes par les lettres minuscules ou majuscules de a ` f), a e e a et notation octale (introduite par un zro). e Solution. Il y a peu ` dire ` part : a a (0|[1-9][0-9]*)|0x[0-9a-fA-F]+|0[0-7]+ On note que, selon lexpression donne, 0 est dcimal, 00 octal, et 09 interdit. e e Enn certains caract`res spciaux sont exprimables ` laide de notations assez usuelles, du e e a style \n est le caract`re n de ligne et \t est la tabulation etc. Le caract`re barre oblique e e inverse (backslash) introduit les notations de caract`res spciaux, mais permet aussi dexprimer e e les caract`res actifs des expressions rguli`res comme eux-mmes, cest ` dire de les citer. Par e e e e a exemple le caract`re toile se note \* et, bien videmment, le caract`re barre oblique inverse se e e e e note \\. Il nest pas lusage de citer les caract`res quand cest inutile, ainsi [^*] reprsente tous e e les caract`res sauf ltoile. e e Exemple 4 Nous sommes dsormais en possession dun langage de description des ensembles de e mots assez puissant. Par exemple voici le motif qui dcrit une des deux sortes de commentaires e Java. //[^\n]*\n Cest-` dire quun commentaire commence par deux / et se poursuit jusqu` la n de la ligne a a courante. Notons que le motif //.*\n ne convient pas, car il ltre par exemple : //Commentaire avant i++ ; Exercice 4 Donner une expression rguli`re qui dcrit lautre sorte de commentaires de Java. e e e ` A savoir, un commentaire commence par /* et stend jusqu` */. e a Solution. On vise un motif de la forme /\*p\*/.Autrement dit, les commentaires commencent par le sous-mot /* et se terminent par le sous-mot */, et, comme on doit identier le premier sous-mot */ pour fermer le commentaire, le motif p doit correspondre au langage P de tous les mots dont */ nest pas un sous-mot. Notons tout de suite quun mot de P peut se terminer par une suite dtoiles, on a donc p = q\**. Soit Q le langage du motif q, en dnissant Q comme e e lensemble des mots dont aucun sous-mot nest */ et qui ne se terminent pas par une suite non-vide dtoiles. Nous donnons ensuite ` q la forme q = r*, cest un moyen simple de prendre e a

148

` CHAPITRE VII. EXPRESSIONS REGULIERES

en compte que Q contient des mots de longueur arbitraire. Il faut maintenant decomposer un mot (non-vide) m comme un prxe appartenant ` R (langage de r) et un suxe m , tels que e a m est dans Q, si et seulement si m est dans Q. Si m commence par un caract`re c qui nest pas ltoile, alors m se dcompose facilement e e e comme m = cm , et lquivalence est vidente. e e Si m commence par une toile. Commenons par supposer m Q, alors cette toile peut e c e tre suivie dautres, et obligatoirement dun caract`re qui nest ni * ni /, ensuite on trouve e e le suxe m . Autrement dit, on pose un prxe dcrit par \*+[^*/]. Il est alors clair que e e Q. Rciproquement si, m Q, alors le dernier caract`re du prxe nest pas une m e e e toile, et on ne peut pas introduire le sous-mot */ en y ajoutant m , mme si m commence e e par /. Soit r = [^*]|\*+[^*/]. Au nal, la solution est : /\*([^*]|\*+[^*/])*\*+/ La syntaxe concr`te est celle introduite dans les sections prcdentes. Larbre de syntaxe abstraite e e e est donn ` la gure 4. ea Fig. 4 Syntaxe abstraite de /\*([^*]|\*+[^*/])*\*+/

Programmation avec les expressions rguli`res e e

Dans cette section nous nous livrons ` une description rapide de quelques outils qui emploient a les expressions rguli`res de faon cruciale. Le sujet est extrmement vaste, car le formalisme e e c e des expressions rguli`res est susamment expressif pour permettre de nombreuses recherches e e dans les chiers textes, voire de nombreuses transformations de ces chiers. Le livre de Jerey E. F. Friedl [5] traite des expressions rguli`re du point de vue des outils. e e

3.1

Dans linterpr`te de commandes Unix e

Le shell cest ` dire linterpr`te de commandes Unix reconna quelques expressions rguli`res a e t e e quil applique aux noms des chiers du rpertoire courant. La syntaxe concr`te des motifs est e e franchement particuli`re. Notons simplement que ? reprsente un caract`re quelconque (not e e e e prcdemment .), tandis que * reprsente un mot quelconque (not prcdemment .*), et que e e e e e e lalternative se note ` peu pr`s {p1 ,p2 } (pour p1 | p2 ). . . a e Ainsi, on pourra eacer tous les chiers objets Java par la commande : % /bin/rm *.class Dans le mme ordre dide, on peut compter toutes les lignes des chiers source du rpertoire e e e courant : % wc -l *.java La commande wc1 (pour word count) compte les lignes, mots et caract`res dans les chiers e donns en argument. Loption -l restreint lachage au seul compte des lignes. Enn, on e peut faire la liste de tous les chiers du rpertoire courant dont le nom comprend de un ` trois e a caract`res : e % ls {?,??,???}
1

On obtient le dtail du fonctionnement dune commande Unix cmd par man cmd. e

` 3. PROGRAMMATION AVEC LES EXPRESSIONS REGULIERES

149

3.2

Recherche de lignes dans un chier, la commande egrep

La commande egrep motif chier ache toutes les lignes de chier dont un sous-mot (et non pas les lignes elles-mmes) ltre motif. La syntaxe des motifs est relativement conforme ` ce e a que nous avons dj` dcrit. Supposons, comme cest souvent le cas, que le chier /usr/share/ ea e dict/french de votre machine Unix est un dictionnaire franais, donn sous forme dun chier c e texte, ` raison dun mot par ligne. On peut alors trouver les mots qui contiennent au moins six a fois la lettre i de cette mani`re. e % egrep i.*i.*i.*i.*i.*i /usr/share/dict/french indivisibilit e On notera que largument motif est donn entre simples quotes , ceci an dviter que le e e shell ninterpr`te les toiles comme faisant partie dun motif ` appliquer aux noms de chier. Ce e e a nest en fait pas toujours ncessaire, mais cest toujours prudent. e

3.3

En Java

Le support pour les expressions rguli`res est assur par les classes Pattern et Matcher e e e du package java.util.regexp, Les objets de la classe Pattern implmentent les motifs. et e sont construits par la mthode statique Pattern.compile qui prend une cha reprsentant e ne e un motif en argument et renvoie un Pattern. On pourrait penser que la mthode compile e interpr`te la cha donne en argument et produit un arbre de syntaxe abstraite. En fait, e ne e par souci decacit, elle proc`de ` bien plus de travail, jusqu` produire un automate (voir le e e a a chapitre suivant). Pour le moment, il sut de considrer quun Pattern est la forme Java dun e motif. Pour confronter un Pattern p ` un mot m il faut fabriquer cette fois un Matcher en invoa quant la mthode matcher(String text) de p, largument text tant le mot m. En simpliant, e e le Matcher obtenu est donc la combinaison dun motif p et dun mot m, il ore (entre autres !) les mthodes matches() et find() qui renvoient des boolens. La premi`re mthode matches e e e e teste si le motif p ltre le mot m, tandis que la seconde find teste si le motif p ltre un sous-mot du mot m. Ainsi, par exemple, un moyen assez compliqu de savoir si un mot mot contient le e sous-mot sous au moins deux fois est dcrire : e static boolean estSousMotDeuxFois(String sous, String mot) { return Pattern.compile(sous + ".*" + sous).matcher.().find(mot) ; } Nous en savons maintenant assez pour pouvoir crire la commande egrep en Java, la e classe Grep dont le code est donn a la gure 5. Dans ce code, la mthode main se come ` e porte principalement ainsi : elle ouvre le chier dont le nom est donn comme second argument e de la ligne de commande, par new FileReader(arg[1]) ; puis enveloppe le chier comme le BueredReader (voir A.6.2.2) in, ceci an de pouvoir le lire ligne ` ligne ; enn le code appelle a la mthode grep. Cette derni`re, apr`s construction du Pattern pat, lapplique ` toutes les e e e a lignes du chier. En cas de succ`s de lappel ` find, la ligne est ache sur la sortie standard. e a e Le comportement global est donc bien celui de la commande egrep. Larchitecture du package java.util.regex peut para bien complique, et cest tout ` tre e a vrai. Mais. . . Lexistence de la classe Pattern se justie dabord par le souci dabstraction : les auteurs ne souhaitent pas exposer comment sont implments les motifs, an de pouvoir changer e e cette implmentation dans les versions futures de Java. Il y a galement un souci decae e cit, la transformation des cha e nes vers les motifs est coteuse et on souhaite la rentabiliser. u Par exemple dans notre Grep (gure 5), nous appelons Pattern.compile une seule fois et pratiquons de nombreux ltrages.

150

` CHAPITRE VII. EXPRESSIONS REGULIERES

Fig. 5 La commande egrep en Java import java.io.* ; // Pour BufferedReader import java.util.regex.* ; // Pour Pattern et Matcher class Grep { // Affiche les lignes de in dont un sous-mot est filtr par le motif p e static void grep(String p, BufferedReader in) throws IOException { Pattern pat = Pattern.compile(p) ; String line = in.readLine() ; while (line != null) { Matcher m = pat.matcher(line) ; i f (m.find()) { System.out.println(line) ; } line = in.readLine() ; } } public static void main(String [] arg) { i f (arg.length != 2) { System.err.println("Usage: java Grep motif fichier") ; System.exit(2) ; } try { BufferedReader in = new BufferedReader (new FileReader (arg[1])) ; grep(arg[0], in) ; in.close() ; } catch (IOException e) { System.err.println("Malaise: " + e.getMessage()) ; System.exit(2) ; } } }

` 4. IMPLEMENTATION DES EXPRESSIONS REGULIERES

151

Lexistence de la classe Matcher sexplique autrement : les Matcher poss`dent un tat e e interne que les mthodes de ltrage modient. Ceci permet dabord dobtenir des informae tions supplmentaires. Par exemple, si find russit, alors on obtient le sous-mot ltr en e e e appelant la mthode group(). Par ailleurs, lappel suivant ` find recherchera un sous-mot e a ltr, non plus ` partir du dbut, mais au del` du dernier sous-mot identi par find. e a e a e Tout cela permet par exemple dextraire tous les entiers prsents dans une cha e ne. static void allInts(String text) { Matcher m = Pattern.compile("[0-9]+").matcher(text) ; while (m.find()) { System.out.println(m.group()) ; } } On notera quil nest pas immdiat que le code ci-dessus ache bien tous les entiers de e la cha text. En eet si text est par exemple "12" les deux achages 12, ou encore 1 ne puis 2 sont corrects : il nachent que des sous-mots ltrs par le motif "[0-9]+". e Plus gnralement, spcier compl`tement ce que fait le couple de mthode find/group e e e e e est un peu dlicat. La solution ` mon avis la plus satisfaisante est spcier que le sous-mot e a e ltr est dabord le plus ` gauche et ensuite le le plus long. La spcication de Java nest e a e pas aussi gnrale : au lieu de dire globalement ce qui doit tre ltr, elle dcrit leet e e e e e de chaque oprateur des expressions rguli`re individuellement. Ici elle dit que loprateur e e e e + est avide (greedy), cest ` dire quil ltre le plus de caract`res possibles. Dans le cas a e de lexpression rguli`re simple "[0-9]+", cela revient en eet ` ltrer les sous-mots les e e a plus longs. Nous ne dcrirons pas plus en dtail les expressions rguli`res de Java. La documentation du e e e e langage ore de nombreuses prcisions. En particulier, la documentation de la classe Pattern e comprend la description compl`te de la syntaxe des motifs qui est plus tendue que ce que nous e e avons vu ici. Attention tout de mmes les descriptions de motifs sont donnes dans labsolu, alors e e que les motifs sont souvent en pratique dans des cha nes. If faut donc en plus tenir compte des r`gles de citation dans les cha e nes. Ainsi le motif \p{L} ltre nimporte quelle lettre (y compris les lettres accentues) mais on le donne sous la forme de la cha "\\p{L}", car il faut citer un e ne backslash avec un backslash !

Implmentation des expressions rguli`res e e e

Le but de cette section est de dcrire une technique dimplmentation possible des expressions e e rguli`res en Java. Il sagit dune premi`re approche, beaucoup moins sophistique que celle e e e e adopte notamment par la biblioth`que Java. Toutefois, on pourra, mme avec des techniques e e e simples, dj` aborder les probl`mes de programmation poss et comprendre comment a ea e e c marche . De fait nous allons imiter larchitecture du package java.util.regexp et crire nous e aussi un package que nous appelons regex tout court. Nous en protons donc pour crire un package. Tous les chiers source du package regex e commencent par la ligne package regex ; qui identie leur classe comme appartenant ` ce a package. En outre, il est pratique de regrouper ces chiers dans un sous-rpertoire nomm e e justement regex.

4.1

Arbres de syntaxe abstraite

` Nous reprenons les techniques de la section V.4 sur les arbres de syntaxe abstraite. A savoir nous dnissons une classe Re des nuds de larbre de syntaxe abstraite. e

152 package regex ; class Re { private final static int private int tag ; private char asChar ; private Re p1, p2 ; private Re() {} . . . }

` CHAPITRE VII. EXPRESSIONS REGULIERES

EMPTY=0, CHAR=1, WILD=2, OR=3, SEQ=4, STAR=5 ;

Nous dnissons cinq sortes de nuds, la sorte dun nud tant identie par son champ tag. e e e Des constantes nommes identient les cinq sortes de nuds. La correspondance entre constante e et sorte de nud est directe, on note la prsence de nuds WILD qui reprsentent les jokers. e e Ensuite nous dnissons tous les champs ncessaires, un champ asChar utile quand le motif est e e un caract`re (tag CHAR), et deux champs p1 et p2 utiles pour les nuds internes qui ont au plus e deux ls. Enn, le constructeur par dfaut est redni et dclar priv. e e e e e On construira les divers nuds en appelant des mthodes statiques bien nommes. Par e e exemple, pour crer un motif caract`re, on appelle : e e static Re charPat(char c) { // On ne peut pas nommer cette mthode char e Re r = new Re() ; r.asChar = c ; return r ; } Pour crer un motif rptition, on appelle : e e e static Re star(Re p) { Re r = new Re() ; r.p1 = p ; return p ; } Les autres autres mthodes de construction sont videntes. e e Les mthodes statiques de construction ne se limitent videmment pas ` celles qui correse e a pondent aux sortes de nuds existantes. On peut par exemple crire facilement une mthode e e plus qui construit un motif p+ comme pp*. static Re plus(Re p) { return seq(p, star(p)) ; } Du point de vue de larchitecture, on peut remarquer que tous les champs et le constructeur sont privs. Rendre le constructeur priv oblige les utilisateurs de la classe Re appeler les e e mthodes statiques de construction, de sorte quil est garanti que tous les champs utiles dans un e nud sont correctement initialiss. Rendre les champs privs interdira leur acc`s de lextrieur e e e e e e de la classe Re. Au nal, la politique de visibilit des noms est tr`s stricte. Elle renforce la scurit de la programmation, puisque si nous ne modions pas les champs dans la classe Re, e e nous pourrons tre srs que personne ne le fait. En outre, la classe Re nest pas publique, son e u e acc`s est donc limit aux autres classes du package regex. La classe Re est donc compl`tement e e invisible pour les utilisateurs du package.

` 4. IMPLEMENTATION DES EXPRESSIONS REGULIERES

153

4.2

Fabrication des expressions rguli`res e e

Nous prsentons maintenant notre classe Pattern , un modeste remplacement de la classe hoe monyme de la biblioth`que Java. Pour le moment nous vitons les automates et nous contentons e e de cacher un arbre Re dans un objet de la classe Pattern . package regex ; /* Une classe Pattern simple : encapsulage dun arbre de syntaxe abstraite */ public class Pattern { private Re pat ; private Pattern(Re pat) { this.pat = pat ; } // Cha^ne -> Pattern public static Pattern compile(String patString) { Re re = Re.parse(patString) ; return new Pattern(re) ; } // Fabriquer le M atcher public Matcher matcher(String text) { return new Matcher(pat, text) ; } } Comme dans la classe de la biblioth`que, cest la mthode statique compile qui appelle le e e constructeur, ici priv. La partie la plus technique de la tche de la mthode compile est e a e le passage de la syntaxe concr`te contenue dans la cha patString ` la syntaxe abstraite e ne a e ee e a e reprsent par un arbre Re, opration dlgue ` la mthode Re.parse. Nous ne savons pas e e crire cette mthode danalyse syntaxique (parsing). (cours INF 431). Mais ne soyons pas dcus, e e e nous pouvons dj` par exemple construire le motif qui reconna au moins k caract`res c, en ea t e appelant la mthode atLeast suivante, ` ajouter dans la classe Pattern . e a public static atLeast(int k, char c) { return new Pattern(buildAtLeast(k, c)) ; } private static Re buildAtLeast(int k, char c) { i f (k <= 0) { return Re.empty() ; } else i f (k == 1) { return Re.charPat(c) ; } else { return Re.seq (Re.charPat(c), Re.seq(Re.star(Re.wild()), buildAtLeast(k-1, c))) } } Enn, la mthode matcher de de la classe Pattern se contente dappeler le constructeur de e notre modeste classe Matcher, que nous allons dcrire. e

4.3

Filtrage

Le source de la classe Matcher (gure 6) indique que les objets contiennent deux champs pat et text, pour le motif et le texte ` ltrer. Comme on pouvait sy attendre, le constructeur a

154

` CHAPITRE VII. EXPRESSIONS REGULIERES

Fig. 6 Notre classe Matcher package regex ; public class Matcher { private Re pat ; private String text ; // Les recherches commencent ` cette position dans text a private int regStart ; // La derni`re sous-cha^ne filtre est text[mStart...mEnd[ e e private int mStart, mEnd ; Matcher(Re this.pat regStart mStart = } pat, String text) { = pat ; this.text = text ; = 0 ; // Commencer ` filtrer ` partir du dbut a a e mEnd = -1 ; // Aucun motif encore reconnu

// Renvoie la derni`re sous-cha^ne filtre, si il y a lieu e e public String group() { e i f (mStart == -1) throw new Error("Pas de sous-cha^ne filtre") ; return text.substring(mStart, mEnd) ; } // Mthode de recherche des sous-cha^nes filtres a peu pr`s e e e // conforme ` celle des Matcher de java.util.regex a public boolean find() { for (int start = regStart ; start <= text.length() ; start++) for (int end = text.length() ; end >= start ; end--) { i f (Re.matches(text, pat, start, end)) { mStart = start ; mEnd = end ; regStart = mEnd ; // Le prochain find commencera apr`s celui-ci e return true ; } } mStart = mEnd = -1 ; // Pas de sous-cha^ne reconnue regStart = 0 ; // Recommencer au dbut, bizarre e return false ; } }

` 4. IMPLEMENTATION DES EXPRESSIONS REGULIERES

155

Matcher(Re pat, String text) initialise ces deux champs. Mais les objets comportent trois champs supplmentaires, mStart, mEnd et regStart. e La valeur du champ regStart indique lindice dans text du dbut de la recherche suivante, e cest-`-dire o` la mthode find doit commencer ` chercher une sous-cha ltre par pat. a u e a ne e Ce champ permet donc aux appels successifs de find de communiquer entre eux. Les champs mStart et mEnd identient la position de la derni`re sous-cha de text e ne dont un appel ` find a dtermine que le motif pat la ltrait. La convention adopte a e e e est celle de la mthode substring (voir la section A.6.1.3). Les deux champs servent ` la e a communication entre un appel a find et un appel subsquent ` group (voir la n de la ` e a section 3.3). La mthode find est la plus intressante, elle cherche ` identier une sous-cha ltre par e e a ne e pat, ` partir de la position regStart et de la gauche vers la droite. La technique adopte est a e franchement na ve, on essaie tout simplement de ltrer successivement toutes les sous-cha nes commenant ` une positon donne (start) des plus longues ` la cha vide. On renvoie true c a e a ne e ne e e (apr`s mise ` jour de ltat du Matcher), d`s quune sous-cha ltre est trouve. Pour savoir si e a e une sous-cha text[start. . .end[ est ltre, on fait appel ` la mthode statique Re.matches. ne e a e Notons que cest notre parti-pris de rendre privs tous les champs de larbre de syntaxe des e expressions rguli`re qui oblige ` crire toute mthode qui a besoin dexaminer cette structure e e ae e comme une mthode de la classe Re. e Exercice 5 Ecrire la mthode matches de la classe Matcher. On suivra la spcication de la e e ` savoir, lappel matches() teste le ltrage de toute lentre classe Matcher de la biblioth`que. A e e par le motif et on peut utiliser group() pour retrouver la cha ltre. ne e Solution. Cest simple : un appel ` Re.matches et on aecte les champs mStart et mEnd selon a le rsultat. e public boolean matches() { i f (Re.matches(text, pat, 0, text.length())) { mStart = 0 ; mEnd = text.length() ; return true ; } else { mStart = mEnd = -1 ; return false ; } }

Pour crire la mthode matches de la classe Re, nous allons distinguer les divers motifs e e possibles et suivre la dnition de p m de la gure 3. e // Test de pat text[i. . .j[ static boolean matches(String text, Re pat, int i, int j) { switch (pat.tag) { . . . } throw new Error ("Arbre Re incorrect") ; } Notons bien que text[i. . .j[ est la cha dont nous cherchons ` savoir si elle est ltre par ne a e pat. La longueur de cette cha est j-i. Nous crivons maintenant le source du traitement des ne e

156

` CHAPITRE VII. EXPRESSIONS REGULIERES

cinq sortes de motifs possibles, cest ` dire la liste des cas du switch ci-dessus. Le cas des motifs a vide, des caract`res et du joker est rapidement rgl. e e e case EMPTY: return i == j ; case CHAR: return i+1 == j && text.charAt(i) == pat.asChar ; case WILD: return i+1 == j ; En eet, le motif vide ltre la cha vide et elle seule (j i = 0), le motif caract`re ne ltre ne e que la cha compose de lui mme une fois, et le joker ltre toutes les cha ne e e nes de longueur un. Le cas de lalternative est galement assez simple, il sut dessayer les deux termes de e lalternative (regles OrLeft et OrRight). case OR: return matches(text, pat.p1, i, j) || matches(text, pat.p2, i, j) ; La squence (rule Seq) demande plus de travail. En eet il faut essayer toutes les dcompositions e e en prxe et suxe de la cha teste, faute de quoi nous ne pourrions pas renvoyer false avec e ne e certitude. case SEQ: for (int k = i ; k <= j ; k++) { i f (matches(text, pat.p1, i, k) && matches(text, pat.p2, k, j)) return true ; } return false ; Et enn, le cas de la rptition q* est un peu plus subtil, il est dabord clair (r`gle StarEmpty) e e e quun motif q* ltre toujours la cha vide. Si la cha text[i. . .j[ est non-vide alors on ne ne cherche ` la dcomposer en prxe et suxe et ` appliquer la r`gle StarSeq. a e e a e case STAR: i f (i == j) { return true ; } else { for (int k = i+1 ; k <= j ; k++) { i f (matches(text, pat.p1, i, k) && matches(text, pat, k, j)) return true ; } return false ; } On note un point un peu subtil, dans le cas dune cha non-vide, on vite le cas k = j ne e qui correspond ` une division de la cha teste en prxe vide et suxe complet. Si tel ntait a ne e e e pas le cas, la mthode matches pourrait ne pas terminer. En eet, le second appel rcursif e e matches(text, pat, k, j) aurait alors les mmes arguments que lors de lappel. Un autre e point de vue est de considrer que lapplication de la r`gle StarSeq ` ce cas est inutile, dans le e e a sens quon ne risque pas de ne pas pouvoir prouver q* m parce que lon abstient de lemployer. q q* q* m m

Linutilit de cette r`gle est particuli`rement agrante, puisquune des prmisses et la conclusion e e e e sont identiques.

5. UNE AUTRE APPROCHE DU FILTRAGE

157

4.4

Emploi de notre package regex

e Nos classes Pattern et Matcher sont susamment proches de celles de la biblioth`que pour que lon puisse dans le code de la gure 5 changer la ligne import java.util.regex.* en import regex.*, ce qui nous donne le nouveau source regexp. D`s lors ` condition que e a le source des classes du package regex se trouve dans un sous-rpertoire regex, nous pouvons e compiler par javac RegGrep.java et nous obtenons un nouveau programme ReGrep. qui utilise notre implmentation des expressions rguli`res ` la place de celle de la biblioth`que. e e e a e Nous nous livrons ensuite ` des expriences et comparons les temps dexcution (par la a e e commande time Java Grep . . . ). (1) Dans le dictionnaire franais, nous recherchons les mots qui contiennent au moins n fois c la mme voyelle non accentue. Par exemple, pour n = 3 nous excutons la commande : e e e % java Grep (a.*a.*a|e.*e.*e|i.*i.*i|o.*o.*o|u.*u.*u) /usr/share/dict/french Grep ReGrep 1 2.7 3.1 2 2.5 9.7 3 1.9 16.4 4 1.7 17.9 5 1.6 18.6 6 1.5 18.0

On voit que notre technique, sans tre ridicule, est nettement moins ecace. e (2) Toujours dans le dictionnaire franais, nous recherchons les mots qui contiennent n fois c la lettre e, apr`s eacement des accents. Par exemple, pour n = 3 nous excutons la e e commande : % java Grep (e||`|^).*(e||`|^).*(e||`|^) /usr/share/dict/french e e e e e e e e e Grep ReGrep 1 2.9 3.1 2 2.2 9.0 3 1.9 12.8 4 1.7 15.2 5 1.6 15.8 6 1.5 16.0 7 1.5 15.9

Cette exprience donne des rsultats similaires ` la prcdente. Plus prcisment dune part e e a e e e e la biblioth`que est plus rapide en valeur absolue ; et dautre part, nos temps dexcution e e sont croissants, tandis que ceux de la biblioth`que sont dcroissants. Mais et cest impore e tant, il semble bien que les temps se stabilisent dans les deux cas. (3) Nous recherchons une sous-cha ltre par X(.+)+X dans la cha XX= =, o` = = ne e ne u est le caract`re = rpt n fois. Cette reconnaissance doit chouer, mais nous savons [5, e e ee e Chapitre 5] quelle risque de mettre en dicult limplmentation de la biblioth`que. e e e Grep ReGrep 16 0.3 0.2 18 0.5 0.2 20 1.3 0.2 22 4.8 0.2 24 18.6 0.2

Et eectivement lemploi de la biblioth`que conduit ` un temps dexcution manifestement e a e exponentiel. Il est fascinant de constater que notre implmentation ne conduit pas ` cette e a explosion du temps de calcul.

5
5.1

Une autre approche du ltrage


Langage driv e e

Soit L un langage sur les mots et c un caract`re. Nous notons c1 L le langage driv, dni e e e e comme les suxes m des mots de L qui sont de la forme c m. c1 L = {m | c m L}

158

` CHAPITRE VII. EXPRESSIONS REGULIERES

Dans le cas dun langage L rgulier engendr par le motif p, nous allons montrer que le langage e e e c1 L est rgulier en calculant le motif c1 p qui lengendre. On observera dabord que, en toute rigueur, le langage c1 L peut tre vide, par exemple e } avec c = c. Or, selon notre dnition des langages rguliers, le langage vide nest si L = { c e e pas rgulier. Qu` cela ne tienne, nous inventons immdiatement un nouveau motif , qui ne e a e ltre aucun mot. En considrant les valeurs [[p]], on tend facilement les trois oprateurs des e e e expressions rguli`res. e e p = p = |p = p p| = p * =

Ces r`gles nous permettent dliminer les occurrences internes de , de sorte quun motif est e e dsormais ou un motif qui ne contient pas . e Proposition 15 Si L = [[p]], alors le langage c1 L est rgulier, engendr par le motif c1 p e e dni ainsi : e c1 c1 c1 c c1 c 1 (p | p ) c 1 2 c1 (p1 p2 ) c1 (p1 p2 ) c1 p* = = = = = = = = pour c = c 1 p | c1 p c 1 2 (c1 p1 ) p2 si p1 (c1 p1 ) p2 | c1 p2 si p1 (c1 p) p*

Preuve. On montre, par induction sur la structure de p, que pour tout mot m, on a lquivalence : e c1 p m p cm

e ee Seuls les trois derniers cas de la dnition de c1 p prsentent un intrt. e Posons p = p1 p2 et supposons en outre p1 . En revenant aux dnitions, puis par e induction, il vient : m = m1 m2 m = m1 m2 p1 c m1 c1 p1 m1 p1 p2 c m p2 m2 p2 m2 Il reste ` conclure, selon la dnition c1 (p1 p2 ) = (c1 p1 ) p2 applicable ici. a e Dans le cas ou p1 , il faut tenir compte dune autre dcomposition possible du mot c m e en prxe vide et suxe complet. Dans ce cas on a par induction : e p1 p2 Ce qui conduit ` lquivalence : a e m = m1 m2 c1 p1 m1 p2 m2 p1 c1 p2 m cm p1 c1 p2 m

Et on peut conclure.

5. UNE AUTRE APPROCHE DU FILTRAGE Posons p = q*. Alors on a : q* c m m = m1 m2 q c m1 q* m2

159

En toute rigueur, lquivalence rsulte dun nombre arbitraire applications de la r`gle Stare e e Seq pour un prxe vide et dune application de la mme r`gle pour la dcomposition crite e e e e e ci-dessus. On peut ensuite conclure par induction. Par ailleurs, on dcide de la validit de p e e Lemme 16 On note N (p) le prdicat p e N () N () N (c) N (p1 | p2 ) N (p1 p2 ) N (p*) par une simple induction structurelle.

. Le prdicat N (p) se calcule inductivement ainsi : e = = = = = = faux vrai faux N (p1 ) N (P2 ) N (p1 ) N (P2 ) vrai

Preuve. Induction facile sur la structure des motifs.

5.2

Filtrage par la drivation des motifs e

Pour savoir si un motif p ltre un mot m, on peut tout simplement itrer sur les caract`res du e e mot, en calculant les drivations successives de p par les caract`res consomms. Un fois atteint e e e la n du mot, il ne reste qu` vrier si le motif driv ltre le mot vide. a e e e Plus prcisment, les deux rsultats de la section prcdente (proposition 15 et lemme 16) e e e e e nous permettent dcrire deux nouvelles mthodes statiques dans la classe Re. e e // Renvoie le motif c1 p ou null si c1 p est static Re derivate(char c, Re p) ; // Calculer N (p) static boolean nullable(Re p) ; Exercice 6 Programmer ces deux mthodes. On fera attention ` la prsence du motif qui e a e sera reprsent par null et ne se trouvera jamais ` lintrieur des arbres Re. Cest-`-dire que le e e a e a rsultat de Re.derivate est null ou un motif standard. e On peut alors crire par exemple la mthode matches (simplie, sans tenir compte de e e e mStart et mEnd) de la classe Matcher ainsi : public boolean matches() { Re d = pat ; for (int k = 0 ; k < text.length() ; k++) { d = Re.derivate(text.charAt(k), d) ; i f (d == null) return false ; } return Re.nullable(d) ; }

160

` CHAPITRE VII. EXPRESSIONS REGULIERES

Exercice 7 Rcrire la mthode find de la classe Matcher (gure 6) en utilisant cette fois la e e drivation de motifs. On seorcera de limiter le nombre des drivations de motif eectues, tout e e e en identiant la sous-cha ltre la plus ` gauche et la plus longue possible. ne e a Solution. Une solution possible est dintroduire une mthode findLongestPrefix qui cherche e le plus long prxe ltr ` partir de la position start. En cas de succ`s, la mthode renvoie la e ea e e premi`re position non-ltre ; en cas dchec, la mthode renvoie -1. e e e e private int findLongestPrefix(int start) { Re d = pat ; int found = -1 ; e e i f (Re.nullable(d)) found = start ; // Trouv le prfixe vide for (int k = start ; k < text.length() ; k++) { d = Re.derivate(text.charAt(k), d) ; i f (d == null) return found ; i f (Re.nullable(d)) found=k+1 ; // Trouv le prfixe de longueur k+1-start e e } return found ; } La mthode findLongestPrefix calcule simplement les motifs drivs par tous les caract`res e e e e de text ` partir de la position start, en se souvenant (dans found) du ltrage le plus long. La a recherche sarrte quand la cha est lue en totalit ou quand le motif driv est . e ne e e e Ecrire find est ensuite immdiat : chercher un prxe ltr dans tous les suxes possibles e e e de text. public boolean find() { for (int start = regStart ; start <= text.length() ; start++) { int end = findLongestPrefix(start) ; i f (end >= 0) { mStart = start ; mEnd = end ; regStart = mEnd ; return true ; } } mStart = mEnd = -1 ; regStart = 0 ; return false ; } On observe que, pour une position start donne, la nouvelle mthode find trouve bien la plus e e longue sous-cha ltre. Une implantation qui donnerait la plus petite sous-cha ltre est ne e ne e plus simple. private int findShortestPrefix(int start) { Re d = pat ; i f (Re.nullable(d)) return start ; // Trouv le prfixe vide e e for (int k = start ; k < text.length() ; k++) { d = Re.derivate(text.charAt(k), d) ; i f (d == null) return -1 ; i f (Re.nullable(d)) return k+1 ; } return -1 ; // Aucun prfixe ne convient e }

5. UNE AUTRE APPROCHE DU FILTRAGE

161

tre La mesure des temps dexcution de la nouvelle classe Matcher fait appara de meilleurs e performances dans les deux premi`res expriences de la section 4.4 et une consommation tr`s e e e importante de la mmoire dans la derni`re exprience. e e e Exercice 8 Calculer la succession des motifs drivs pour le motif X(.+)+X et le mot XX= = e e et justier le rsultat de la derni`re exprience. e e e Solution. Notons p0 , p1 etc. la suite des motifs drivs Il faut dabord driver deux fois par X. e e e p0 = X(.+)+X p1 = (.+)+X p2 = .*(..*)*X

Pour calculer p2 on a exprim p+ comme pp*. Si on suit la dnition de la drivation on a en toute e e e rigueur des rsultat dirents par exemple, p1 = ()(.+)+X. Mais on enl`ve les motifs () des e e e squences, les motifs sont dj` bien assez compliqus comme cela. Soit le motif q = .*(..*)*, e ea e on a p2 = qX. Par ailleurs la drivation =1 q vaut q | q. Le motif p ltre le mot vide, mais la e drivation de X par = vaut . On a donc : e p3 = (q | q) X Et en posant q1 = q et qn+1 = qn | qn il est clair que lon a : pn+2 = qn+1 X Motif dont la taille est exponentielle en n.

162

` CHAPITRE VII. EXPRESSIONS REGULIERES

Chapitre VIII

Les automates
1 Pourquoi tudier les automates e

Ce chapitre est une tr`s succincte introduction ` la thorie des automates que vous aurez e a e loccasion de voir de faon dtaille si vous choisissez un cursus dinformatique. Ce chapitre est, c e e par nature, un peu plus thorique et un peu moins algorithmique que les prcdents. e e e Les automates sont des objets mathmatiques, tr`s utiliss en informatique, qui permettent e e e de modliser un grand nombre de syst`mes (informatiques). Ltude des automates a commenc e e e e vers la n des annes cinquante. Elle se base sur de nombreuses techniques (topologie, thorie des e e graphes, logique, alg`bre, etc.). De faon tr`s informelle, un automate est un ensemble dtats e c e e du syst`me, relis entre eux par des transitions qui sont marques par des symboles. Etant e e e donn un mot fourni en entre, lautomate lit les symboles du mot un par un et va dtat en e e e tat selon les transitions. Le mot lu est soit accept par lautomate soit rejet. e e e Avant de donner une dnition plus formelle des concepts dcrits ci-dessus, citons quelques e e exemples classiques dutilisation dautomates : vrication de circuits lectroniques, e e recherche doccurrence dans un texte (moteur de recherches sur le web, etc.), vrication de protocoles de communication, e compression de donnes, e compilation, biologie (gnomique). e En dehors de ces utilisations pratiques des automates, notons quils sont aussi utiliss e pour modliser les dispositifs de calculs comme les ordinateurs. Ils sont ` la base de la thorie e a e de la dcidabilit (comprendre ce quun ordinateur peut faire) et de la thorie de la complexit e e e e (comprendre ce quun ordinateur peut faire ecacement). Ils sont donc au cur des mod`les de e linformatique fondamentale.

Rappel : alphabets, mots, langages et probl`mes e

Nous reprenons ici les notations du chapitre VII. Un alphabet est un ensemble de caract`res e (ou symboles). Un mot est une suite nie de caract`res. La longueur dun mot est le nombre de e caract`res de ce mot, cest-`-dire la longueur de la suite qui le dnit. Lensemble des mots sur e a e est not . Un langage est un sous-ensemble de , cest-`-dire un ensemble particulier de e a mots. Parmi les mots de on distingue le mot vide not . Le mot vide est lunique mot de e longueur zro. e 163

164

CHAPITRE VIII. LES AUTOMATES

Automates nis dterministes e


Un automate ni dterministe est un 5-uplet (Q, , , q0 , F ) constitu des lments suivants : e e ee

(1) un alphabet ni , (2) un ensemble ni dtats Q, e (4) un tat de dpart q0 Q, e e (3) une fonction de transition : Q Q,

(5) un ensemble dtats naux (ou acceptant) F Q. e

3.1

Fonctionnement dun automate ni dterministe e

Lautomate prend en entre un mot et laccepte ou la rejette. On dit aussi quil le reconna e t ou ne le reconna pas. Le langage associ ` un automate est constitu de lensemble des mots t ea e quil reconna t. Voici comment lautomate proc`de pour dcider si un mot appartient ` son langage : e e a Le processus commence ` ltat de dpart q0 . a e e Les symboles du mot sont lus les uns apr`s les autres. e ` A la lecture de chaque symbole, on emploie la fonction de transition pour se dplacer e vers le prochain tat (en utilisant ltat actuel et le caract`re qui vient dtre lu). e e e e Le mot est reconnu si et seulement si le dernier tat (i.e., ltat correspondant ` la lecture e e a du dernier caract`re du mot) est un tat de F . e e De faon plus formelle, pour dnir le langage reconnu par un automate, nous devons introc e duire la fonction de transition tendue aux mots. Ce fonction va de Q dans Q. e Elle se dnit rcursivement comme suit : e e a ` partir dun tat q en lisant le mot vide on reste dans ltat q, i.e., e e q Q, (q, ) = q.

Etant donn un mot c se terminant par a (i.e., c = c a avec c {}), et un tat e e q de Q, (q, c a) = ((q, c ), a). Le langage L(A) accept par un automate ni dterministe A = (Q, , , q0 , F ) se dnit e e e alors formellement par L(A) = {c|(q0 , c) F }.

3.2

Des reprsentation compactes des automates e

On peut associer ` un automate une table de transition qui dcrit de mani`re extensive la a e e fonction de transition : Chaque colonne correspond ` un caract`re de lalphabet. a e Une ligne correspond ` un tat de lautomate (ltat initial est prcd dune `che ; a e e e e e e ltat nal dune toile ). e e La valeur (q, a) pour q Q, et a correspond ` ltat indiqu ` lintersection de la ligne q a e ea et de la colonne a. Notons qu` partir de cette table il est ais de retrouver lensemble des tats a e e Q ainsi que lalphabet et donc didentier compl`tement lautomate. e Exemple 1 Considrons la table de transition ci-dessous. e 1 2 a 1 1 b 2 2

3. AUTOMATES FINIS DETERMINISTES

165

Il correspond ` lautomate (Q, , , q0 , F ) avec a Q = {1, 2}, = {a, b}, (1, a) = 1, (1, b) = 2, (2, a) = 1, (2, b) = 2, q0 = 1, F = {2}. Il est facile de voir que le langage de cet automate est constitu exactement des mots composs e e de a et de b qui se terminent par un b. Pour reprsenter de faon intuitive un automate ni dterministe (Q, , , q0 , F ), on peut e c e aussi utiliser une reprsentation graphique. On le reprsente sous la forme dun graphe (de e e transitions) constitu des lments suivants : e ee Lensemble de sommets est Q. Il y a des arcs entre les sommets valus par un symbole de : un arc entre les tats q et e e q valu par le symbole s signie que (q, s) = q . e Ltat initial q0 est marqu par une `che entrante. e e e Les tats naux F sont entours dun double cercle. e e Lautomate de lexemple 1 est ainsi reprsent comme sur la gure 1. e e b a 1 a Fig. 1 Un automate ni dterministe e Pour simplier encore cette reprsentation, un arc entre deux sommets q, q peut tre valu e e e par plusieurs symboles s1 , ..., sn spars par des virgules. Cette derni`re convention signie sime e e plement que i n, (q, si ) = q et elle permet dviter une multiplication darcs sur le graphe. e La gure 2 illustre une telle simplication. b b a a 1 b Fig. 2 Deux reprsentations quivalentes du mme automate ni e e e Exercice 1 Quel est le langage reconnu par lautomate de la gure 2 ? Solution. Tous les mots qui contiennent un b. Exercice 2 Ecrire la table de transition de lautomate suivant. Quel est le langage reconnu ? 0 1 A 0 B Solution. La table de transition de lautomate est 1 C 0, 1 2 a 1 2 a, b 2 b

166 0 B B C 1 A C C

CHAPITRE VIII. LES AUTOMATES

A B C

Cet automate reconna les mots qui contiennent 01. t

Exercice 3 Soit lautomate ni dterministe ({q0 , q1 , q2 , q3 }, {0, 1}, , q0 , {q0 }) donn par la table e e q0 q1 q2 q3 0 q2 q3 q0 q1 1 q1 q0 q3 q2

Dessiner lautomate et montrer quil accepte 110101. Solution. 1 q0 1 0 0 1 q2 1 q3 0 0 q1

Exercice 4 Construire un automate ni dterministe qui reconna le langage e t L = {x {0, 1} |n1 (x) 0 mod 4} o` n1 (x) est le nombre doccurrence du symbole 1 dans le mot x. u 0 0 A Solution. 1 B 1 Exercice 5 Construire les automates nis dterministes qui reconnaissent les langages suivants e L1 = {m (a + b) |chaque a de m est immdiatement prcd et immdiatement suivi dun b}; e e e e e |m contienne ` la fois ab et ba}; L2 = {m (a + b) a L3 = {m (a + b) |m contienne exactement une occurrence de aaa}. 1 C 1 D 0 0

4. AUTOMATES FINIS NON-DETERMINISTES

167

Automates nis non-dterministes e

Un automate ni non-dterministe est un automate tel que, dans un tat donn, il peut y e e e avoir plusieurs transitions avec le mme symbole. e Cette terminologie de non-dterminisme vient du fait que le fonctionnement dun tel aue tomate nest donc pas totalement dtermin , car on ne sait pas quel tat lautomate va e e e choisir. Lintrt est quils orent une grande facilit dexpression, sans perte de puissance : ils peree e mettent de modliser plus facilement des langages complexes, mais peuvent toujours tre convere e tis en des automates nis dterministes. Cependant, ces derniers peuvent tre exponentiellement e e plus grand que les automates non dterministes dont ils sont issus. e Formellement, un automate ni non-dterministe est un 5-uplet (Q, , , q0 , F ) constitu des e e lments suivants : ee (1) un alphabet ni (), (2) un ensemble ni dtats (Q), e (3) une fonction de transition qui associe ` tout tat q Q et tout symbole s un sous a e ensemble de Q not (q, s), e (4) un tat de dpart (q0 ), e e (5) un ensemble dtats naux (ou acceptant) F . e Lunique dirence avec le concept dautomate ni dterministe est donc dans la fonction : e e nest plus une fonction de Q dans Q, mais de Q dans lensemble des parties de Q. Remarquons que tout automate ni dterministe est aussi un automate ni non-dterministe, e e en considrant tout tat comme le singleton rduit ` cet tat. e e e a e Les reprsentations compactes des automates nis dterministes stendent naturellement e e e aux automates nis non-dterministes. Une cellule de la table de transition contient un souse ensemble dtats (ventuellement vide) : voir les exemples qui suivent. e e

4.1

Fonctionnement dun automate ni non-dterministe e

Comme pour un automate ni dterministe, lautomate prend en entre un mot et laccepte e e ou le rejette. Le langage associ est constitu de lensemble des mots quil reconna e e t. Exemple 2 Voici automate qui reconna les mots dnis sur lalphabet {a, b, c} qui comt e mencent par a et qui nissent par c. a c q0 q1 q2

La table associe ` cet automate est alors : e a q0 q1 q2 a {q1 } {q1 }

a, b, c b {q1 } c {q1 , q2 }

Comme pour les automates dterministes, nous introduisons la fonction de transition e tendue aux mots. Cette fonction va de Q dans les parties de Q. e Elle se dnit rcursivement comme suit : e e

168

CHAPITRE VIII. LES AUTOMATES A partir dun tat q en lisant le mot vide on reste dans ltat q, i.e., e e q Q, (q, ) = {q}.

Etant donn un mot c se terminant par a (i.e., c = c a avec c {}), et un tat e e q de Q, (q, c a) = (p, a).
p(q,c )

Autrement dit, plus informellement, un mot est accept sil existe une drivation correspone e dant ` ce mot qui m`ne ` un tat de F : on essaye de lire un ` un les caract`res du mot ; sil y a a e a e a e plusieurs possibilits on en choisit une ; sil ny a aucune possibilit (on est dans ltat q et on lit e e e a avec (q, a) = ), on se bloque. Un mot est accept sil existe une possibilit de lire toutes les e e lettres du mot qui permette darriver sur un tat de F ; sinon (sil nexiste pas de telle possibilit e e de lire tous les caract`res et darriver sur un tat de F ), le mot est rejet. e e e Exercice 6 Construire lautomate ni non-dterministe associ ` la table ci-dessous. e ea 0 1 2 3 a {0, 1, 3} {3} b {2} {3} {1}

Le langage L(A) accept par un automate ni dterministe A = (Q, , , q0 , F ) se dnit e e e alors par L(A) = {c|(q0 , c) F = }.

Solution. 1 a a a 0 b a b 3 b

2 Exercice 7 Construire un automate ni non-dterministe qui reconna les mots qui contiennent e t church ou chomsky. Solution. 0 h c 1 c h 8 9 u 10 r 11 c 12 h 13 2 o 3 m 4 s 5 k 6 y 7

4. AUTOMATES FINIS NON-DETERMINISTES

169

Exercice 8 Construire un automate nis non-dterministe qui reconna les mots de lalphabet e t {a, b} qui terminent par bab. Solution. a b 0 1 a 2 b 3

Exercice 9 Construire un automate nis non-dterministe qui reconna le langage constitu e t e des mots sur lalphabet = {0, 1} tels que le 5`me symbole en partant de la droite soit un 1. e Solution. 0 1 0 1 0, 1 2 0, 1 3 0, 1 4 0, 1 5

On pourra observer dans les trois exemples prcdents quil est beaucoup plus dicile de e e produire directement un automate ni dterministe qui reconna e trait les mme langages. En e particulier, on peut montrer quil nen existe pas avec moins que 32 = 25 tats pour le dernier e exercice [7]. Exercice 10 Construire un automate ni non-dterministe et un automate ni dterministe qui e e reconna les mots sur lalphabet {a, b, c} dcrits par lexpression rguli`re (a + b + c) b(a + b + c). t e e e Exercice 11 Construire un automate ni non-dterministe qui reconna les nombres dont le e t dernier chire nappara quune fois. t Exercice 12 Modlisation dun jeu (dapr`s la page de Jean-Eric Pin). Le joueur a les yeux e e bands. Face ` lui, un plateau sur lequel sont disposs en carr quatre jetons, blancs dun ct e a e e oe et noirs de lautre. Le but du jeu est davoir les quatre jetons du ct blanc. Pour cela, le joueur oe peut retourner autant de jetons quil le souhaite, mais sans les dplacer. A chaque tour, le ma e tre de jeu annonce si la conguration obtenue est gagnante ou pas, puis eectue une rotation du plateau de zro, un, deux ou trois quarts de tours. La conguration de dpart est inconnue du e e joueur, mais le ma de jeu annonce avant le dbut du jeu quelle nest pas gagnante. Chaque tre e annonce prend une seconde, et il faut 3 secondes au joueur pour retourner les jetons. Pouvez-vous aider le joueur ` gagner en moins dune minute ? a

170

CHAPITRE VIII. LES AUTOMATES

4.2

Dterminisation dun automate ni non-dterministe e e

Un automate ni dterministe peut aussi tre vu comme non-dterministe. Donc tout langage e e e reconnu par un automate ni dterministe est reconnu par un automate ni non-dterministe. e e Plus surprenant, la rciproque est aussi vraie : cela constitue le Thor`me de Rabin-Scott : e e e tout langage reconnu par un automate ni non-dterministe est reconnu par un automate ni e dterministe. e Le principe de la preuve est le suivant : considrons un automate ni non-dterministe e e An = (Qn , , n , q0 , Fn ) et construisons un automate ni dterministe Ad = (Qd , , d , {q0 }, Fd ) e qui reconna exactement le mme langage : t e Les alphabets de An et de Ad sont identiques. Les tats de dpart sont respectivement q0 et le singleton {q0 }. e e Qd est constitu de tous les sous-ensembles de Qn . e Fd est lensemble des sous-ensembles de Qn qui contiennent au moins un lment de Fn . ee Etant donn un sous ensemble S de Qn (cest-`-dire un lment de Fd ) et un symbole e a ee a , on dnit la fonction de transition d (S, a) de la mani`re suivante e e d (S, a) =
qS

n (q, a).

Par construction, on a d = n pour les fonctions de transition tendues aux mots correspone dantes ` chacun des automates, et donc les deux automates reconnaissent les mmes langages. a e Nous illustrons le thor`me de Rabin-Scott sur quelques exemples. e e Exemple 3 Reprenons lexemple de lexercice 8. Il sagissait de construire un automate ni non-dterministe reconnaissant les mots de lalphabet {a, b} qui terminent par bab. Lautomate e suivant rpond ` la question. e a a b 0 1 a 2 b 3

b Essayons maintenant de le dterminiser en construisant un nouvel tat ` partir de chaque sous e e a ensemble dtats possible. e a b {0} b {0, 1} a
{1} {2} {3} {0, 2} {0, 3} {1, 2}

a {1, 3} b

b a {0, 1, 3}

{2, 3}

{1, 2, 3} {0, 2, 3}

{0, 1, 2}

Remarquons que les tats {1}, {2}, {3}, {0, 2}, {0, 3}, {1, 2}, {2, 3}, {0, 1, 2}, {1, 2, 3}, {0, 2, 3} e sont inatteignables et peuvent tre retirs de lautomate. e e

4. AUTOMATES FINIS NON-DETERMINISTES

171

En pratique, lors de la conversion, on ne cre pas immdiatement tous les tats de lautomate e e e ni dterministe. Les tats utiles sont cres quand on en a besoin en suivant la mthode de e e e e construction ci-dessous : Qd est initialis ` et soit E un ensemble de parties de Qn initialis ` E = {{q0 }}. ea ea Tant que E est non vide, choisir un lment S de E (S est donc un sous ensemble de Qn ), ee ajouter S ` Qd , a pour tout symbole a , + calculer ltat S = qS n (q, a) ; e + si S nest pas dj` dans Qd , lajouter ` E ; ea a + ajouter un arc sur lautomate entre S et S et le valuer par a. Exercice 13 Dterminiser lautomate de lexercice 7 (long). e

4.3

Les -transitions

Rappelons qu reprsente le mot vide. Une -transition (note sur larc dun automate) e e permet de passer dun tat ` lautre dun automate sans lire de symbole. Cette facilit permet e a e dcrire encore plus facilement des automates complexes. e Une table de transition peut tre associe ` un automate contenant des -transitions. La e e a table est identique ` celle utilise pour un automate ni non-dterministe ` ceci pr`s quon la a e e a e compl`te dune colonne associe au caract`re vide . e e e Exemple 4 Pour illustrer les -transitions, construisons un automate ni non dterministe qui e reconna les nombres dcimaux. Rappelons quun nombre dcimal est un nombre rel qui est t e e e le quotient dun entier relatif par une puissance de dix. Plus prcisment, on souhaite pouvoir e e crire le nombre dcimal en commenant par un + ou un -, suivi dune suite de chires, dune e e c virgule et dune suite de chires. Bien entendu, le + ou le - sont optionnels, la premi`re e cha de chires ne peut pas tre vide et ne commence pas par 0 (sauf si le nombre dcimal ne e e est 0). La seconde cha ne se termine pas par 0. Si seconde cha est vide, on omet la ,. ne ne 0, , 9 A , +, B 1, , 9 0 F 0, , 9 La transition de ltat A ` ltat B est rgie par , +, . Ainsi, on peut passer de A ` B soit en e a e e a lisant +, soit en lisant soit enn, en ne lisant rien. La table de transition associe ` cet automate est alors : e a A B C D E F {B} + {B} {B} , {D} 0 {F } {C} {D} 1 {C} {D, E} 2 {C} {D, E} 9 {C} {D, E} , C D 1, 9 E

Exercice 14 On cherche ` construire un automate qui reconna les mots qui se terminent par a t bab ou qui commencent par aba.

172

CHAPITRE VIII. LES AUTOMATES On sait construire un automate qui reconna les mots qui se terminent par bab (exercice 8) : t a b 0 1 a 2 b 3

b Il est facile de construire un automate qui reconna les mots qui commencent par aba : t a, b a 4 5 b 6 a 7

Il sut alors dassembler ces automates avec une simple -transition : a, b a b i 4 a 5 b 6 a 7 0 1 a 2 b 3

Lintroduction des -transitions ne change pas la nature des langages reconnus par les automates. Comme pour les automates nis non-dterministes que lon peut toujours dterminiser, il est e e toujours possible dliminer les -transitions et obtenir un automate ni dterministe quivalent. e e e Le principe de la construction est une (relativement simple) gnralisation des constructions e e pour passer des automates nis non-dterministes aux automates nis dterministes, que nous e e laissons en exercice ` notre lecteur : voir [7] pour une preuve compl`te. a e

Automates nis et expressions rguli`res e e

Les automates nis et les expressions rguli`res ont la mme expressivit. En eet, le thor`me e e e e e e dquivalence des expressions rguli`res et des automates nis (thor`me de Kleene) tablit e e e e e e que le langage accept par un automate ni correspond toujours ` une expression rguli`re e a e e et rciproquement, tout langage qui correspond ` une expression rguli`re est reconnu par un e a e e automate ni. Autrement dit, combin avec les rsultats prcdents, automates nis dterministes, autoe e e e e mates nis non-dterministes, avec ou sans -transitions, et expressions rguli`res ont exactement e e e la mme puissance dexpressivit : tout langage qui se dnit par lun se dnit par lautre. e e e e Pour prouver le thor`me de Kleene, il faut prouver que lon peut passer dune expression e e rguli`re ` un automate ni, puis que lon peut toujours construire une expression rguli`re e e a e e quivalente au langage reconnu par un automate ni. e

` 5. AUTOMATES FINIS ET EXPRESSIONS REGULIERES

173

5.1

Des expressions rguli`res aux automates nis e e

La premi`re tape, le passage dune expression rguli`re ` un automate ni, se prouve par e e e e a induction. On montre qutant donn un motif p, on peut construire rcursivement un automate qui e e e reconna le langage [[p]]. On va montrer en fait que lon peut le construire avec -transitions, et t ayant un unique tat nal. e Pour le motif , il sut de considrer lautomate : e q0 Pour le motif , il sut de considrer lautomate : e q0

q1

q1

Pour le motif c, o` c , il sut de considrer lautomate : u e q0


c

q1

Et rcursivement, si les motifs p1 et p2 correspondent ` des automates, pour le motif p1 .p2 : e a p1 et pour le motif p1 |p2 :

p2

p1

p2 et enn pour le motif p : 1


p1

o` les rectangles tiquets par p1 et p2 dsignent des automates construits rcursivement pour u e e e e p1 et p2 .

5.2

Des automates nis aux expressions rguli`res e e

Le passage dun automate ni ` une expression rguli`re peut se prouver en utilisant de a e e lalg`bre. e Remarquons tout dabord que les identits (L1 L2 )L = L1 LL2 L et L(L1 L2 ) = LL1 LL2 , e o` L, L1 et L2 sont des langages, incitent ` aussi noter + la disjonction : on peut alors en eet u a crire ces identits comme e e (L1 + L2 )L = L1 L + L2 L L(L1 + L2 ) = LL1 + LL2

174

CHAPITRE VIII. LES AUTOMATES

De mme, les identits L = L = L et {}L = L{} = L incitent ` aussi noter 0 pour e e a , et 1 pour {} : on a alors en eet, L+0 0+L 1L L1 = = = = L L L L

Une structure, avec une opration + associative et commutative et une opration . associative, e e qui vrie ces identits, comme la structure des langages avec la concatnation et la disjonction, e e e est appele une alg`bre de Kleene. e e Passer dun automate ni non dterministe (Q, , , q0 , F ) ` un langage revient ` rsoudre e a a e le syst`me dquations e e Xq = Kp,q Xp + Lq , q Q,
p

o` Kp,q = {a |q (p, a)}, et Lq vaut 0 si q F , 1 sinon. u En eet, les langages Xq constitus des mots w tel que (q, w) F vrient le syst`me e e e prcdent, et rciproquement, toute solution de ce syst`me dnit les tels langages Xq . Le langage e e e e e accept par lautomate est alors clairement donn par Xq0 . e e Exemple 5 Dterminer le langage accept par lautomate e e
a

b a

3 revient ` rsoudre le syst`me a e e

On peut montrer que si un langage K ne contient pas le mot vide, lquation e X = KX + L dinconnue X admet comme unique solution X = K L. Remarquons quen particulier, cela implique que si L et K sont rguliers, alors X lest aussi. e En eet, K L est bien solution, car K(K L) + L = (KK)L + L = (KK + 1)L = K L. Soit Y une autre solution, prouvons que X = Y , pour X = K L. On prouve par rcurrence sur e |w| que w K L implique w Y : Si |w| = 0 (|.| dsigne la longueur) w L, car K. Donc e w KY + L = Y . Si |w| = n + 1, w = w w , avec w K , w L. Puisque w = , |w | n et par hypoth`se de rcurrence w Y , et donc w = w w Y L Y L + L = Y . Donc X Y . De e e faon duale, on montre que Y X. c Lobservation prcdente, permet alors de rsoudre les syst`mes prcdents sur le principe e e e e e e de llimination de Gauss. e Plutt que de traiter le cas gnral, nous allons illustrer le raisonnement sur lexemple 5. o e e Remplaons X3 par aX2 + 1 dans (VIII.1). On obtient c

X1 = aX2 + bX3 X2 = aX1 + bX3 + 1 X3 = aX2 + 1

(VIII.1)

6. UN PEU DE JAVA

175

Remplaons alors X1 par (a + ba)X2 + b, pour obtenir c

= (a + ba)X2 + b X1 = aX2 + b(aX2 + 1) X2 = aX1 + b(aX2 + 1) + 1 = aX1 + baX2 + (b + 1) X3 = aX2 + 1

Rsolvons lquation X2 = (aa + aba + ba)X2 + (ab + b + 1). e e X2 = (aa + aba + ba) (ab + b + 1) X1 = (a + ba)X2 + b X3 = aX2 + 1 Reportons.

X2 = a((a + ba)X2 + b) + baX2 + (b + 1) = (aa + aba + ba)X2 + (ab + b + 1) X1 = (a + ba)X2 + b X3 = aX2 + 1

Le langage reconnu par lautomate de lexemple (5) est donn par X1 , et donc vaut (a + e ba)(aa + aba + ba) (ab + b + 1) + b.

X2 = (aa + aba + ba) (ab + b + 1) X1 = (a + ba)(aa + aba + ba) (ab + b + 1) + b X3 = a(aa + aba + ba) (ab + b + 1) + 1

Un peu de Java

Il est ais de reprsenter les automates nis sous la forme dun programme Java. Notre e e objectif est de : modliser un automate ni non-dterministe (sans -transition), e e et dcrire un programme qui dtermine si une cha de caract`re est reconnue par laue e ne e tomate. Sans perte de gnralit, nous allons supposer que lalphabet est lensemble des caract`res ASCII e e e e et que les tats sont numrots ` partir de 0. e e e a

6.1

Mod`le e

Le mod`le de donnes est alors tr`s simple : e e e Ltat initial de lautomate est indiqu par un entier q0. e e La table de transition delta est un tableau bidimensionnel de listes dentiers (la liste dentier tant ici le moyen le plus simple de reprsenter un ensemble dtats). Ainsi e e e delta[q][c] est la liste des tats atteignables ` partir de q en lisant le caract`re c. e a e Lensemble des tats finaux est une liste dentiers. e Enn, le mot que lon cherche ` reconna est une cha de caract`res String mot. a tre ne e Soit donc en Java les deux classes List et Automate. class Liste { int val; Liste suivant; Liste(int v, Liste x) { val = v; suivant = x;

176 } }

CHAPITRE VIII. LES AUTOMATES

class Automate { // tat initial e int q0; Liste[][] delta; // fonction de transition Liste finaux; // tats finaux e Automate(int q, Liste f, Liste[][]d) { q0 = q; delta = d; finaux = f; } }

6.2

Algorithme de recherche

Nous aurons besoin de quelques fonctions classiques sur les listes // La longueur dune liste static int longueur(Liste x) { i f (x == null) return 0; else return 1 + longueur(x.suivant); } // Le k `me lment e e e static int kieme(Liste x, int k) { i f (k == 1) return x.val; else return kieme(x.suivant, k-1); } static boolean estDans(Liste x, int v) { // Le test dappartenance i f (x == null) return false; else return x.val == v || estDans(x.suivant, v); } La fonction accepter(String mot, Automate a) qui permet de vrier quun mot mot est ace cept par lautomate a appelle la fonction static boolean accepter(String mot, Automate e a, int i, int q). static boolean accepter(String mot, Automate a) { return accepter(mot, a, 0, a.q0); } static boolean accepter(String mot, Automate a, int i, int q) { i f (i == mot.length()) return Liste.estDans(a.finaux, q); else { boolean resultat = false; // le code ASCII du caract`re courant e int c = mot.charAt(i); for (Liste nv_q = a.delta[q][c]; nv_q != null; nv_q = nv_q.suivant) resultat = resultat || accepter(mot, a, i+1, nv_q.val); return resultat; } }

6. UN PEU DE JAVA

177

la fonction static boolean accepter(String mot, int i, Automate a, int q) prend, en plus de lautomate et du mot que lon tudie, deux autres param`tres : e e La position du caract`re courant i dans le mot. e Ltat courant q. e Elle renvoie true si et seulement si le sous-mot de mot dont on a retir les i premiers caract`res e e tait reconnu par un automate semblable ` a dont ltat initial serait q. e a e Remarquons que si i est le dernier caract`re du mot (ce qui correspond au test if (i == e mot.length())) alors il sut de tester lappartenance de q ` a.finaux. Si ce nest pas le cas, on a va essayer demprunter toutes les transitions possibles. on explore ainsi tous les tats nv_q.val e atteignable ` partir de q en lisant c. Une fois dans ltat nv_q.val, on appelle rcursivement a e e accepter. Remarquons que la fonction accepter fonctionne ici par back-tracking : elle essaye toutes les possibilits, et revient en arri`re, pour explorer une autre possibilit, d`s quun chec se produit. e e e e e

6.3

Mise en uvre sur un automate

Considrons lautomate suivant qui accepte toutes les cha e nes qui se terminent par 01. 0 0 1 1 2

0, 1 La table associe ` cet automate est alors : e a 0 1 2 0 {0, 1} 1 {0} {2}

Pour le construire, il nous sut de construire la table de transition. public static void main(String [] arg) { Liste[][] delta = new Liste[3][128]; delta[0][(int)0] = new Liste(0, new Liste(1, null)); delta[0][(int)1] = new Liste(0, null); delta[1][(int)0] = null; delta[1][(int)1] = new Liste(2, null); delta[2][(int)0] = null; delta[2][(int)1] = null; Automate a = new Automate(0, new Liste(2, null), delta); System.out.println("accepter = " + accepter(arg[0], a)); } Remarquons que le code ASCII du caract`re 0 est obtenu par (int)0. e Exercice 15 Comment procder pour coder un automate avec des -transitions ? e

178

CHAPITRE VIII. LES AUTOMATES

Annexe A

Morceaux de Java
Lobjectif de ce chapitre est de rappeler et de prciser certains points cruciaux de la programe mation Java. Nous revenons sur la notion de classe et sur la visibilit des mthodes et variables e e dans la section 1. Nous revenons sur la notion dobjet, leur construction et sur quelques notions lies ` lhritage, dans la section 2. La section 3 prsente un panorama de lensemble des e a e e concepts qui regardent lensemble des langages de programmation, et pas seulement Java. Nous voquons les exceptions Java dans la section 4. La section 5 dcrit comment les entres sorties e e e sont gres en Java. La section 6 prsente quelques classes utiles de la biblioth`que standard ee e e Java. La section 7 est un pot-pourri de quelques astuces et pi`ges Java. e

Un langage plutt classe o

Java est un langage objet avec des classes. Les classes ont une double fonction : structurer les programmes et dnir comment on construit les objets. Pour ce qui est de la seconde fonction, e il nest pas si facile de prciser ce quest exactement un objet dans le cas gnral. Disons quun e e e objet poss`de un tat et des mthodes qui sont des esp`ces de fonctions propres ` chaque objet. e e e e a Par exemple tous les objets poss`dent une mthode toString sans argument et qui renvoie e e une cha reprsentant lobjet et normalement utilisable pour lachage. La section 2 dcrit la ne e e construction des objets ` partir des classes. a Mais dcrivons dabord la structuration des programmes ralise par les classes. Les classes e e e regroupent des membres qui sont soit des variables (plus prcisment des champs), soit des e e mthodes, soit (plus rarement) dautres classes. e

1.1

Un programme minimal

Un programme se construit ` partir de une ou plusieurs classes, dont une au moins contient a une mthode main qui est le point dentre du programme. Une variable ou une mthode qui e e e existent d`s que la classe existe sont dites statiques. e Commenons par un programme en une seule classe. Par exemple, la classe simple suivante c est un programme qui ache Coucou ! sur la console : class Simple { static String msg = "Coucou !" ; // dclaration de variable e

public static void main (String [] arg) // dclaration de mthode e e { System.out.println(msg) ; } } 179

180

ANNEXE A. MORCEAUX DE JAVA

Cette classe ne sert pas ` fabriquer des objets. Elle se sut ` elle mme. Par consquent tout a a e e ce quelle dnit (variable msg et mthode main) est statique. Par re-consquent, toutes les e e e dclarations sont prcdes du mot-cl static, car, par dfaut, si on ne met rien les membres ne e e e e e e sont pas statiques. Si le source est contenu dans un chier Simple.java, il se compile par javac Simple.java et se lance par java Simple. Cest une bonne ide de mettre les classes dans des e chiers homonymes, car cela aide ` sy retrouver. a En termes de programmation objet, la mthode main invoque la mthode println de lobjet e e System.out, avec largument msg. System.out dsigne la variable out de la classe System, e qui fait partie de la biblioth`que standard de Java. e Notons que msg pourrait tre crit Simple.msg, mais dans la classe Simple, on peut se e e passer de rappeler que msg est une variable de la classe Simple, alors autant en proter. Reste ` se demander quel est lobjet rang dans System.out. Et bien, disons que cest un a e objet dune autre classe (la classe PrintStream) qui a t mis l` par le syst`me Java et ne nous ee a e en proccupons plus pour le moment. e

1.2

Complment : la mthode main e e

La dclaration de cette mthode doit obligatoirement tre de la forme : e e e public static void main (String [] arg) En plus dtre statique, la mthode main doit imprativement tre publique (mot-cl public) et e e e e e prendre un tableau de cha en argument. Le sens du mot-cl public est expliqu plus loin. ne e e Le reste des obligations porte sur le type de largument de main (le nom de cet argument est libre). Le tableau de cha est initialis par le syst`me de telle sorte quil contienne les ne e e arguments de la ligne de commande. Par exemple, on peut facilement crire une commande echo en Java.1 e class Echo { public static void main (String [] arg) { for (int i = 0 ; i < arg.length ; i++) { System.out.println(arg[i]); } } } Ce qui nous donne apr`s compilation : e % java Echo bonjour ` toi a bonjour a ` toi

1.3

Collaboration de classe

La classe-programme Simple utilise dj` une autre classe, la classe System crite par les ea e auteurs du syst`me Java. Pour structurer vos programmes, vous pouvez (devez) vous aussi crire e e plusieurs classes. Par exemple, rcrivons le programme simple ` laide de deux classes. Le message e a est fourni par une classe Message class Message { static String msg = "Coucou !" ; }
1

echo est une commande Unix qui ache ses arguments

1. UN LANGAGE PLUTOT CLASSE Tandis que le programme est modi ainsi : e class Simple { e e public static void main (String [] arg) { // dclaration de mthode System.out.println(Message.msg) ; } }

181

Si on met la classe Message dans un chier Message.java, elle sera compile automatiquement e lorsque lon compile le chier Simple.java (par javac Simple.java). Encore une bonne raison pour mettre les classes dans des chiers homonymes.

1.4

Abstraction

Lorsque lon fabrique un programme avec plusieurs classes, lune dentre elles contient la mthode main. Les autres fournissent des services, en gnral sous forme de mthodes accessibles e e e e a ` partir des autres classes. Supposons que la classe Hello doit fournir un message de bienvenue, en anglais ou en franais. c On pourra crire. e class Hello { private static String hello ; static void setEnglish() { hello = "Hello!" ; } static void setFrench() { hello = "Coucou !" ; } static String getHello() { return hello ; } static { setEnglish() ; } } Classe utilise par une nouvelle classe Simple. e class Simple { public static void main (String [] arg) { System.out.println(Hello.getHello()) ; } } La variable hello est prive (mot-cl private) ce qui interdit son acc`s ` partir de code qui e e e a nest pas dans la classe Message. Une mthode getHello est donc fournie, pour pouvoir lire le e message. Deux autres mthodes laissent la possibilit aux utilisateurs de la classe de slectionner e e e le message anglais ou le message franais. Enn, le bout de code static { setEnglish() ; } c est excut lors de la cration de la classe en machine. Il assure le choix initial de la langue du e e e message. Il en rsulte que Hello .hello contient ncessairement un message de bienvenue en e e franais ou en anglais. c La pratique de restreindre autant que possible la visibilit des variables et mthodes amliore e e e a ` la fois la sret et la structuration des programmes. u e Chaque classe propose un service, qui sera maintenu mme si la ralisation de ce service e e change. Il est alors moins risqu de modier la ralisation dun service. En outre, puisque e e seul le code de la classe peut modier les donnes prives, on peut garantir que ces donnes e e e seront dans un certain tat, puisquil sut de contrler le code dune seule classe pour e o sen convaincre. La structure des programmes est plus claire car, pour comprendre comment les diverses classes interagissent, il sut de comprendre les mthodes (et variables) exportes (i.e., e e non-prives) des classes. En fait il sut normalement de comprendre les dclarations des e e mthodes exportes assorties dun commentaire judicieux. e e

182

ANNEXE A. MORCEAUX DE JAVA

On parle dabstraction, la sparation en classe segmente le programme en units plus petites, e e dont on na pas besoin de tout savoir. ` A lintrieur de la classe elle-mme, la dmarche dabstraction revient ` crire plusieurs e e e a e mthodes, chaque mthode ralisant une tche spcique. Les classes elle-mmes peuvent tre e e e a e e e regroupes en packages, qui constituent une nouvelle barri`re dabstraction. La biblioth`que de e e e Java, qui est norme, est structure en packages. e e Le dcoupage en packages, puis en classes, puis en mthodes, qui interagissent selon des e e conventions claires qui disent le quoi et cachent les dtails du comment, est un fondement de la e bonne programmation, cest-`-dire de la production de programmes comprhensibles et donc de a e programmes qui sont (plus) facilement mis au point, puis modis. e

1.5

Niveaux de visibilit e

Il y a en Java quatre niveaux de visibilit, du plus restrictif au plus permissif. e private : visible de la classe. Rien : visible du package. protected visible du package et des classes qui hritent de la classe (voir ci-dessous). e public : visible de partout. Nous connaissons dj` quelques emplois de public. ea Toutes les classes dont les sources sont dans le rpertoire courant sont membres dun e mme package implicite. La classe qui initialise le syst`me dexcution de Java, puis lance e e e le programme de lutilisateur, nest pas membre de ce package. Il est donc logique que main soit dclare public. Noter que dclarer public les autres mthodes de vos classes e e e e na aucun sens, ` moins dtre en train dcrire un package. a e e La dclaration compl`te de la mthode toString des objets est e e e public String toString() ; Et cest logique, puisquil est normal de pouvoir acher un objet de nimporte o` dans le u programme. Quand vous lisez la documentation dune classe de la biblioth`que (par exemple String) e vous avez peut tre dj` remarqu que tout est public (ou tr`s rarement protected). e ea e e

1.6

Reproduction de classe par hritage e

La plus grande part de la puissance de la programmation objet rside dans le mcanisme de e e lhritage. Comme premi`re approche, nous examinons rapidement lhritage et seulement du e e e e point de vue de la classe. Soit une classe Foo qui hrite dune classe Bar. class Foo extends Bar { ... } e e La classe Foo dmarre dans la vie avec toutes les variables et toutes les mthode de la classe Bar. Mais la classe Foo ne va pas se contenter de dmarrer dans la vie, elle peut eectivement e tendre la classe dont elle hrite en se donnant de nouvelles mthodes et de nouvelles variables. e e e Elle peut aussi rednir les mthodes et variables hrites. Par exemple, on peut construire une e e e e classe HelloGoodBye qui ore un message dadieu en plus du message de bienvenue de Hello , et garantit que les deux messages sont dans la mme langue. e class HelloGoodBye extends Hello { private static String goodbye ; static void setEnglish() { Hello.setEnglish() ; goodbye = "Goodbye!" ; }

2. OBSCUR OBJET static void setFrench() { Hello.setFrench() ; goodbye = "Adieu !" ; } static String getGoodBye() { return goodbye ; } static { setEnglish() ; } }

183

On note que deux mthodes sont rednies (setEnglish et setFrench) et quune mthode est e e e ajoute (getGoodBye). La mthode setEnglish ci-dessus doit, pour assurer la cohrence des e e e u deux messages, appeler la mthode setEnglish de la classe Hello , do` lemploi dune notation e compl`te Hello .setEnglish. e Comme dmontr par la nouvelle et derni`re classe Simple, la classe HelloGoodBye a bien e e e reu la mthode getHello en hritage. c e e class Simple { public static void main (String [] arg) { System.out.println(HelloGoodBye.getHello()) ; System.out.println(HelloGoodBye.getGoodBye()) ; } } En fait, lhritage des classes na que peu dintrt en pratique, lhritage des objets est bien e ee e plus utile.

Obscur objet

Dans cette section, nous examinons la dialectique question de la classe et de lobjet.

2.1

Utilisation des objets

Sans mme construire des objets nous mmes, nous en utilisons forcment, car lenvironnee e e ment dexcution de Java est principalement en style objet. Autrement dit on sait dj` que les e ea objets ont des mthodes. e (1) Les tableaux sont presque des objets. (2) Les cha nes String sont des objets. (3) La biblioth`que standard de Java construit des objets dont nous appelons les mthodes, e e comme par exemple lorsque nous crivons out.println(). e Les objets poss`dent aussi des champs galement appels variables dinstance, auxquels on acc`de e e e e par la notation en . comme pour les variables de classes. Par exemple, si t est un tableau t.length est la taille du tableau. La biblioth`que de Java emploie normment les objets. Par exemple, le point est un objet e e e de la classe Point du package java.awt. On cre un nouveau point par un appel de constructeur e dont la syntaxe est : Point p = new Point () ; // Point origine On peut aussi crer un point en donnant explicitement ses coordonnes (enti`res) au construce e e teur : Point p = new Point (100,100) ; On dit que le constructeur de la classe Point est surcharg (overloaded ), cest-`-dire quil y e a a en fait deux constructeurs qui se distinguent par le type de leurs arguments. Les mthodes, e statiques ou non, peuvent galement tre surcharges. e e e On peut ensuite accder aux champs dun point ` laide de la notation usuelle. Les points e a ont deux champs x et y qui sont leurs coordonnes horizontales et verticales : e

184

ANNEXE A. MORCEAUX DE JAVA

i f (p.x == p.y) System.out.println("Le point est sur la diagonale"); Les points poss`dent aussi des mthodes, par exemple la mthode distance, qui calcule la e e e distance euclidienne entre deux points, renvoye comme un e ottant double prcision double. e Un moyen assez compliqu dacher une approximation de 2 est donc e System.out.println(new Point ().distance(new Point (1, 1))) ; Les objets sont relis aux classes de deux faons : e c Lobjet est cr par un constructeur dni dans une classe. Ce constructeur dnit la classe ee e e dorigine de lobjet, et lobjet garde cette classe-l` toute sa vie. a Les classes sont aussi plus ou moins les types des objets, quand nous crivons e Point p = . . . Nous dclarons une variable de type Point. Dans certaines conditions (voir les sections e 2.3 et IV.1.2), la classe-type peut changer au cours de la vie lobjet. Le plus souvent, cela signie quun objet dont la classe reste immuable, peut, dans certaines conditions, tre e rang dans une variable dont le type est une autre classe. e Un premier exemple (assez extrme) de cette distinction est donn par null . La valeur null e e na ni classe dorigine (elle nest pas cr par new), ni champs, ni mthodes, ni rien, mais alors ee e rien du tout. En revanche, null appartient ` toutes les classes-types. a

2.2

Fabrication des objets

Notre ide est de montrer comment crer des objets tr`s similaires aux points de la section e e e prcdente. On cre tr`s facilement une classe des paires (dentiers) de la faon suivante : e e e e c class Pair { int x ; int y ; Pair () { this(0,0) ; } Pair (int x, int y) { this.x = x ; this.y = y ; } double distance(Pair p) { int dx = p.x - this.x ; int dy = p.y - this.y ; return Math.sqrt (dx*dx + dy*dy) ; // Math.sqrt est la racine carre e } } Nous avons partout explicit les acc`s aux variables dinstance et par this .x et this .y. Nous e e nous sommes aussi laisss aller ` employer la notation this (0,0) dans le constructeur sans e a arguments, cela permet de partager le code entre constructeurs. On remarque que les champs x et y ne sont pas introduits par le mot-cl static. Chaque e objet poss`de ses propres champs, le programme e Pair p1 = new Pair (0, 1) ; Pair p2 = new Pair (2, 3) ; System.out.println(p1.x + ", " + p2.y) ; ache 0, 3 , ce qui est somme toute peu surprenant. De mme, la mthode distance e e est propre ` chaque objet, ce qui est logique. En eet, si p est une autre paire, les distances a p1.distance(p) et p2.distance(p) nont pas de raison particuli`res dtre gales. Rien nempche e e e e

2. OBSCUR OBJET

185

de mettre des membres static dans une classe ` crer des objets. Le concepteur de la classe Pair a e peut par exemple se dire quil est bien dommage de devoir crer deux objets si on veut sime plement calculer une distance. Il pourrait donc inclure la mthode statique suivante dans sa e classe Pair . static double distance(int x1, int y1, int x2, int y2) { int dx = x2-x1, dy = y2-y1 ; return Math.sqrt(dx*dx+dy*dy) ; } Il serait alors logique dcrire la mthode distance dynamique plutt ainsi : e e o double distance(Pair p) { return distance(this.x, this.y, p.x, p.y) ; } O` bien sr, distance ( this .x,. . . se comprend comme Pair .distance ( this .x,. . . u u

2.3

Hritage des objets, sous-typage e

Lhritage dans toute sa puissance sera abord au cours suivant INF431. Mais nous pratie e quons dj` lhritage sans le savoir. En eet, les objets des classes que nous crivons ne dmarrent ea e e e pas tout nus dans la vie. Toute classe hrite implicitement (pas besoin de extends, voir la sece tion 1.6) de la classe Object. Les objets de la classe Object (et donc tous les objets) poss`dent e quelques mthodes, dont la fameuse mthode toString. Par consquent, le code suivant est e e e accept par le compilateur, et sexcute sans erreur. Tout se passe exactement comme si nous e e avions crit une mthode toString, alors que cette mthode est en fait hrite. e e e e e Pair p = new Pair (1, 0) ; String repr = p.toString() ; System.out.print(repr) ; Ce que renvoie la mthode toString des Object nest pas bien beau. e Pair@10b62c9 e On reconna le nom de la classe Pair suivi dun nombre en hexadcimal qui est plus ou moins t ladresse dans la mmoire de lobjet dont on a appel la mthode toString. e e e Mais nous pouvons rednir (override) la mthode toString dans la classe Pair . e e // public car il faut respecter la dclaration initiale e public String toString() { return "(" + x + ", " + y + ")" ; } Et lachage de p.toString() produit cette fois ci le rsultat bien plus satisfaisant (1, 0). e Mme si cest un peu bizarre, il nest au fond pas tr`s surprenant que lorsque nous appelons e e la mthode toString de lintrieur de la classe Pair comme nous le faisons ci-dessus, ce soit la e e nouvelle mthode toString qui est appele. Mais crivons maintenant plus directement : e e e System.out.print(p) ; Et nous obtenons une fois encore lachage (1, 0). Or, nous aurons beau parcourir la liste des neuf dnitions de mthodes print surcharges de la classe PrintStream, de print(boolean b) e e e a ` print(Object obj), nous navons bien videmment aucune chance dy trouver une mthode e e print( Pair p). Mais un objet de la classe Pair peut aussi tre vu comme un objet de la classe Object. Cest e le sous-typage : le type des objets Pair est un sous-type du type des objets Object (penser e ne sous-ensemble : Pair Object). En Java, lhritage entra le sous-typage, on dit alors plus synthtiquement que Pair est une sous-classe de Object. e

186

ANNEXE A. MORCEAUX DE JAVA

On note que le compilateur proc`de dabord ` un sous-typage (il se dit quun objet Pair est e a un objet Object), pour pouvoir rsoudre la surcharge (il slectionne la bonne mthode print e e e parmi les neuf possibles). La conversion de type vers un sur-type est assez discr`te, mais on peut e lexpliciter. System.out.print((Object)p) ; // Change le type explicitement Cest donc nalement la mthode print(Object obj) qui est appele. Mais ` lintrieur de e e a e cette mthode, on ne sait pas que obj est en fait un objet Pair . En simpliant un peu, le code e de cette mthode est quivalent ` ceci e e a public void print (Object obj) { i f (obj == null) { this.print("null") ; } else { this.print(obj.toString()) ; } } Cest-`-dire que, au cas de null pr`s, la mthode print(Object obj) appelle print(String s) a e e avec comme argument obj.toString(). Et l`, il y a une petite surprise : cest la mthode a e toString() de la classe dorigine (la vraie classe de obj) qui est appele, et non pas la e mthode toString() des Object. Cest ce que lon appelle parfois la liaison tardive. e

Constructions de base

Nous voquons des concepts qui regardent lensemble des langages de programmation, et pas e seulement les langages objet.

3.1

Valeurs, variables

Par valeur on entend en gnral le rsultat de lvaluation dune expression du langage de e e e e programmation. Si on prend un point de vue mathmatique, une valeur peut tre ` peu pr`s e e a e nimporte quoi, un entier (de Z), un ensemble dentiers etc. Si on prend un point de vue technique, une valeur est ce que lordinateur manipule facilement, ce qui entra des contraintes : par ne exemple, un entier na pas plus de 32 chires binaires (pas plus de 32 bits). Dans les descriptions qui suivent nous entendons valeur plutt dans ce sens technique. Par variable on entend en o gnral (selon le point de vue technique) une case qui poss`de un nom o` peut tre range une e e e u e e valeur. Une variable est donc une portion nomme de la mmoire de la machine. e e 3.1.1 Scalaires et objets

Il y a en Java deux grandes catgories de valeurs les scalaires et les objets. La distinction e est en fait technique, elle tient ` la faon dont ces valeurs sont traites par la machine, ou plus a c e exactement sont ranges dans la mmoire. Les valeurs scalaires se susent ` elle-mmes. Les e e a e valeurs des objets sont des rfrences. Une rfrence pointe vers quelque chose (une zone de ee ee la mmoire) (rfrence est un nom civilis pour pointeur ou adresse en mmoire). Ecrivons par e ee e e exemple int x = 1 ; int [] t = {1, 2, 3} ; Les variables x et t sont deux cases, qui contiennent chacune une valeur, la premi`re valeur e tant scalaire et la seconde une rfrence. Un schma rsume la situation. e ee e e

3. CONSTRUCTIONS DE BASE

187

x 1

t 1 2 3

Le tableau {1, 2, 3} correspond ` une zone de mmoire qui contient des trois entiers, mais la a e valeur qui est range dans la variable t est une rfrence pointant vers cette zone. Le schma e ee e est une simplication de ltat de la mmoire, les zones mmoires apparaissent comme des cases e e e (les variables portent un nom) et les rfrences apparaissent comme des `ches qui pointent vers ee e les cases. Si x et y sont deux variables, la construction y = x se traduit par une copie de la valeur contenue dans la variable x dans la variable y, que cette valeur soit une rfrence ou non. Ainsi, ee le code int y = x ; int [] u = t ; produit ltat mmoire simpli suivant. e e e x 1 y 1 t u 1 2 3 Le schma permet par exemple de comprendre pourquoi (ou plutt comment) le programme e o suivant ache 4. int [] t = {1, 2, 3} ; int [] u = t ; u[1] = 4 ; System.out.println(t[1]) ; Il existe une rfrence qui ne pointe nulle part null, nous pouvons lemployer partout o` une ee u rfrence est attendue. ee int [] t = null ; Dans les schmas nous reprsentons null ainsi : e e t Puisque null ne pointe nulle part il nest pas possible de le drfrencer cest ` dire daller e ee a voir o` il pointe. Un essai par exemple de t[0] dclenche une erreur ` lexcution. u e a e Egalit des valeurs e Loprateur dgalit == de Java sapplique aux valeurs ainsi que loprateur dirence !=. e e e e e Si lgalit de deux scalaire ne pose aucun probl`me, il faut comprendre que == entre deux objets e e e traduit lgalit des rfrences et que deux rfrences sont gales, si et seulement si elles pointent e e ee ee e vers la mme zone de mmoire. Autrement dit, le programme e e int [] t = {1, 2, 3} ; int [] u = t ; int [] v = {1, 2, 3} ; System.out.println("t==u : " + (t == u) + ", t==v : " + (t == v)) ; ache t==u : true, t==v : false. Les rfrences t et u sont gales parce quelles pointent ee e vers le mme objet. Les rfrences t et v qui pointent vers des objets distincts sont distinctes. e ee Cela peut se comprendre si on revient aux tats mmoire simplis. e e e

188

ANNEXE A. MORCEAUX DE JAVA

u 1 2 3

v 1 2 3

On dit parfois que == est lgalit physique. Lgalit physique donne parfois des rsultats sure e e e e prenants. Soit le programme Test simple suivant class Test { public static void main (String [] arg) { String t = "coucou" ; String u = "coucou" ; String v = "cou" ; String w = v + v ; System.out.println("t==u : " + (t == u) + ", t==w : " + (t == w)) ; } } Une fois compil et lanc, ce programme ache t==u : true, t==w : false. Ce qui rv`le e e e e que les cha nes (objets) rfrencs par t et u sont exactement les mmes, tandis que w est une ee e e autre cha ne. t u "coucou" w "coucou"

La plupart du temps, un programme a besoin de savoir si deux cha nes ont exactement les mmes caract`res et non pas si elles occupent la mme zone de mmoire. Il en rsulte principae e e e e lement quil ne faut pas tester lgalit des cha e e nes (et ` vrai dire des objets en gnral) par ==. a e e Dans le cas des cha nes, il existe une mthode equals spcialise (voir 6.1.3) qui compare les e e e cha nes caract`re par caract`re. La mthode equals est lgalit structurelle des cha e e e e e nes.

3.2

Types

Gnralement un type est un ensemble de valeurs. Ce qui ne nous avance pas beaucoup ! e e Disons plutt quun type regroupe des valeurs qui vont naturellement ensemble, parce quelles o ont des reprsentations en machine identiques (byte occupe 8 bits en machine, int en occupe 32), e ou surtout parce quelles vont naturellement ensemble (un objet Point est un point du plan, un objet Object est un objet). 3.2.1 Typage statique

Java est un langage typ statiquement, cest-`-dire que si lon crit un programme incorrect e a e du point de vue des types, alors le compilateur refuse de le compiler. Il sagit non pas dune contrainte irraisonne impose par des informaticiens fous, mais dune aide ` la mise au point e e a des programmes : la majorit des erreurs stupides ne passe pas la compilation. Par exemple, le e programme suivant contient deux erreurs de type : class MalType { static int incr (int i) { return i+1 ; } static void mauvais() { System.out.println(incr(true)) ; // Mauvais type

3. CONSTRUCTIONS DE BASE System.out.println(incr()) ; } } La compilation de la classe MalType choue : e % javac MalType.java MalType.java:9: Incompatible type for method. Cant convert boolean to int. System.out.println(incr(true)) ; // Mauvais type ^ MalType.java:10: No method matching incr() found in class MalType. System.out.println(incr()) ; // Oubli dargument ^ 2 errors // Oubli dargument

189

Le syst`me de types de Java assez puissant et les classes permettent certaines audaces. La e plus courante se voit tr`s bien dans lutilisation de System.out.println (acher une ligne sur e la console), on peut passer nimporte quoi ou presque en argument, spar par des + : e e System.out.println ("boolen :" + (10 < 11) + "entier : " + (314*413)) ; e Cela peut se comprendre si on sait que + est loprateur de concatnation sur les cha e e nes, que System.out.println prend une cha en argument et que le compilateur ins`re des converne e sions l` o` il sent que cest utile. a u Il y a huit types scalaires, ` savoir dabord quatre types entier , byte, short, int et a long. Ces entiers sont en fait des entiers modulo 2p (avec p respectivement gal ` 8, 16, 32 e a p sont centrs autour de zro, cestet 64) les reprsentants des classes dquivalence modulo 2 e e e e a `-dire compris entre 2p1 (inclus) et 2p1 (exclu). On dit aussi que les quatre types entiers correspondent aux entiers reprsentables sur 8, 16, 32 et 64 chires binaires, selon la technique e dite du complment ` la base (loppos dun entier n est 2p n). e a e Les autres types scalaires sont les boolens boolean (deux valeurs true et false ), les e caract`res char et deux sortes de nombres ottants, simple prcision oat et double prcision e e e double. Lconomie de mmoire ralise en utilisant les oat (sur 32 bits) ` la place des e e e e a double (64 bits) na dintrt que dans des cas spcialiss. ee e e Les tableaux sont un cas ` part (voir 3.6). Les classes sont des types pour les objets. Mais a attention, les types sont en fait bien plus une proprit du source des programmes, que des valeurs ee lors de lexcution. Nous essayons dviter de parler du type dun objet. En revanche, il ny e e a aucune dicult ` parler du type dune variable qui peut contenir un objet, ou du type dun ea argument objet. 3.2.2 Conversion de type

La syntaxe de la conversion de type est simple (type)expression La smantique est un peu moins simple. Une conversion de type sadresse dabord au compilateur. e Elle lui dit de changer son opinion sur expression. Par exemple, comme nous lavons dj` vu ea en page 186 Pair p = . . . ; System.out.println((Object)p) ; dit explicitement au compilateur que lexpression p (normalement de type Pair ) est vue comme un Object. Comme Pair est une sous-classe de Object (ce que sait le compilateur), ce changement dopinion est toujours possible et (Object)p ne correspond ` aucun calcul au cours de a

190

ANNEXE A. MORCEAUX DE JAVA

lexcution. En fait, dans ce cas dune conversion vers un sur-type, on peut mme omettre la e e conversion explicite, le compilateur saura changer son opinion tout seul si besoin est. Il nen va pas de mme dans lautre sens e Object o = . . . ; Pair p = (Pair)o ; System.out.println(p.x) ; Le compilateur accepte ce source, la conversion est ncessaire pour le faire changer dopinion sur e la valeur dabord range dans o, puis dans p : cet objet est nalement une paire, et poss`de donc e e une variable dinstance x. Mais ici, rien ne le garantit, et lexpression ( Pair )o correspond ` une a vrication lors de lexcution. Si cette vrication choue, alors le programme choue aussi (en e e e e e lanant lexception ClassCastException). Il est malheureusement parfois ncessaire dutiliser c e de telles conversions (vers un sous-type) voir IV.3.2, mme quand on programme proprement en e ne mlangeant pas des objets de classes distinctes. e On peut aussi convertir le type des scalaires, cette fois les conversion entra nent des calculs, car les reprsentations internes des scalaires ne sont pas toutes identiques. Par exemple si on e change un int (32 bits) en long (64 bits), la machine ralise un certain travail. Ce calcul nest pas e ici une vrication, mais un changement de reprsentation. La plupart des conversions de types e e entre scalaires restent implicites et sont eectues ` loccasion des oprations. Si par exemple e a e on crit 1.5 + 2, alors le compilateur arrive ` comprendre 1.5 + (double)2, an deectuer e a une addition entre double. Il y a un cas o` on ins`re soit-mme ce type de conversions. u e e // a et b sont des int, on veut calculer le pourcentage a/b double p = 100 * (((double)a)/b) ; (Notez labondance de parenth`ses pour bien spcier les arguments de la conversion et des e e oprations.) Si on ne change pas explicitement un des arguments de la division en double, e alors / est la division euclidienne, alors que lon veut ici la division des ottants. On aurait dailleurs pu crire : e double p = (100.0 * a) / b ; En eet, le compilateur change alors a en double, pour avoir le mme type que lautre argument e de la multiplication. Ensuite, la division est entre un ottant et un int , et ce dernier est converti an deectuer la division des ottants. Le compilateur neectue jamais tout seul une conversion qui risque de faire perdre de lin` formation. A la place, quand une telle conversion est ncessaire pour typer le programme, il e choue. Par exemple, prenons la partie enti`re dun double. e e // d est une variable de type double, on veut prendre sa partie enti`re e int e = d ; Le compilateur, assez bavard, nous dit : T.java:4: found : required: int e possible loss of precision double int = d ; ^

Dans ce cas, on doit prendre ses responsabilits et crire e e int e = (int)d ; Il faut noter que nous avons eectivement pris le risque de faire nimporte quoi. Si d est trop gros pour que sa partie enti`re tienne dans 32 bits (suprieure ` 231 1), alors on a un rsultat e e a e trange. Le programme e

3. CONSTRUCTIONS DE BASE double d = 1e100 ; // 10100 System.out.println(d + ", " + (int)d) ; conduit ` acher 1.0E100, 2147483647. a 3.2.3 Complment : caract`res e e

191

Les caract`res de Java sont dnis par deux normes internationales synchronises ISO/e e e CEI 10646 et Unicode. Le nom gnrique le plus appropri semblant tre UTF-16. En simplie e e e ant, une valeur du type char occupe 16 chires binaires et chaque valeur correspond ` un a 16 caract`res et quil faut parcaract`re. Cest simpli parce quen fait Unicode dnit plus de 2 e e e e fois plusieurs char pour faire un caract`re Unicode. Dans la suite nous ne tenons pas compte de e cette complexit supplmentaire introduite notamment pour reprsenter tous les idogrammes e e e e chinois. Un char a une double personnalit, est dune part un caract`re (comme a, etc.) et e e e dautre part un entier sur 16 bits (disons le code du caract`re), qui contrairement ` short est e a toujours positif. La premi`re personnalit dun caract`re se rv`le quand on lache, la seconde e e e e e quand on le compare ` un autre caract`re. Les 128 premiers caract`res (cest-`-dire ceux dont les a e e a codes sont compris entre 0 et 127) correspondent exactement ` un autre standard international a bien plus ancien, lASCII. a e LASCII regroupe notamment les chires de 0 ` 9 et les lettres (non-accentues) minuscules et majuscules, mais aussi un certain nombre de caract`res de contrle, dont les plus e o frquents expriment le retour a la ligne . Une petite digression va nous montrer que la stane ` dardisation du jeu de caract`res nest malheureusement pas susante pour tout normaliser. En e Unix un retour ` la ligne sexprime par le caract`re line feed not \n, en Windows cest la a e e squence dun carriage return not \r et dun line feed, et en Mac OS, cest un carriage ree e turn tout seul ! Le plus souvent ces dtails restent cachs, par exemple System.out.println() e e eectue toujours un retour ` la ligne sur lachage de la console, cest le code de biblioth`que a e qui se charge de fournir les bons caract`res ` la console selon le syst`me sur lequel le programme e a e est en train de sexcuter. Toutefois des probl`mes peuvent surgir en cas de transfert de chiers e e dun syst`me ` lautre. . . e a

3.3

Dclarations e

De faon gnrale, une dclaration tablit une correspondance entre un nom et une construcc e e e e tion nommable (variable, mthode, mais aussi constante camoue en variable). Les dclarations e e e de variable sont de la forme suivante : modiers type name ; Les modiers sont des mots-cls ( static , spcication de visibilit private etc., et nal pour e e e les constantes), le type est un type (genre int , int [] ou String) et name est un nom de variable. Une bonne pratique est dinitialiser les variables d`s leur dclaration, ca vite bien des e e e oublis. Pour ce faire : modiers type name = expression ; O` expression est une expression du bon type. Par exemple, voici trois dclarations de variables, u e de types respectifs entier , cha et tableau de cha : ne ne int i = 0; String message = "coucou" ; String [] jours = {"dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"} ;

192

ANNEXE A. MORCEAUX DE JAVA

Les dclarations de variable sont en fait de trois sortes : e (1) Dans une classe, (a) Une dclaration avec le modicateur static dnit une variable (un champ) de la e e classe. (b) Une dclaration sans le modicateur static dnit une variable dinstance des objets e e de la classe. (2) Dans une mthode, une dclaration de variable dnit une variable locale. Dans ce cas seul e e e le modicateur nal est autoris. e Ces trois sortes de variables sont toutes des cases nommes mais leurs comportements e sont dirents, voire tr`s dirents : les variables locales sont autonomes, et leurs noms obissent e e e e a ` la porte lexicale (voir structure de bloc dans 3.4), les deux autres variables existent e comme sous-parties respectivement dune classe et dun objet ; elles sont normalement dsignes e e comme C.x et o.x, o` C et o sont respectivement une classe et un objet. u Sur une note plus mineure, les variables de classe et dobjet non-initialises explicitement e contiennent en fait une valeur par dfaut qui est fonction de leur type zro pour les types e e dentiers et de ottants (et pour char), false pour les boolean, et null pour les objets. En revanche, comme le compilateur Java refuse lacc`s ` une variable locale quand celle-ci na pas e a t initialise explicitement, ou aecte avant lecture, il ny a pas lieu de parler dinitialisation ee e e par dfaut des variables locales. e Les dclarations de mthode ne peuvent se situer quau niveau des classes et suivent la forme e e gnrale suivante : e e modiers type name (args) Les modiers peuvent tre, static (mthode de classe), une spcication de visibilit, nal e e e e (rednition interdite), synchronized (acc`s en exclusion mutuelle, voir le cours suivant) et e e native (mthode crite dans un autre langage que Java). La partie type est le type du rsultat e e e de la mthode (void si il ny en a pas), name est le nom de la mthode et args sont les e e dclarations des arguments de la mthode qui sont de btes dclarations de variables (sans le e e e e ; nal) spares par des virgules , . Lensemble de ces informations constitue la signature e e de la mthode. e Suit ensuite le corps de la mthode, entre accolades { et } . Il est notable que les e dclarations darguments sont des dclarations de variables ordinaires. En fait on peut voir les e e ` arguments dune mthode comme des variables locales presque normales. A ceci pr`s quil ny e e a pas lieu dinitialiser les arguments ce qui dailleurs naurait aucun sens puisque les arguments sont initialiss ` chaque appel de mthode. e a e

3.4

Principales expressions et instructions

Nous dcrivons maintenant ce qui se trouve dans le corps des mthodes, cest-`-dire le code e e a qui fait le vrai travail. Le corps dune mthode est une squence dinstructions). Les instruce e tions sont excutes. Une instruction (par ex. une aectation) peut inclure une expression. Les e e expressions sont values en un rsultat. e e e La distinction entre expressions (dont lvaluation produit un rsultat) et instructions (dont e e lexcution ne produit rien) est assez hypocrite, car toute expression suivie de ; devient une e instruction (le rsultat est jet). e e Les expressions les plus courantes sont : Constantes Soit 1 (entier), true (boolen), "coucou !" (cha e ne), etc. Une constante amusante est null , qui est un objet sans champs ni mthodes. e Usage de variable Soit x , o` x est le nom dune variable (locale) dclare par ailleurs. u e e

3. CONSTRUCTIONS DE BASE

193

Mot-cl this Dans une mthode dynamique , this dsigne lobjet dont on a appel la mthode. e e e e e Dans un constructeur, this dsigne lobjet en cours de construction. Il en rsulte que this e e nest jamais null , car si on en est arriv ` excuter un corps de mthode, cest bien que ea e e lobjet dont a appel la mthode existait. e e Acc`s ` un champ Si x est un nom de champ et classe un nom de classe C, alors C.x e a dsigne le contenu du champ m de C. De mme objet.x dsigne le champ de nom x e e e de lobjet objet. Notez que, contrairement ` x, objet nest pas forcment un nom, cest a e une expression dont la valeur est un objet. Heureusement ou malheureusement, il existe des notations abrges qui all`gent lcriture (voir 3.5), mais font parfois passer lacc`s ` e e e e e a un champ pour un usage de variable. Appel de mthode Cest un peu comme pour les champs : e statique Soit, C.m(...), dynamique ou bien, objet.m(...). O` m est un nom de mthode et (...) est une squence dexpressions spares par des u e e e e virgules. Notez bien que les mmes notations abrges que pour les champs sappliquent e e e au nom de la mthode. Incidemment, si une mthode a un type de retour void, son appel e e nest pas vraiment une expression, cest une instruction. Appel de constructeur Gnralement de la forme new C (...). La construction des tableaux e e est loccasion de nombreuses variantes, voir 3.6 Usage des oprateurs Par exemple i+1 (addition) ou i == 1 || i == 2 (oprateur galit et e e e e oprateur ou logique). Quelques oprateurs inattendus sont donns en 7.2. Notons quun e e e oprateur en informatique est simplement une fonction dont lapplication se fait par une e syntaxe spciale. Lapplication des oprateurs est souvent inxe, cest-`-dire que loprateur e e a e appara entre ses arguments. Mais elle peut tre prxe, cest-`-dire que loprateur est t e e a e avant son argument, comme dans le cas de la ngation des boolens ! ; ou encore postxe, e e comme pour loprateur de post-incrment i++. En Java, comme souvent, les oprateurs e e e eux-mmes sont exclusivement composs de caract`res non alphabtiques (+, -, =, etc.). e e e e Acc`s dans les tableaux Par exemple t[i], o` t est un tableau dni par ailleurs. Il nest pas e u e surprenant que lon puisse mettre une expression ` la place de i. Il est un peu plus surprea nant que cela soit galement le cas pour t, comme par exemple dans t[i][j], ` comprendre e a comme (t[i])[j] (t est un tableau de tableaux). Aectation Par exemple i = 1, lexpression ` droite de = est calcule sa valeur est range dans a e e la variable donne ` gauche de =. En fait, on peut trouver autre chose quune variable ` e a a gauche de =, on peut trouver tout ce qui dsigne une case de mmoire, par exemple, un e e lment de tableau t[i] ou une dsignation de champ objet.x. ee e Laectation est une expression dont le rsultat est la valeur aecte. Ce qui permet des e e trucs du genre i = j = 0, pour initialiser i et j ` zro. Cela se comprend si on lit cette a e expression comme i = (j = 0). Expression parenthse Si e est une expression, alors (e) est aussi une expression. Cela permet ee essentiellement de contourner les priorits relatives des oprateurs, mais aussi de rendre e e un source plus clair. Par exemple, on peut crire (i == 1) || (i == 2), cest peut-tre e e plus lisible que i == 1 || i == 2. Java a beaucoup emprunt au langage C, il reprend quelques expressions assez peu ordinaires. e Incrment, dcrment Soit i variable de type entier (en fait, soit e dsignation de case mmoire e e e e e qui contient un entier). Alors lexpression i++ range i+1 dans i et renvoie lancienne valeur de i. Lexpression i-- fait la mme chose avec i-1. Enn lexpression ++i (resp. --i) est e similaire, mais elle renvoie la valeur incrmente (resp. dcrmente) de i en rsultat. e e e e e e

194

ANNEXE A. MORCEAUX DE JAVA

Aectations particuli`res La construction i op= expression est sensiblement quivalente ` i = i e e a op expression. Par exemple : i *= 2 range deux fois le contenu de i dans i et renvoie donc ce contenu est doubl. Les nauds e noteront que ++i est aussi i += 1. Ces expressions avances sont gniales, mais il est de bon got de les employer avec parcimonie. e e u Que lon essaie de deviner ce que fait t[++i] *= --t[i++] et on comprendra. Les instructions les plus courantes sont les suivantes : Expression comme une instruction : e ; Evidemment, cette construction nest utile que si e fait des eets de bord (cest-`-dire fait autre chose que rendre son rsultat). Cest bien sr a e u le cas dune aectation. Squence On peut grouper plusieurs instructions en une squence dinstruction, les instructions e e sexcutent dans lordre. e i = i + 1; i = i + 1; Dclarations de variables locales Une dclaration de variable (suivie de ;) est une instruction e e qui rserve de la place pour la variable dclare et linitialise ventuellement. e e e e int i = 0; Il ne faut pas confondre aectation et dclaration, dans le premier cas, on modie le e contenu dune variable qui existe dj`, dans le second on cre une nouvelle variable. ea e Bloc On peut mettre une squence dinstructions dans un bloc {. . . }. La porte des dclaration e e e internes au bloc steint ` la sortie du bloc. Par exemple, le programme e a int i = 0 ; { int i = 1; // Dclaration dun nouvel i e System.out.println("i=" + i); } System.out.println("i=" + i); ache une premi`re ligne i=1 puis une seconde i=0. Il faut bien comprendre que dun e achage ` lautre lusage de variable i ne fait pas rfrence ` la mme variable. a ee a e Lorsque lon veut savoir ` quelle dclaration correspond un usage, la r`gle est de remonter a e e le source du regard vers le haut, jusqu` trouver la bonne dclaration. Cest ce que lon a e appelle parfois, la porte lexicale. e Attention, seule la porte des variables est limite par les blocs, en revanche leet des e e instruction passe all`grement les fronti`res de blocs. Par exemple, le programme e e int i = 0 ; { i = 1; // Affectation de i System.out.println("i=" + i); } System.out.println("i=" + i); ache deux lignes i=1. Retour de mthode On peut dans le corps dune mthode retourner ` tout moment par linse e a truction return expression; , o` expression est une expression dont le type est celui u des valeurs retournes par la mthode. e e Par exemple, la mthode twice qui double son argument entier scrit e e

3. CONSTRUCTIONS DE BASE int twice (int i) { return i+i ; } Si la mthode ne renvoie rien, alors return na pas dargument. e void rien () { return ; }

195

` A noter que, si la derni`re instruction dune mthode est return ; (sans argument), alors e e on peut lomettre. De sorte que la mthode rien scrit aussi : e e void rien () { } Conditionnelle Cest la tr`s classique instruction if : e e i f (i % 2 == 0) { // oprateur modulo System.out.println("Cest pair") ; } else { System.out.println("Cest impair") ; } Instruction switch Cest une gnralisation de la conditionnelle aux types dentiers. Elle permet e e de brancher selon la valeur dun entier (de byte ` long, mais aussi char). Par exemple : a switch (i) { case -1: System.out.println("moins un") ; break ; case 0: e System.out.println("zro") ; break ; case 1: System.out.println("un") ; break ; default: System.out.println("beaucoup") ; } } Selon la valeur de lentier i on slectionnera lune des trois clauses case, ou la clause par e dfaut default . Il faut surtout noter le break, qui renvoie le contrle ` la n du switch. e o a En labsence de break lexcution se poursuit en squence, donc la clause suivante est e e excute. Ainsi si on omet les break et que i vaut -1 on a lachage e e moins un zro e un beaucoup Cette abominable particularit permet de grouper un peu les cas. Par exemple, e switch (i) { case -1: case 0: case 1: System.out.println("peu") ; break ; default: System.out.println("beaucoup") ; }

196

ANNEXE A. MORCEAUX DE JAVA Noter que si la clause se termine par return, alors break nest pas utile. static String estimer(int i) { switch (i) { case -1: return "moins un" ; case 0: return "zro" ; e case 1: return "un" ; default: return "beaucoup" ; } }

Boucle while Cest la boucle la plus classique, celle que poss`dent tous les langages de proe grammation, ou presque. Voici les entiers de zro ` neuf. e a int i = 0 ; while (i < 10) { System.out.println(i) ; i = i+1 ; } Soit while (expression) instruction, on excute expression, qui est une expression boolenne, e e si le rsultat est false cest ni, sinon on excute instruction et on recommence. e e Boucle do Cest la mme chose mais on teste la condition ` la n du tour de boucle, conclusion : e a on passe au moins une fois dans la boucle. Voici une autre faon dacher les entiers de 0 c a ` 9. int i = 0 ; do { System.out.println(i) ; i = i+1 ; } while (i < 10) Boucle for Cest celle du langage C. for (int i=0 ; i < 10 ; i = i+1) System.out.println(i); La syntaxe gnrale est e e for (einit ; econd ; instruction enext )

Lexpression einit est value une fois initialement, exceptionnellement lexpression einit e e peut tre une dclaration de variable, auquel cas la porte de la variable est limite ` la e e e e a boucle. Lexpression econd est de type boolean, cest la condition teste avant chaque e itration (y compris la premi`re), litration a lieu si econd vaut true. Enn, enext est e e e value ` la n de chaque itration. Autrement dit une boucle for est presque la mme e e a e e chose que la boucle while suivante. { einit ; while (econd ) { instruction enext ; } } Enn, notez que econd peut tre omise, en ce cas la condition est considre vraie. Cette e ee particularit est principalement utilise dans lidiome de la boucle innie. e e

3. CONSTRUCTIONS DE BASE for ( ; ; ) { // Boucle infinie, on sort par break ou return ... }

197

Gestion du contrle Certaines instructions permettent de sauter quelque-part de lintrieur o e des boucles. Ce sont des formes polies de goto. Il sagit de break, qui fait sortir de la boucle, et de continue qui commence litration suivante en sautant par dessus ce qui e reste de litration en cours. e Ainsi pour rechercher si lentier foo est prsent dans le tableau t, on peut crire e e boolean trouve = false ; for (int i = 0 ; i < t.length ; i++) { i f (t[i] == foo) { trouve = true ; break ; } } // trouve == true <=> foo est dans t Ou encore, si on veut cette fois compter les occurrences de foo dans t, on peut crire e int count = 0 ; for (int i = 0 ; i < t.length ; i++) { i f (t[i] != foo) continue ; count++ ; } Noter que dans les deux cas on fait des choses un peu compliques. Dans le premier cas, on e pourrait faire une mthode et utiliser return, ou une boucle while sur trouve. Dans le e second cas, on pourrait crire le test dgalit. Dans certaines situations, ces instructions e e e sont pratiques (break plus frquemment que continue). e

3.5

Notations compl`tes et abrges e e e

Pour les classes Quand une classe C est dnie dans un package pkg, son nom est pkg.C. e Il en va dailleurs de mme pour les constructeurs de la classe C. Ainsi, le nom complet de la e classe Point dnie dans le package java.awt est java.awt.Point. On est donc cens crire e ee par exemple java.awt.Point p = new java.awt.Point(100, 50) ; Cest tr`s lourd, pour lviter on peut introduire la dclaration suivante au dbut du chier e e e e source import java.awt.Point ; Et si le nom de classe Point survient plus tard dans le chier, le compilateur sait que lon a voulu dire java.awt.Point. Une autre dclaration possible en dbut de chier est e e import java.awt.* ; Cela revient aux dclarations java.awt.C eectues pour toutes les classes C du package java.awt. e e Pour les champs et les mthodes Une variable (une mthode) dune classe C est normae e lement dsigne en la prxant par C. ; mais dans le code de la classe elle-mme le nom de e e e e la variable sut. De mme, dans le code des mthodes des objets (et des constructeurs) une e e

198

ANNEXE A. MORCEAUX DE JAVA

variable dinstance (une mthode) est normalement dsigne en la prxant par this . ; mais le e e e e nom de la variable (mthode) sut. e Les notations compl`tes permettent parfois dinsister sur la nature dune variable, et de sure monter le masquage des variables dinstance et de classe par des variables locales. Ces masquages ne sont pas toujours malvenus. Par exemple, nous avons tendance ` donner aux constructeurs des a arguments homonymes aux variables dinstance initialises. Ce qui reste possible en explicitant e les variables dinstance en tant que telles. class List { int val ; List next ; List (int val, List next) { this.val = val ; this.next = next ; } } Dans le constructeur ci-dessus, this .val est la variable dinstance, val tout court est largument.

3.6
3.6.1

Les tableaux
Les types

Les tableaux sont presque des objets, presque car mais ils nont pas vraiment de classe, tout se passe plus ou moins comme si, pour chaque type type, un type des tableaux dont les lments ee sont de type type tombait du ciel. Ce type des tableaux dont les lments sont de type type, se ee note (comme dans le langage C) type[] . En particulier on a : int [] ti ; String [] ts ; // tableau dentiers // tableau de cha^nes

Les tableaux ` plusieurs dimensions ou matrices, nexistent pas en tant que tels, ce sont en a fait des tableaux de tableaux. String [] [] tts ; // tableau de tableaux de cha^nes 3.6.2 Valeurs tableaux

Contrairement ` bien des langages, il est possible en Java, de fabriquer des tableaux direca tement. On distingue : La constante null avec laquelle on ne peut rien faire est aussi un tableau dont on ne peut rien faire. Lappel de constructeur : new type [taille], o` type est le type des lments du tableau u ee et taille la taille du tableau. Par exemple, pour dclarer et initialiser un tableau t de trois e entiers, et un tableau ts de trois cha nes, on crit : e static int [] ti = new int [3] ; static String [] ts = new String [3] ; Les lments du tableau sont initialiss par dfaut , ` une valeur qui dpend de leur type ee e e a e zro pour les entiers et ottants, false pour les boolens, et null pour les objets. Cest e e exactement le mme principe que pour les variables de classes et dinstance initialises par e e dfaut (voir 3.3). Ainsi les deux dclarations ci-dessus produisent ltat mmoire simpli e e e e e t 0 0 0 ts La notation par constructeur sapplique aussi aux tableaux de tableaux. static String [] [] t1 = new String [3][3] ; // 3 x 3 cha^nes. Laspect tableau de tableaux appara aussi. Il est possible domettre les derni`res tailles. t e

3. CONSTRUCTIONS DE BASE static String [] [] t2 = new String [3][]

199 ; // 3 tableaux de cha^nes

Ce qui, compte tenu de la r`gle dinitialisation des objets ` null nous donne. e a t1 t2

Un tableau explicite, sous la forme {e1 , e2 ,. . . en } , o` les ei sont des expressions du u type des lments du tableau. Par exemple, pour dclarer et initialiser le tableau ti des ee e seize premiers entiers, on crira : e int [] ti = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16} ; Dans le cas des tableaux de tableaux, on crira : e int [] [] tj = {{1,2}, {3}}; Ce qui nous donne ceci : tj 3 1 2 Autrement dit tj[0] est le tableau {1,2} et tj[1] est le tableau {3}. Notons au passage lexistence du tableau vide (tableau ` zro case) {} ` ne pas confondre avec null . a e a La notation explicite ci-dessus est une esp`ce dabrviation rserve ` linitialisation des e e e e a variables. Dans un autre contexte, il faut indiquer le type du tableau en invoquant le constructeur. Ainsi new int [] {1,2} et new int [] [] {{1,2}, {3}} sont des valeurs tableaux. 3.6.3 Oprations sur les tableaux e

Il y a deux oprations primitives : e Accder ` un lment : t[ei ], o` t est le tableau et ei une expression de type entier. e a ee u Attention, les indices commencent a zro. ` e Trouver la longueur dun tableau : cest une syntaxe objet, tout se passe comme si la longueur tait le champ length du tableau, soit t.length. e La classe de biblioth`que Arrays (package java.util) fournit quelques mthodes toutes stae e tiques qui op`rent sur les tableaux, et notamment des tris. e

3.7

Passage par valeur

La r`gle gnrale de Java est que les arguments sont passs par valeur. Cela signie que e e e e lappel de mthode se fait par copie des valeurs passes en argument et que chaque appel de e e mthode dispose de sa propre version des param`tres. On doit noter que Java est ici parfaitement e e cohrent : si une mthode f poss`de un argument int x, alors lappel f(2) revient ` crer, pour e e e a e la dure de lexcution de cet appel une nouvelle variable (de nom x) qui est initialise ` 2. e e e a Examinons un exemple. static void dedans(int i) { i = i+2 ;

200 System.out.println("i=" + i) } static void dehors(int i) { System.out.println("i=" + i) ; dedans(i) ; System.out.println("i=" + i) ; }

ANNEXE A. MORCEAUX DE JAVA

Lappel de dehors(0) se traduira par lachage de trois lignes i=0, i=2, i=0. Cest tr`s seme blable ` lexemple sur la structure de bloc. Ce qui compte cest la porte des variables. Le rsultat a e e est sans doute moins inattendu si on consid`re cet autre programme rigoureusement quivalent : e e static void dedans(int j) { // j = j+2 ; System.out.println("j=" + j) } change ici i -> j

static void dehors(int i) { System.out.println("i=" + i) ; dedans(i) ; System.out.println("i=" + i) ; } Mais ce que lon doit bien comprendre cest que le nom de largument de dedans, i ou j na pas dimportance, cest une variable muette et heureusement. La r`gle du passage des arguments par valeur sapplique aussi aux objets, mais alors cest e une rfrence vers lobjet qui est copie. Ceci concerne en particulier les tableaux. ee e // Affichage dun tableau (dentiers) pass en argument e static void affiche(int[] t) { for (int i = 0 ; i < t.length ; i++) System.out.print ("t[" + i + "] = " + t[i] + " ") ; System.out.println() ; // sauter une ligne } // trois faons dajouter deux c static void dedans (int[] t) { t[0] = t[0] + 2; t[1] += 2; t[2]++ ; t[2]++ ; } static void dehors () { int[] a = {1,2,3} ; // dclaration et initialisation dun tableau e affiche (a) ; dedans (a) ; affiche (a) ; } Lachage est le suivant : 1 2 3 3 4 5 Ce qui illustre bien quil ny a, tout au cours de lexcution du programme quun seul tableau, e a, t tant juste des noms dirents de ce mme tableau. On peut aussi renommer largument t e e e en a ca ne change rien.

4. EXCEPTIONS

201

Exceptions

Une fois un programme accept par le compilateur, il nest pas garanti quaucune erreur se e produira. Bien au contraire, lexprience nous apprend que des erreurs se produiront. e

4.1

Exceptions lances par le syst`me dexcution e e e

Certaines erreurs empchent lexcution de se poursuivre d`s quelles sont commises. Par e e e exemple, si nous cherchons ` accder ` la quatri`me case dun tableau qui ne comprend que a e a e trois cases, le syst`me dexcution ne peut plus rien faire de raisonnable et il fait chouer le e e e programme. class Test { public static void main(String args[]) int [] a = {2, 3, 5}; System.out.println(a[3]); } } Nous obtenons, % java Test Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 3 at Test.main(Test.java:4) Le message ach nous signale que notre programme a chou ` cause dune exception dont e e e a le nom est ArrayIndexOutOfBoundsException (qui semble ` peu pr`s clair). Lachage a e ore quelques bonus, lindice fautif, et surtout la ligne du programme qui a lanc lexception. e Dautres exemples classiques derreur sanctionne par une exception sont le drfrencement e eee de null (sanctionn par NullPointerException) ou la division par zro (sanctionne par e e e ArithmeticException). Lancer une exception ne signie pas du tout que le programme sarrte immdiatement. On e e peut expliquer le mcanisme ainsi : lexception remplace un rsultat attendu (par exemple le e e rsultat dune division) et ce rsultat exceptionnel est propag ` travers toutes les mthodes e e ea e en attente dun rsultat, jusqu` la mthode main. Le syst`me dexcution de Java ache alors e a e e e un message pour signaler quune exception a t lance. Pour prciser cet eet de propagation ee e e considrons lexemple suivant dun programme Square. e class Square { static int read(String s) { return Integer.parseInt(s); // Lire lentier en base 10, voir 6.1.1 } public static void main(String[] args) int i = read(args[0]); System.out.println(i*i); } } Le programme Square est cens acher le carr de lentier pass sur la ligne de commande. e e e % java Square 11 121 Mais si nous donnons un argument qui nest pas la reprsentation dun entier en base dix, nous e obtenons ceci : { {

202

ANNEXE A. MORCEAUX DE JAVA

% java Square bonga Exception in thread "main" java.lang.NumberFormatException: For input string: "bonga" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:48) at java.lang.Integer.parseInt(Integer.java:447) at java.lang.Integer.parseInt(Integer.java:497) at Square.read(Square.java:3) at Square.main(Square.java:7) O` on voit bien la suite des appels en attente que lexception a du remonter avant datteindre u main. Le mcanisme des exceptions est intgr dans le langage, ce qui veut dire que nous poue e e vons manipuler les exceptions nous-mme. Ici, la survenue de lexception trahit une erreur de e programmation : en raction ` une entre incorrecte le programme devrait normalement avere a e tir lutilisateur et donner une explication. Pour ce faire on attrape lexception ` laide dune a instruction spcique. e public static void main(String[] args) { try { int i = read(args[0]); System.out.println(i*i); } catch (NumberFormatException e) { System.err.println("Usage : java Square nombre") ; } } Et on obtient alors. % java Square bonga Usage : java Square nombre Sans trop rentrer dans les dtails, linstruction try { instrtry } catch ( E e ) { instrfail } e sexcute comme linstruction instrtry , mais si une exception E est lance lors de cette excution, e e e alors linstruction instrfail est excute. En ce cas, lexception attrape dispara Dans linstruce e e t. tion instrfail on peut accder ` lexception attrape par le truchement de la variable e. Cela permet e a e par exemple dacher lexception coupable par e.toString() comme ici, ou plus frquemment e le message contenu dans lexception par e.getMessage(). public static void main(String[] args) { try { int i = read(args[0]); System.out.println(i*i); } catch (NumberFormatException e) { System.err.println("Usage : java Square nombre") ; System.err.println("Echec sur : " + e) ; } } Et on obtient alors % java Square bonga Usage : java Square nombre chec sur : java.lang.NumberFormatException: For input string: "bonga" E Pour tre tout ` fait complet sur lexemple de Square, il y a encore un probl`me. Nous e a e crivons read(arg[0]) sans vrication que le tableau arg poss`de bien un lment. Nous e e e ee devons aussi envisager dattraper une exception ArrayIndexOutOfBoundsException. Une solution possible est dcrire : e

4. EXCEPTIONS private static void usage() { System.err.println("Usage : java Square nombre") ; System.exit(2) ; } public static void main(String[] args) { try { int i = read(args[0]); System.out.println(i*i); } catch (NumberFormatException _) { usage() ; } catch (ArrayIndexOutOfBoundsException _) { usage() ; } }

203

On note quil peut en fait y avoir plusieurs clauses catch. Les deux clauses catch appellent ici la mthode usage, qui ache un bref message rsumant lusage correct du programme et arrte e e e lexcution. e

4.2

Lancer une exception

Lorsque lon crit un composant quelconque, cest-`-dire du code rutilisable par autrui, on se e a e trouve parfois dans la situation de devoir signaler une erreur. Il faut alors procder exactement e comme le syst`me dexcution de Java et lancer une exception. Ainsi lutilisateur de notre e e composant a lopportunit de pouvoir rparer son erreur. Au pire, un message comprhensible e e e sera ach. e Supposons par exemple une classe des piles dentiers. Une tentative de de dpiler sur une e pile vide se solde par une erreur, on lance alors une exception par linstruction throw. int pop() { i f (isEmpty()) throw new Error ("Pop: empty stack") ; ... } Il appara clairement quune exception est un objet dune classe particuli`re, ici la classe Error. t e En raison de sa simplicit nous utilisons systmatiquement Error dans nos exemples. e e Si vous prenez la peine de lire la documentation vous verrez que dans lesprit des auteurs de la biblioth`que, les exceptions Error ne sont pas censes tre attrapes. e e e e An Error [. . . ] indicates serious problems that a reasonable application should not try to catch. Most such errors are abnormal conditions. Pour signaler des erreurs rparables, la documentation encourage plutt la classe Exception, e o ou plus exactement les sous-classes de cette classe. The class Exception and its subclasses [. . . ] indicates conditions that a reasonable application might want to catch. Il est en eet prudent de lancer ses propres exceptions, an dviter les interfrences avec les e e exceptions lances par les mthodes de la biblioth`que. e e e Nous procdons donc en deux temps, dabord dclaration de la classe de notre exception. e e class Stack { . . . static class Empty extends Exception { } . . .

204 }

ANNEXE A. MORCEAUX DE JAVA

e La classe Empty est dnie comme un membre statique de la classe Stack (oui, cest possible), de sorte que lon la dsigne normalement comme Stack.Empty. Ce dtail mis ` part, e e a il sagit dune dclaration de classe normale, mais qui ne poss`de aucun membre en propre. e e Le code indique simplement que Empty est une sous-classe (voir 2.3 et 1.6) de Exception. Un constructeur par dfaut Empty () est implicitement fourni, qui se contente dappeler le e constructeur Exception (). Ensuite, notre exception est lance comme dhabitude. e int pop() { i f (isEmpty()) throw new Empty () ; ... } Mais alors, la compilation de la classe Stack choue. e % javac Stack.java Stack.java:13: unreported exception Stack.Empty; must be caught or declared to be thrown if (isEmpty()) throw new Empty () ; ^ 1 error En eet, Java impose de dclarer quune mthode lance une exception Exception (mais pas e e une exception Error). On dclare que la mthode pop lance lexception Empty par le mot cl e e e throws (noter le s ). int pop() throws Empty { i f (isEmpty()) throw new Empty () ; ... } On peut considrer que les exceptions lances par une mthode font partie de sa signature e e e (voir 3.3), cest-`-dire font partie des informations ` conna pour pouvoir appeler la mthode. a a tre e Les dclarations obligatoires des exceptions lances sont assez contraignantes, en eet il faut e e tenir compte non seulement des exceptions lances directement, mais aussi de celles lances e e indirectement par les mthodes appeles. Ainsi si on conoit une mthode remove( int n) pour e e c e enlever n lments dune pile, on doit tenir compte de lexception Empty ventuellement lance ee e e par pop. void remove (int n) throws Empty { for ( ; n > 0 ; n--) { pop() ; } } Noter quil est galement possible pour remove de ne pas signaler que la pile comportait moins e de n lments. Cela revient ` attraper lexception Empty, et cest dautant plus facile que remove ee a ne renvoie pas de rsultat. e void remove (int n) { try { for ( ; n > 0 ; n--) { pop() ; } } catch (Empty e) { } }

5. ENTREES-SORTIES

205

Entres-sorties e

Par entres-sorties on entend au sens large la faon dont un programme lit et crit des e c e donnes, et au sens particulier linteraction entre le programme et le syst`me de chiers de la e e machine. Un chier est un ensemble de donnes qui survit ` lexcution des programmes, les chiers e a e se trouvent en gnral sur le disque de la machine, mais ils peuvent aussi se trouver sur une e e disquette, ou sur le disque dune autre machine et tre accds ` travers le rseau. Un syst`me e e e a e e dexploitation tel quUnix, ore une abstraction des chiers, gnralement sous forme de ux e e (stream). Dans sa forme la plus simple le ux ore un acc`s squentiel : on peut lire le prochain e e lment dun ux (ce qui le consomme) ou crire un lment dans un ux (gnralement ` la ee e ee e e a n). Dans la majorit des cas (et surtout en Unix), les chiers contiennent du texte, il est alors e logique, vu de Java, de les considrer comme des ux de char. Par chier texte, nous entendons e dabord chiers quun humain peut crire, lire et comprendre, par exemple un source Java. Les e chiers qui ne sinterpr`tent pas ainsi sont dits binaires, par exemple une image ou un chier de e bytecode Java. Il est logique, vu de Java, de considrer les chiers binaires comme des ux de e byte. Par la suite nous ne considrons que les chiers texte. e Quand vous lancez un programme dans une fentre de commandes (shell ), il peut lire ce que e vous tapez au clavier (ux dentre standard) et crire dans la fentre (ux de sortie standard). e e e Le shell permet de rediriger les ux standard vers des chiers (par <chier et >chier), ce qui permet de lire et dcrire des chiers simplement. Enn il existe un troisi`me ux standard, la e e sortie derreur standard normalement connecte ` la fentre comme la sortie standard. Mme e a e e si cela ne semble pas utile, il faut crire les messages de diagnostic et derreur dedans, et nous e allons voir pourquoi.

5.1

Le minimum ` savoir : une entre et une sortie standard a e

Nous connaissons dj` la sortie standard, cest System.out et System.err est la sortie ea derreur. Nous savons dj` crire dans ces ux de classe PrintStream, par leurs mthodes print eae e et println. Pour lire lentre standard (System.in, comme vous pouviez vous en douter), cest e un rien plus complexe, mais sans plus. Il faut tout dabord fabriquer un ux de caract`res, objet de la classe, Reader (voir aussi 6.2.1) e a ` partir de System.in : Reader in = new InputStreamReader(System.in) ; On note que lobjet InputStreamReader est rang dans une variable de type Reader. Et en e eet, InputStreamReader est une sous-classe de Reader (voir 2.3). Ensuite, on peut lire le prochain caract`re de lentre par lappel in.read(). Cette mthode e e e renvoie un int , avec le comportement un peu tordu suivant : Si un caract`re est lu, il est renvoy comme un entier. e e Si on est ` la n de lentre, -1 est renvoy. La n de lentre est par vous signale par a e e e e Control-d au clavier et est la n de chier si il y a eu redirection. En cas derreur lexception IOException est lance. Ces erreurs sont du genre de celles e dtectes au niveau du syst`me dexploitation, comme la panne dun disque (rare !), ou e e e un dlai abusif de transmission par le rseau (plus frquent). Bref, lexception est lance e e e e quand, pour une raison ou une autre, la lecture est juge impossible. e Voici Cat un ltre trivial qui copie lentre standard dans la sortie standard : e import java.io.*; // La classe Reader est dans le package java.io class Cat { public static void main(String [] arg) {

206

ANNEXE A. MORCEAUX DE JAVA try { Reader in = new InputStreamReader(System.in) ; for ( ; ; ) {// Boucle infinie, on sort par break int c = in.read (); i f (c == -1) break; System.out.print ((char)c); } } catch (IOException e) { // En cas de malaise System.err.print("Malaise : " + e.getMessage()) ; System.exit(2) ; // Terminer le programme sur erreur }

} } Remarquons : Il y a un try. . . catch (voir 4.1), car la mthode read dclare lancer lexception IOException, e e ` qui doit tre attrape dans main si elle nest pas dclare dans la signature de main. A la n e e e e du traitement de lexception, on utilise la mthode System.exit qui arrte le programme e e dun coup. Le message derreur est crit dans la sortie derreur. e Pour acher c comme un caract`re, il faut le changer en caract`re, par (char)c. Sinon, e e cest un code de caract`re qui sache. e On peut tester le programme Cat par exemple ainsi : % java Cat < Cat.java import java.io.*; // La classe Reader est dans le package java.io . . .

5.2

Un peu plus que le minimum : lire des chiers

La commande cat de Unix est plus puissante que notre Cat. Si on donne des arguments sur la ligne de commande ils sont interprts comme des noms de chiers, et le contenu des chiers ee est ach sur la sortie standard, un chier apr`s lautre. La commande cat permet donc de e e concatner des chiers. Par exemple e % cat a a > b range deux copies mises bout-`-bout du contenu du chier a dans le chier b. a Pour lire un chier ` partir de son nom, on construit une nouvelle sorte de Reader : un a FileReader. Cela revient ` ouvrir un chier en lecture, cest-`-dire ` demander au syst`me a a a e dexploitation de mobiliser les ressources ncessaires pour cette lecture. Si name est un nom de e chier, on proc`de ainsi. e Reader in = new FileReader (name) ; Si le chier de nom name nexiste pas, une exception est lance (FileNotFoundException e sous-classe de IOException). Il est de bon ton de fermer les chiers que lon a ouvert, an de rendre les ressources mobilises lors de louverture. En eet un programme donn ne peut pas e e possder plus quun nombre x de chiers ouvert en lecture. On ferme un Reader en appelant e e sa mthode close(). Il faut noter quune tentative de lecture dans une entre ferme choue et e e e e se solde par le lancement dune exception spcique. Voici la nouvelle classe Cat2 . e import java.io.* ; class Cat2 { static void cat(Reader in) throws IOException {

5. ENTREES-SORTIES for ( ; ; ) { int c = in.read (); i f (c == -1) break; System.out.print ((char)c); } } public static void main(String [] arg) { i f (arg.length == 0) { try { cat(new InputStreamReader(System.in)) ; } catch (IOException e) { System.err.println("Malaise : " + e.getMessage()) ; System.exit(2) ; } } else { for (int k = 0 ; k < arg.length ; k++) { try { Reader in = new FileReader(arg[k]) ; cat(in) ; in.close() ; } catch (IOException e) { System.err.println("Malaise : " + e.getMessage()) ; } } } } }

207

Remarquons : La lecture dun chier est ralise par une mthode cat, car cette lecture sop`re dans e e e e deux contextes dirents. On note aussi lintrt du sous-typage : largument Reader in e ee est ici un objet InputStreamReader ou FileReader, mais comme il nest pas utile de le savoir dans la mthode cat, on en prote pour partager du code. e Puisque le try. . . catch est dans la boucle for ( int k = 0 ;. . . , le programme nchoue e pas en cas derreur de lecture dun chier donn par son nom ou mme de chier e e inexistant, car FileNotFoundException est une sous-classe de IOException. Au lieu dchouer, on passe au chier suivant. e Les messages derreur sont crits dans la sortie derreur et non pas dans la sortie standard. e Ainsi si le chier a existe mais que le chier x nexiste pas, la commande suivante a un comportement raisonnable. % java Cat2 a x a > b Malaise : x (No such file or directory) Un message derreur est ach dans la fentre, et b contient deux copies de a. e e

5.3

Encore un peu plus que le minimum : crire dans un chier e

La sortie standard est souvent insusante, on peut vouloir crire plus dun chier ou ne e pas obliger lutilisateur ` faire une redirection. On se dit donc quil doit bien y avoir un moyen a dobtenir un ux de caract`res en sortie connect ` un chier dont on donne le nom. Le ux e e a de sortie le plus simple est le Writer, qui poss`de une mthode write( int c) pour crire un e e e caract`re. Comme pour les Reader on utilise en pratique des sous-classes de Writer. Il existe e entre autres un FileWriter que nous utilisons pour crire une version simple de la commande e

208

ANNEXE A. MORCEAUX DE JAVA

Unix cp , o` cp name1 name2 copie le contenu du chier name1 dans le chier name2 . u import java.io.* ; class Cp { public static void main(String [] arg) { i f (arg.length != 2) { System.err.println("Usage : java Cp name1 name2") ; System.exit(2) ; } String name1 = arg[0], name2 = arg[1] ; try { Reader in = new FileReader(name1) ; Writer out = new FileWriter(name2) ; for ( ; ; ) { int c = in.read() ; i f (c == -1) break ; out.write(c) ; } out.close() ; in.close() ; } catch (IOException e) { System.err.println("Malaise : " + e.getMessage()) ; System.exit(2) ; } } } Il y a peu ` dire, mais remarquons que nous prenons soin de fermer les ux que nous ouvrons. a Cela semble une bonne habitude ` prendre (voir aussi 5.6 pour tout savoir ou presque). a

5.4

Toujours plus loin : entres-sorties buerises e e

La documentation recommande lemploi de ux bueriss (buered dsol pas de terme e e e franais adquat) pour atteindre lecacit maximale top eciency . Lide des ux bueriss, c e e e e qui est tr`s gnrale, est la suivante : lcriture ou la lecture eective de caract`res a un cot xe e e e e e u important, qui est pay quelque soit le nombre de caract`res eectivement transfrs entre chier e e ee et programme. Plus prcisment, crire ou lire n caract`res cote de lordre de K0 +K1 n, o` K0 e e e e u u est bien plus grand que K1 . Cela peut sexpliquer par divers phnom`nes. Par exemple, un appel e e au syst`me dexploitation est relativement lent, et une opration dentre-sortie signie un seul e e e appel syst`me, quelque soit le nombre de caract`res impliqus. Ou encore, le cot des transferts e e e u entre disque et mmoire est largement indpendant du nombre de caract`res transfrs, jusqu` e e e ee a une certaine taille, de par la nature mme du dispositif physique disque qui lit et crit des e e donnes par blocs de taille xe. e e Pour xer les ides, prenons lexemple de lcriture. Lide est alors de ne pas crire eectie e e e vement chaque caract`re en raction ` un appel out.write(c) mais ` la place de le ranger dans e e a a une zone mmoire appele tampon (buer ). Le tampon a une taille xe, et les caract`res du tame e e pon sont eectivement transmis au syst`me dexploitation quand le tampon est plein. De cette e faon le cot xe K0 est pay moins souvent et lecacit totale est amliore. Les transferts c u e e e e vers les chiers ` travers un tampon mmoire prsentent linconvnient que le caract`re c peut a e e e e 2 d`s que lon appelle out.write(c). Leet est particuli`rement ne pas se trouver dans le chier e e gnant ` la n du programme. Si le tampon nest pas vid (ushed ), son contenu est perdu. En e a e eet, le tampon est de la mmoire appartenant au programme et qui donc dispara avec lui. Il e t en rsulte gnralement que la n du ux ne se retrouve pas dans le chier. Il faut donc vider e e e
2

Ou plus exactement c nest pas dans les tampons du syst`me dexploitation, en route vers le chier e

5. ENTREES-SORTIES

209

le tampon avant de terminer le programme, ce que fait la mthode close() de fermeture des e ux, avant de fermer eectivement le ux. On peut aussi vider le tampon plus directement en appelant mthode flush() des ux bueriss. Lexistence dun retard entre ce qui est crit par e e e le programme dans le ux et ce qui est eectivement crit dans le chier donne donc une raison e supplmentaire de fermer les chiers. e Pour ce qui est dun chier ouvert en lecture, la technique du tampon sapplique galement, e avec les mmes bnces en terme decacit. Dans ce cas, les lectures (read) se font dans le e e e e tampon, qui est rempli ` partir du chier quand une demande de lecture trouve un tampon a vide. Il y a alors bien entendu une avance ` la lecture mais ce dcalage ne pose pas les mmes a e e probl`mes que le retard ` lcriture. e a e Un ux bueris en criture (resp. lecture) est un objet de la classe BueredWriter (resp. e e BueredReader), qui se construit simplement ` partir dun Writer (resp. Reader), et qui a e reste un Writer (resp. Reader). Voici une autre version Cp2 de la commande cp crite en Java, qui emploie les entres-sorties buerises. e e import java.io.* ; class Cp2 { public static void main(String [] arg) { String name1 = arg[0], name2 = arg[1] ; try { Reader in = new BufferedReader (new FileReader(name1)) ; Writer out = new BufferedWriter (new FileWriter (name2)) ; for ( ; ; ) { int c = in.read() ; i f (c == -1) break ; out.write(c) ; } out.close() ; in.close() ; } catch (IOException e) { System.err.println("Malaise : " + e.getMessage()) ; System.exit(2) ; } } } a e Une mesure rapide des temps dexcution montre que le programme Cp2 est ` peu pr`s trois e fois plus rapide que Cp. Les ux bueriss BueredWriter et BueredReader orent aussi une vue des chier e texte comme tant composs de lignes. Il existe une mthode Newline pour crire une n e e e e le ligne, et une mthode readLine pour lire une ligne. On utilise souvent BueredReader e pour cette fonctionnalit de lecture ligne ` ligne. Il faut aussi noter que les lignes sont dnies e a e indpendamment de leur ralisation par les divers syst`mes dexploitation (voir 3.2.3). e e e

5.5
5.5.1

Entres-sorties formates e e
Sorties formates e

Nous savons dsormais crire dans un chier de nom name de faon ecace en construisant e e c un FileWriter, puis un BueredWriter. Writer out = new BufferedWriter (new FileWriter name) ; Toutefois, crire caract`re par caract`re (ou mme par cha avec write(String str)), nest e e e e ne pas toujours tr`s pratique. La mthode print de System.out est bien plus commode. Malheue e reusement, les PrintStream de la biblioth`que sont tr`s inecaces. Par exemple, la copie du e e

210

ANNEXE A. MORCEAUX DE JAVA

chier a dans le chier b par java Cat < a > b est environ vingt-cinq fois plus lente que java Cp2 a b ! Mais la mthode print est bien pratique, voici par exemple une mthode simple qui ache e e les nombres premiers jusqu` n directement dans System.out. a static void sieve(int n) { boolean [] t = new boolean [n+1] ; int p = 2 ; for ( ; ; ) { System.out.println(p) ; // Afficher p premier /* Identifier les multiples de p */ for (int k = p+p ; k <= n ; k += p) t[k] = true ; /* Chercher le prochain p premier */ do { p++ ; i f (p > n) return ; } while (t[p]) ; } } Lapplication du crible dEratosth`ne est na mais le programme est inecace dabord ` cause e ve, a de ses achages. On sen rend compte en modiant sieve pour utiliser un PrintWriter. Les objets de la classe PrintWriter sont des Writer (ux de caract`res) qui orent les sorties e formates, cest-`-dire quils poss`dent une mthode print (et prinln) surcharge qui all`ge un e a e e e e peu la programmation. Par ailleurs, les PrintWriter sont bueriss par dfaut. e e static void sieve(int n, PrintWriter out) throws IOException { boolean [] t = new boolean [n+1] ; int p = 2 ; for ( ; ; ) { out.println(p) ; for (int k = p+p ; k <= n ; k += p) t[k] = true ; do { p++ ; i f (p > n) return ; } while (t[p]) ; } } public static void main(String [] arg) { . . . try { PrintWriter out = new PrintWriter ("primes.txt") ; sieve(n, out) ; out.close() ; } catch (IOException e) { System.err.println("Malaise: " + e.getMessage()) ; System.exit(2) ; } } La nouvelle mthode sieve crit dans le PrintWriter out dont linitialisation est rendue pnible e e e par les exceptions possibles. Noter aussi que la sortie out est vide (et mme ferme) par la e e e mthode main qui la cre. Il y a plusieurs constructeurs des PrintWriter, qui prennent divers e ee arguments. Par exemple, si lon avait voulu crire dans la sortie standard, et non pas dans le e chier primes.txt, on aurait tr`s bien pu emballer la sortie standard dans un PrintWriter, e par new PrintWriter(System.out). Pour ce qui est du temps dexcution des essais rapides e

5. ENTREES-SORTIES

211

montrent que le nouveau sieve (avec PrintWriter) peut tre jusqu` dix fois plus rapide que e a lancien (avec PrintStream). Nous insistons donc : System.out est vraiment tr`s inecace, e il faut renoncer ` son emploi d`s quil y a beaucoup de sorties. a e 5.5.2 Entres formates e e

Les Reader permettent de lire caract`re par caract`re, les BueredReader permettent e e de lire ligne par ligne. Cest dj` pas mal, mais cest parfois insusant. Par exemple, on peut ea imaginer vouloir relire le chier primes.txt de la section prcdente. Ce que lon veut alors cest e e lire une squence dentiers int . Pour ce faire nous pourrions reconstituer nous mme les int ` e e a partir de la squence de leurs chires, mais ca ne serait pas tr`s moderne. e e Heureusement, il existe des objets qui savent faire ce style de lecture pour nous : ceux de la classe Scanner du package java.util. Les objets Scanner sont des ux de lex`mes (tokens). e Les lex`mes sont simplement ` un langage informatique ce que les mots sont ` un langage dit e a a naturel. Par dfaut les lex`mes reconnus par un Scanner sont les mots de Java un entier, un e e identicateur, une cha ne, etc. Un Scanner poss`de une mthode next() qui renvoie le lex`me suivant du ux dentre e e e e (comme un String) et une mthode hasNext() pour savoir si il existe un lex`me suivant, ou si e e le ux est termin. Il poss`de aussi des mthodes spcialises pour lire des lex`mes particuliers e e e e e e ou savoir si le lex`me suivant existe et est un lex`me particulier (par exemple nextInt() et e e hasNextInt() pour un int ). On peut donc utiliser primes.txt pour dcomposer un int en e facteurs premiers ainsi (mme si ce nest pas une tr`s bonne ide algorithmiquement parlant). e e e import java.util.* ; import java.io.* ; class Factor { private static PrintWriter out = new PrintWriter (System.out) ; private static void factor (int n, String filename) { try { Scanner scan = new Scanner (new FileReader (filename)) ; out.print(n + ":") ; while (n > 1) { i f (!scan.hasNextInt()) throw new IOException ("Format of file " + filename) ; int p = scan.nextInt() ; while (n % p == 0) { out.print(" " + p) ; n /= p ; // Pour n = n / p ; } } out.println() ; out.flush() ; scan.close() ; } catch (IOException e) { System.err.println("Malaise : " + e.getMessage()) ; System.exit(2) ; } } public static void main(String arg []) { for (int k = 0 ; k < arg.length ; k++) factor(Integer.parseInt(arg[k]), "primes.txt") ; }

212 }

ANNEXE A. MORCEAUX DE JAVA

On note que le Scanner est construit ` partir dun Reader. Il y a bien entendu dautres a constructeurs. On note aussi que lentre scan est ferme explicitement par scan.close(), d`s e e e quelle nest plus utile ; tandis que la sortie nest que vide par out.flush(). En eet, il ne e serait pas adquat de fermer la sortie out qui sert plusieurs fois, mais il faut sassurer que les e sorties du programme sont bien envoyes au syst`me dexploitation. e e

5.6

Complment : tout ou presque sur les chiers texte e

Si Java et Unix sont bien daccord sur ce quest en gros un chier texte (un chier quun humain peut lire au moins en principe), ils ne sont plus daccord sur ce que sont les lments ee dun tel chier. Pour Java, un chier de texte est un ux de char (16 bits), tandis que pour Unix cest un ux doctets (8 bits, un byte pour Java). Le passage de lun ` lautre demande a dappliquer un encodage. Un exemple simple dencodage est par exemple ISO-8859-1, lencodage que java emploie par dfaut sur les machines de lcole. Cest un encodage qui fonctionne pour presque toutes e e les langues europennes (mais pas pour le symbole e). Cest un encodage simple sur 8 bits, e qui permet dexprimer 256 caract`res seulement parmi les 216 dUnicode. Pour voir les cae ract`res dnis en ISO-8859-1, vous pouvez essayer man latin1 sur une machine de lcole. e e e Lencodage ISO-8859-1 est techniquement le plus simple possible : les codes des caract`res e sont les mmes ` la fois en ISO-8859-1 et en Unicode. Il nest donc tout simplement pas e a possible dexprimer les char dont les codes sont suprieurs ` 256 en ISO-8859-1 (dont juse a tement e, dont la valeur Unicode hexadcimale est 0x20AC, note U+20AC). Il existe dautres e e encodages 8 bits, dont ISO-8859-15 qui entre autres tablit justement la correspondance entre e le caract`re Unicode U+20AC et le byte 0xA4, au dpend du caract`re Unicode U+00A4 ( ) e e e qui nest plus exprimable. Il existe bien entendu des encodages qui permettent dexprimer tout Unicode, mais alors un caract`re Unicode peut sexprimer comme comme plusieurs octets. Un e encodage multi-octet rpandu est UTF-8, o` les caract`res Unicode sont reprsents par un e u e e e nombre variable doctets selon un syst`me un peu compliqu que nous ne dcrirons pas (voir e e e http ://fr.wikipedia.org/wiki/UTF-8 qui est raisonnablement clair). Revenons aux ux de Java. Pour xer les ides nous considrons dabord les ux en lecture. e e Un ux doctets est un InputStream. La classe InputStream fonctionne sur le mme principe e que la classe Reader : cest une sur-classe des divers ux de byte qui peuvent tre construits. e Par exemple on ouvre un ux doctets sur un chier name en crant un objet de la classe e FileInputStream. InputStream inBytes = new FileInputStream (name) ; Pour lire un ux doctets comme un ux de char, on fabrique un InputStreamReader. Reader inChars = new InputStreamReader (inBytes) ; Lencodage est ici implicite, cest lencodage par dfaut. On peut aussi expliciter lencodage en e donnant son nom comme second argument au constructeur. Reader inChars = new InputStreamReader (inBytes, "UTF-8") ; Et voil` nous disposons maintenant dun Reader sur un chier dont la suite doctets dnit a e une suite de caract`res Unicode encods en UTF-8. Ici, comme UTF-8 est un encodage multie e octets, la lecture dun char dans inChars implique de lire un ou plusieurs byte dans le ux inBytes sous-jacent. Les ux en criture suivent un schma similaire, il y a des ux de byte e e (OutputStream) et des ux de char (Writer), avec une classe pour faire le pont entre les deux (OutputStreamWriter). Par exemple, voici comment fabriquer un Writer connect ` ea la sortie standard et qui crit de lUTF-8 sur la console : e

` 6. QUELQUES CLASSES DE BIBLIOTHEQUE // System.out (un PrintStream) est aussi un OutputStream Writer outChar = new OutputStreamWriter (System.out, "UTF-8") ;

213

Notons quune fois obtenu un Reader ou un Writer nous pouvons fabriquer ce dont nous avons besoin, par exemple un Scanner ou un PrintWriter etc., ` laide des constructeurs naturels a de ces classes, qui prennent un Reader ou un Writer en argument. Les diverses classes de ux poss`dent parfois des constructeurs qui semblent permettre de court-circuiter le passage par un e InputStreamReader ou un OutputStreamWriter ; mais ce nest quune apparence, il y aura toujours dcodage et encodage. Par exemple, new PrintWriter (String name) ouvre direce tement le chier name, mais ` quelques optimisations internes toujours possibles pr`s, employer a e ce constructeur synthtique revient ` : e a new PrintWriter (new OutputStreamWriter (new FileOutputStream (name))) ; Finalement, en thorie nous savons lire et crire tout Unicode. En pratique, il faut encore pouvoir e e entrer ces caract`res au clavier et les visualiser dans une fentre, mais cest une autre histoire e e qui ne regarde plus Java. Toutes ces histoires dencodage et de dcodage de char en byte font qucrire un char dans e e un Writer ou lire un char dans un Reader ne sont jamais des oprations simples.3 Comme on e a tendance a tout simplier on a parfois des surprises : par exemple, les FileWriter (voir 5.3) poss`dent en fait dj` un tampon. On se rend vite compte de lexistence de ce tampon si on e ea oublie de fermer un FileWriter. Si on en croit la documentation : Each invocation of a write() method causes the encoding converter to be invoked on the given character(s). The resulting bytes are accumulated in a buer before being written to the underlying output stream. Il sagit donc dun tampon de byte dans lequel le FileWriter stocke les octets rsultants de e lencodage quil eectue. On peut alors se demander do` provient le gain decacit constat en u e e emballant un FileReader dans un BueredWriter (voir 5.4), puisque le cot irrductible de la u e vritable sortie est dj` amorti par un tampon. Et bien, il se trouve que le cot de lapplication e ea u de lencodage des char vers les byte suit lui-aussi le mod`le dun cot constant important e u relativement au cot proportionnel au nombre de caract`res encods. Le tampon (de char) u e e introduit en amont du FileWriter par new BueredWriter (new FileWriter (name2)) a alors pour fonction damortir ce cot irrductible de lencodage. u e

Quelques classes de biblioth`que e

En gnral une biblioth`que (library) est un regroupement de code susamment structur et e e e e document pour que dautres programmeurs puissent sen servir. La biblioth`que associe ` un e e e a langage de programmation est diuse avec le compilateur, syst`me dexcution etc. La majeure e e e partie de des programmes disponibles dans la biblioth`que dun langage est normalement crite e e dans ce langage lui-mme. e La premi`re source de documentation des classes de la biblioth`que de Java est bien entendu e e la documentation en ligne4 de cette derni`re, ou comme on dit la Application Programmer Intere face Specication. Nous donnons donc des extraits de cette documentation assortis de quelques commentaires personnels. Cette section entend surtout vous aider ` prendre la bonne habitude a de consulter la documentation du langage que vous utilisez. En particulier, la version web du polycopi comporte des liens vers la documentation en ligne. e On objectera que la biblioth`que est norme. Mais en pratique on sy retrouve assez vite : e e
Encodage et dcodage font aussi quil ne faut pas crire Cp et Cat comme nous lavons fait (avec des ux e e de char). Cest inutilement inecace et mme dangereux certains dcodages pouvant parfois chouer, il aurait e e e mieux valu employer les ux de byte. 4 http://java.sun.com/j2se/1.5.0/docs/api
3

214

ANNEXE A. MORCEAUX DE JAVA

La documentation en ligne est organise de faon systmatique, La page daccueil est une e c e liste de liens vers les pages des packages, la page dun package est une liste de liens vers les pages des classes, qui contiennent (au dbut) une liste de liens vers les champs, mthodes e e etc. Les classes que nous utilisons appartiennent ` trois packages seulement. a Les descriptions sont parfois un peu cryptiques (car elles sont compl`tes), mais on arrive e vite ` en extraire lessentiel. a Il est vain dobjecter que la documentation est en anglais. Cest comme ca.

6.1

Package java.lang, biblioth`que standard e

Ce package regroupe les fonctionnalits les plus basiques de la la biblioth`que. Il est import e e e par dfaut (pas besoin de import java.lang.*). e 6.1.1 Classes des scalaires

` A chaque type scalaire ( int , char etc.), correspond une classe (Integer, Character etc.) qui permet surtout de transformer les scalaires en objets. Une transformation que le compilateur sait automatiser (voir III.2.3). Prenons lexemple de la classe Integer le pendant objet du type scalaire int . Deux mthodes e permettent le passage dun scalaire ` un objet et rciproquement. a e La mthode valueOf transforme le scalaire en objet. e public static Integer valueOf(int i) La mthode rciproque intValue extrait le int cach dans un objet Integer. e e e public int intValue() La classe Integer a bien dautres mthodes, comme par exemple e La mthode parseInt permet de lire un entier. e public static int parseInt(String s) throws NumberFormatException Si la cha s contient la reprsentation dcimale dun entier, alors parseInt renvoie ne e e cet entier (comme un int ). Si la cha s ne reprsente pas un int , alors lexception ne e NumberFormatException est lance. e 6.1.2 Un peu de maths

Les fonctions mathmatiques usuelles sont dnies dans la classe Math. Il ny a pas dobjets e e Math. Cette classe ne sert qu` la structuration. Elle ore des mthodes (statiques). a e La valeur absolue abs, disponible en quatre versions. public public public public static static static static int abs(int a) long abs(long a) float abs(float a) double abs(double a)

Les fonctions maximum et minimum, max et min, galement disponibles en quatre versions. e public static int max(int a, int b) public static int min(int a, int b) . . . Les divers arrondis des double, par dfaut oor, par exc`s ceil et au plus proche round. e e

` 6. QUELQUES CLASSES DE BIBLIOTHEQUE public static double floor(double a) public static double ceil(double a) public static long round(double a)

215

On note que floor et ceil renvoient un double. Tandis que round renvoie un long. Les mthodes existent aussi pour les oat et round( oat a) renvoie un int . Cela peut e sembler logique si on consid`re que les valeurs double et long dune part, et oat et e int dautre part occupent le mme nombres de bits (64 et 32). Mais en fait ca ne r`gle e e pas vraiment le probl`me des ottants trop grands pour tre arrondis vers un entier (voir e e la documentation). Et bien dautres mthodes, comme la racine carre sqrt, lexponentielle exp, le logarithme e e naturel log, etc. 6.1.3 Les cha nes

Les cha nes sont des objets de la classe String. Une cha est tout simplement une squence ne e nie de caract`res. En Java les cha e nes sont non-mutables : on ne peut pas changer les caract`res e dune cha ne. Les cha nes sont des objets normaux, ` un peu de syntaxe spcialise pr`s. Les a e e e constantes cha nes scrivent entre doubles quotes, par exemple "coucou". Si on veut mettre e un double quote " dans une cha ne, il fait le citer en le prcdant dune barre oblique e e inverse (backslash). System.err.print("Je suis un double quote : \"") ache Je suis un double quote : " sur la console. En outre, la concatnation de deux cha e nes s1 et s2 , scrit e avec loprateur + comme s1 +s2 . e Les autres oprations classiques sur les cha e nes sont des mthodes normales. e La taille ou longueur dune cha s, cest-`-dire le nombre de caract`res quelle contient ne a e sobtient par s.length() public int length() Le caract`re dindice index sextrait par la mthode charAt. e e public char charAt(int index) Comme pour les tableaux, les indices commencent ` zro. a e On extrait une sous-cha par la mthode substring. ne e public String substring(int beginIndex, int endIndex) beginIndex est lindice du premier caract`re de la sous-cha et endIndex est lindice du e ne, caract`re qui suit immdiatement la sous-cha La longueur de la sous-cha extraite est e e ne. ne donc endIndex-beginIndex. Si le couple dindices ne dsigne pas une sous-cha lexe ne ception IndexOutOfBoundsException est lance. Les r`gles de dnition du couple e e e dindices dsignant une sous-cha valide conduisent ` la particularit bien sympathique e ne a e que s.substring(s.length(), s.length()) renvoie la cha vide "" (voir la documenne tation pur le dtail des r`gles). e e La mthode equals est lgalit structurelle des cha e e e nes, celle qui regarde le contenu des cha nes. public boolean equals(Object anObject) Il faut remarquer que largument de la mthode est un Object. Il faut juste le savoir, cest e tout. La classe String tant une sous-classe de Object (voir 2.3), la mthode sapplique e e en particulier au seul cas utile dun argument de classe String. La mthode compareTo permet de comparer les cha e nes. public int compareTo(String anotherString)

216

ANNEXE A. MORCEAUX DE JAVA Compare la cha avec une autre selon lordre lexicographique (lordre du dictionnaire). ne Lappel s1 .compareTo(s2 ) renvoie un entier r avec : r < 0, si s1 est strictement infrieure ` s2 . e a r = 0, si les deux cha nes sont les mmes. e r > 0, si s1 est strictement plus grande t s2 . Lordre sur les cha nes est induit par lordre des codes des caract`res, ce qui fait que lon e obtient lordre du dictionnaire stricto-sensu seulement pour les mots sans accents. Il y a beaucoup dautres mthodes, par exemple toLowerCase et toUpperCase pour mie nusculer et majusculer. public String toLowerCase() public String toUpperCase() Cette fois-ci les caract`res accentus sont respects. e e e

6.1.4

Les fabricants de cha nes

On ne peut pas changer une cha par exemple changer un caract`re individuellement. Or, ne, e on veut souvent construire une cha petit ` petit, on peut le faire en utilisant la concatnation ne a e mais il y a alors un prix ` payer, car une nouvelle cha est cre ` chaque concatnation. En a ne ee a e fait, lorsque lon construit une cha de gauche vers la droite (pour achage dun tableau par ne ex.) on a besoin dun autre type de structure de donnes : le StringBuilder (ou StringBuer e essentiellement quivalent). Un objet StringBuilder contient un tampon de caract`re interne, e e qui peut changer de taille et dont les lments peuvent tre modis. Les membres les plus utiles ee e e de cette classe sont : Le constructeur sans argument. public StringBuilder () Cre un nouveau fabricant dont le tampon a la capacit initiale par dfaut (16 caract`res). e e e e Les mthodes append surcharges. Il y en a treize, six pour les scalaires, et les autres pour e e divers objets. public StringBuilder append(int i) public StringBuilder append(String str) public StringBuilder append(Object obj) Toutes ces mthodes ajoutent la reprsentation sous forme de cha de leur argument ` e e ne a la n du tampon interne, dont la capacit est augment silencieusement si ncessaire. e e e Dans le cas de largument objet quelconque (le dernier), cest la cha obtenue par ne obj.toString() qui est ajoute, ou "null" si obj est null . Les autres mthodes sont l`, e e a soit pour des raisons decacit (cas de String), soit pour les tableaux dont le toString() e est inutilisable, soit parce que largument est de type scalaire (cas de int ). Toutes les mthodes append renvoient lobjet StringBuilder dont on appelle la mthode, ce qui est e e absurde, cet objet restant le mme. Je crois quil en est ainsi an dautoriser par exemple e lcriture sb.append(i).append(:) ; au lieu de sb.append(i) ; sb.append(:) ; e (o` i est une variable par exemple de type int ). u Et bien videmment, une mthode toString rednie. e e e public String toString() Copie le contenu du tampon interne dans une nouvelle cha Contrairement aux tampons ne. de chier, le tampon nest pas vid. Pour vider le tampon, on peut avoir recours ` la e a mthode setLength avec largument zro. e e On trouvera un exemple dutilisation classique dun objet StringBuilder en I.2.1 : une cration, e des tas de append(...), et un toString() nal.

` 6. QUELQUES CLASSES DE BIBLIOTHEQUE 6.1.5 La classe System

217

Cette classe System est un peu fourre-tout. Elle posss`de trois champs et quelques mthodes e e assez utiles. Les trois ux standards. public static final InputStream in public static final PrintStream out public static final PrintStream err Ces trois ux sont des ux doctets (voir la section sur les entres-sorties 5). Notez que e les champs sont dclars nal , on ne peut donc pas les changer (bizarrement il existe e e quand mme des mthode pour changer les trois ux standard). Les deux ux de sortie e e orent des sorties formates par une mthode print bien pratique, car elle est surcharge e e e et fonctionne pour toutes les valeurs. La sortie out est la sortie normale du programme, tandis que la sortie err est la sortie derreur. En fonctionnement normal ces deux ux semblent confondus car aboutissant tous deux dans la mme fentre. Mais on peut rediriger la sortie out, par exemple vers e e un chier (>chier). Les messages derreurs sachent alors au lieu de se mlanger avec la e sortie normale du programme dans chier. Une mthode pour arrter imdiatement le programme. e e e public static void exit(int status) Lentier status est le code de retour qui vaut conventionellement zro dans le cas dune e excution normale, et autre chose pour une excution qui sest mal passe, En Unix, le e e e code est accessible juste apr`s lexcution comme $status ou $?, selon les shells. e e Deux mthodes donnent acc`s ` un temps absolu. La seconce permet de mesurer le temps e e a dexcution par une soustractions. e public static long currentTimeMillis() public static long nanoTime() La mthode currentTimeMillis renvoie les milisecondes coules depuis le 1er janvier e e e 1970. Il sagit donc du temps ordinaire. La mthode nanoTime() renvoie les nanosecondes e (109 ) depuis une date arbitraire. Il sagit du temps pass dans le code du programme, ce e qui est dirent du temps ordinaire surtout dans un syst`me dexploitation multi-tches e e a comme Unix.

6.2

Package java.io, entres-sorties e

Ce package regroupe sattaque principalement ` la communication avec le syst`me de chiers a e de la machine. Nous ne passons en revue ici que les ux de caract`res, qui correspondent aux e chiers texte (voir 5). 6.2.1 Classe des lecteurs

Les objets de la classe Reader sont des ux de caract`res ouverts en lecture. Un tel ux e est simplement une squence de caract`res que lon lit les un apr`s les autres, et qui nit par se e e e terminer. La mthode la plus signicative est donc celle qui permet de lire le caract`re suivant du e e ux. public int read() throws IOException Cette mthode renvoie un int qui est le caract`re en attente dans le ot dentre, ou 1, si e e e le ot est termin. On notera que read renvoie un int et non pas un char prcisment pour e e e

218

ANNEXE A. MORCEAUX DE JAVA pouvoir identier la n du ux facilement. En outre et comme annonc par sa dclaration e e elle peut lever lexception IOException en cas derreur (possible d`s que lon sintresse e e au chiers). Quand la n dun ux est atteinte, il faut fermer le ux. public void close() throws IOException

Cette opration lib`re les ressources mobilises lors de la cration dun ot dentre (numros e e e e e e divers lies au syst`me de chiers, tampons internes). Si on ne ferme pas les ots dont on e e ne sert plus, on risque de ne pas pouvoir en ouvrir dautres, en raison de lpuisement de e ces ressources. La classe Reader sert seulement ` dcrire une fonctionnalit et il nexiste pas ` proprement a e e a parler dobjets de cette classe. En revanche, il existe diverses sous-classes (voir 2.3) de Reader qui permettent de construire concr`tement des objets, qui peuvent se faire passer pour des Reader. Il sagit par entre autres e des classes StringReader (lecteur sur une cha ne), InputStreamReader (lecteur sur un ux doctets) et FileReader (lecteur sur un chier). On peut donc par exemple obtenir un Reader ouvert sur un chier /usr/share/dict/french par Reader in = new FileReader ("/usr/share/dict/french") ; et un Reader sur lentre standard par e Reader in = new InputStreamReader (System.in) ; 6.2.2 Lecteur avec tampon

Un objet BueredReader est dabord un Reader qui groupe les appels au ux sous-jacent et stocke les caract`res lus dans un tampon, an damliorer les performances (voir 5.4). Mais e e un BueredReader ore quelques fonctionnalits en plus de celles des Reader. e Le constructeur le plus simple prend un Reader en argument. public BufferedReader(Reader in) Construit un ux dentre bueris ` partir dun lecteur quelconque. e ea La classe BueredReader est une sous-classe de la classe Reader de sorte quun objet BueredReader poss`de une mthode read. e e public int read() throws IOException Cette mthode nest pas hrite mais rednie, an de grer le tampon interne. e e e e e La lecture dune ligne se fait par une nouvelle mthode, que les Reader ne poss`dent pas. e e public String readLine() throws IOException Renvoie une ligne de texte ou null en n de ux. La mthode fonctionne pour les trois e conventions de n de ligne (\n, \r ou \r suivi de \n) et le ou les caract`res n e de ligne ne sont pas renvoys. La mthode fonctionne encore pour la derni`re ligne dun e e e ux qui ne serait pas termine par une n de ligne explicite. e

6.3
6.3.1

Package java.util : trucs utiles


Les sources pseudo-alatoires e

Une source pseudo-alatoire est une suite de nombres qui ont lair dtre tirs au hasard. Ce e e e qui veut dire que les nombres de la suite obissent ` des crit`res statistiques censs exprimer e a e e le hasard (et en particulier luniformit, mais aussi un aspect chaotique pas vident ` spcier e e a e prcisment). Une fois la source construite (un objet de la classe Random), diverses mthodes e e e

` 7. PIEGES ET ASTUCES

219

permettent dobtenir divers scalaires ( int , double, etc.) que lon peut considrer comme tirs au e e hasard. On a besoin de tels nombres tirs au hasard par exemple pour raliser des simulations e e (voir III.1.1) ou produire des exemples destiner ` tester des programmes. a Les constructeurs. public Random() public Random(long seed) La version avec argument permet de donner une semence de la source pseudo-alatoire, e deux sources qui ont la mme semence se comportant ` lidentique. La version sans argue a ment laisse la biblioth`que choisir une semence comme elle lentend. La semence choisie e change ` chaque appel du constructeur et on suppose quelle est choisie de faon la plus a c arbitraire et non-reproductible possible (par exemple ` partir de lheure quil est). a Les nombres pseudo-alatoires sont produits par diverses mthodes next . Distinguons e e particuli`rement, e Un entier entre zro (inclus) et n (exclu). e public int nextInt(int n) Cette mthode est pratique pour tirer un entier au pseudo-hasard sur un intervalle e [a. . . b[, par a + rand.nextInt(b-a). Un ottant entre 0.0 (inclus) et 1.0 (exclu). public float nextFloat() public double nextDouble() Ces mthodes sont pratiques pour dcider selon une probabilit quelconque. Du style e e e rand.nextDouble() < 0.75 vaut true avec une probabilit de 75 %. e

Pi`ges et astuces e

Cette section regroupe des astuces de programmation ou corrige des erreurs frquemment e rencontres. e

7.1

Sur les caract`res e

Les codes des caract`res de 0 ` 9 se suivent, de sorte que les deux trucs suivants e a sappliquent. Pour savoir si un caract`re c est un chire (arabe) : e 0 <= c && c <= 9 Il existe aussi une mthode Character.isDigit(char c) qui tient compte de tous les chires e possibles (en arabe cest-`-dire indiens, etc.). Pour rcuprer la valeur dun chire : a e e int i = c - 0 ; Ici encore il y a des mthodes statiques qui donnent les valeurs de chires dans la classe e Character.

7.2

Oprateurs e

Java poss`de un oprateur ternaire (` trois arguments) lexpression conditionnelle ec ? et : e e a ef . Lexpression ec est de type boolean, tandis que le types de et et ef sont identiques. Lexpression conditionnelle vaut et si ec vaut true, et ef si ec vaut false . De sorte que lon peur acher un boolen ainsi : e // b est un boolean, on veut afficher + si b est vrai et - autrement. System.out.println(b ? + : -) ;

220

ANNEXE A. MORCEAUX DE JAVA

7.3

Connecteurs paresseux

Les oprateurs || et && sont respectivement la disjonction (ou logique) et la conjonction (et e logique), ils sont de la varit dite squentielle (gauche-droite) ou parfois paresseuse : si valuer ee e e la premi`re condition sut pour dterminer le rsultat, alors la seconde condition nest pas e e e value. Plus prcisment, si dans e1 || e2 (resp. e1 && e2 ) e1 vaut true (resp. false ) , alors e e e e e2 nest pas value, et la disjonction (resp. conjonction) vaut true (resp. false). Cest un idiome e e de proter de ce comportement pour crire des conditions concises. Par exemple, pour savoir si e la liste xs contient au moins deux lments, on peut crire ee e . . . xs != null && xs.next != null . . . Cette expression nchoue jamais, cas si xs vaut null alors lacc`s xs.next nest pas tent. e e e On remarque que les connecteurs && et || peuvent sexprimer ` laide dun seul oprateur : a e loprateur ternaire e1 ? e2 : e3 . La conjonction e1 && e2 sexprime comme e1 ? e2 : false e et la disjonction e1 || e2 comme e1 ? true : e2

Bibliographie
[1] A. V. Aho, J. E. Hopcroft et J. D. Ullman, The Design and Analysis of Computer Algorithms, Addison Wesley, 1974. [2] J. L. Bentley et M. D. McIlroy, Engineering a Sort Function, Software - Practice and Experience 23,11 (1993), 12491265. [3] J. Berstel et J.-E. Pin, Programmation et Algorithmique, Polycopi du cours INF421 e de lEcole Polytechnique, 2003. [4] T. H. Cormen, C. E. Leiserson et R. L. Rivest, Introduction a lalgorithmique, Du` nod, 1994. [5] J. E. F. Friedl, Ma trise des expressions rguli`res, OReilly, 2001. e e [6] C. Froidevaux, M. Gaudel et M. Soria, Types de donnees et algorithmes, Ediscience International, 1993. [7] J. E. Hopcroft, R. Motwani et J. D. Ullman, Introduction to Automata Theory, Languages, and Computation, Addison-Wesley, d. 2nd, 2001. e [8] J. Kleinberg et E. Tardos, Algorithm design, Addison-Wesley Longman Publishing Co., Inc. Boston, MA, USA, 2005. [9] D. E. Knuth, Fundamental Algorithms. The Art of Computer Programming, vol. 1, Addison Wesley, 1968. [10] D. E. Knuth, Seminumerical Algorithms. The Art of Computer Programming, vol. 2, Addison Wesley, 1969. [11] D. E. Knuth, Sorting and Searching. The Art of Computer Programming, vol. 3, Addison Wesley, 1973. [12] R. Sedgewick, Algorithms, (2nd edition), Addison-Wesley, 1988. [13] R. Sedgewick et P. Flajolet, Introduction a lanalyse dalgorithmes, International ` Thomson Publishing, FRANCE, 1996. [14] R. Uzgalis, Hashing Concepts and the Java Programming Language, Rap. Tech., University of Auckland, New Zealand, 1996.

221

Index
!= (oprateur), voir galit e e e && (oprateur), voir et logique e == (oprateur), voir galit e e e || (oprateur), voir ou logique e acc`s e direct, 9 squentiel, 9, 205 e add (mthode des les) e en liste, 67 en tableau, 66 adresse, 9, 185, 186 algorithme formalisation, 29 allocation (mmoire), 7 e alphabet, 97 anctre, 90 e append (mthode) e itratif, 20 e rcursif, 20 e arbre, 90 de syntaxe abstraite, 151 AVL, 132 binaire, 95 complet, 96, 112 de recherche, 125 tass, 105 e couvrant, 95 de dcision, 99 e de drivation, 146 e de Fibonacci, 133 de slection, 110 e de syntaxe abstraite, 100 enracin, 90 e quilibr, 132 e e libre, 90 ordonn, 91, 124 e arcs, 89 arte, 90 e arit, 90 e association, 73 automate, 163 transition, 171 ni dterministe, 164 e ni non-dterministe, 167 e reprsentation Java, 175 e table de transition, 164 backtracking, 177 balise, 139 biblioth`que, 213 e break (mot-cl), 195, 197 e buer, voir tampon BueredReader (classe), 218 calculabilit, 29 e catch (mot-cl), 202 e champ, 179, 183 char (type scalaire), 219 chemin simple, 90 circuit, 89 class gnrique, 63, 84 e e code, 97 prxe, 111 e complet, 111 collision, 77 complexit, 29 e asymptotique, 33 dun algorithme au pire cas, 30 en moyenne, 30 dun probl`me, 31 e concatnation e des cha nes, 11, 215 des listes, 20, voir append et nappend des mots, 97, 143 contenu, 121 continue (mot-cl), 197 e copy (mthode) e itratif, 15 e cot amorti, 61 u dbordement de la pile, 43 e dcidable, 29 e 222

INDEX descendant, 90 dichotomique,recherche, 35 dictionnaire, 126 distance, 96 do (mot-cl), 196 e double hachage, 83 double rotation, 134 droite, 134 galit, 187 e e physique, 188 structurelle, 188 else (mot-cl), 195 e encapsulage, 25, 76, 153 encodage, 212 encoding, voir encodage enfant, 90 enscapsulage, 24 entre e standard, 205 erreur de type, 188 et logique, 220 exception, 61, 201 attraper, voir try, 202 dclarer, voir throws, 204 e dnir, 203 e lancer, voir throw, 203 expression, 192 expression conditionnelle, 219, 220 extends (mot-cl), 182, 203 e extrmit, 89 e e facteur de charge, 79 feuille, 90 chier, 205 binaire, 205 fermer, 206 ouvrir, 206 texte, 205, 212 FIFO, 53 le, 53, 123 de priorit, 104 e FileReader (classe), 218 lle, 90 ls, 90 droit, 121 gauche, 121 nal (mot-cl), 191 e ux, 205 bueris, 208 e for (mot-cl), 196 e fort, 91 e fusion de listes ordonnes, 109 e des listes tries, 40, voir merge e garbage collection, 13 gestion automatique de la mmoire, 7, 13 e Grand , 34 Grand , 34 Grand O, 34 graphe, 89 connexe, 90 non orient, 89 e orient, 89 e hachage, 77 HashMap (classe), 84 hauteur dun arbre, 90 dun arbre binaire, 96 moyenne, 132 hritage, 182 e idiome, 9 boucle innie, 50, 196 boucle vide, 21 oprateur logique squentiel, 37 e e oprateur logique squentiel, 38, 220 e e parcours de liste, 9 if (mot-cl), 195 e implements (mot-cl), 75 e import (mot-cl), 197 e indcidable, 29 e inxe, 193 initialisation dire, 15, 43 ee initialisation par dfaut, 192, 198 e InputStreamReader (classe), 218 insertion dans un arbre, 106 dans une liste trie, 37 e insertionSort (mthode), 37 e instruction, 192 interface (mot-cl), 74 e Landau, 34 lettre, 97 lex`me, 211 e LIFO, 53 liste, 7 boucle, 45 e circulaire, 45

223

224 doublement cha ee, 50, 69 n simplement cha ee, 8 n longueur dun chemin, 89 dun mot, 97, 143, 163 dun tableau, 199 dune cha ne, 215 dune liste, 10 main (mthode), 180 e mem (mthode) e itratif, 11 e rcursif, 12 e membre (dune classe), 179 m`re, 90 e merge (mthode) e itratif, 43 e rcursif, 40 e mergeSort (mthode), 41 e mesure lmentaire, 30 ee mot, 97 nappend (mthode) e itratif, 21 e rcursif, 20 e nud, 89 interne, 90 nombres de Catalan, 97 notation inxe, 57, 101, 102 postxe, 55 nremove (mthode) e itratif, 18 e rcursif, 18 e null (mot-cl), 184 e objet, 183, 186 ordre fractionnaire, 99 inxe, 98 prxe, 98 e suxe, 98 origine, 89 ou logique, 12, 37, 220 overloading, voir surcharge overriding, 185 package, 182 parcours, 123 darbre, 98 en largeur, 98, 123 en profondeur, 98 inxe, 98 postxe, 98 prxe, 98, 123 e suxe, 98 parent, 90 partition, 91 p`re, 90 e pile, 53, 123 pointeur, 186 de pile, 59 pop (mthode) e en liste, 63 en tableau, 59 porte lexicale, 194 e postxe, 193 prxe, 97, 193 e prxe (dun mot), 143 e prxiel, 97 e private (mot-cl), 181, 182 e profondeur dun nud, 96 protected (mot-cl), 182 e public (mot-cl), 180, 182 e push (mthode) e en liste, 63 en tableau, 59 queue, voir le a ` deux bouts, 69

INDEX

racine, 90, 95, 121 Random (classe), 218 Reader (classe), 217 recherche dichotomique, 35 rcursion e terminale, 23 rednition de mthode, voir overriding e e rfrence, 186 ee remove (mthode) e itratif, 16 e rcursif, 12 e remove (mthode des les) e en liste, 67 en tableau, 66 return (mot-cl), 194 e reverse (mthode) e itratif, 21 e rcursif, 22 e rotation, 134

INDEX scalaire, 186 signature, 192, 204 sondage linaire, 81 e sortie derreur standard, 205 standard, 205 sous-arbre, 90 droit, 95 gauche, 95 sous-classe, 185, 203, 205, 206 sous-typage, 185, 189, 207 spcication de visibilit, 182 e e Stack (classe), 63 stack, voir pile stack overow, voir dbordement de la pile e static (mot-cl), 179, 191 e String (classe), 215 StringBuer (classe), 216 StringBuilder (classe), 11, 216 structure dynamique, 7 fonctionnelle, voir non-mutable imprative, voir mutable e inductive, 8 mutable, 18, 20 non-mutable, 12, 20 persistante, voir non-mutable squentielle, 9 e statique, 7 structure de bloc, 194 suxe (dun mot), 143 suppression dune cl, 128 e dans un arbre, 106 surcharge, 183, 185, 216 switch (mot-cl), 195 e tableau, 198 taille dun arbre, 123 dun tableau, 199 dune cha ne, 215 tampon, 208, 216 tas, 105 this (mot-cl), 184, 193, 197 e throw (mot-cl), 47, 61, 203 e throws (mot-cl), 61, 62, 204 e token, voir lex`me e toString, 10 toString rednition, 185 e tri par comparaison, 99 par tas, 109 tri (des listes) fusion, 41 par insertion, 37 try (mot-cl), 62, 202 e type, 188 abstrait, 71 Union-Find, 91 uniq (mthode), 37 e valeur, 186 variable, 186 variable dinstance, 183 while (mot-cl), 196 e

225

Das könnte Ihnen auch gefallen