Planche 1
Planche 2
Plan
- Représentation des arbres
- Calcul formel
- Piles
- Arbres de recherche
- Arbres équilibrés
- Exercices
Planche 3
Arbres en Java
Simple généralisation de la structure de Liste.
class ArbreInt {
int valeur;
ArbreInt filsG;
ArbreInt filsD;
ArbreInt (int v, ArbreInt a, ArbreInt b) {
valeur = v; filsG = a; filsD = b;
}
}
Planche 4
Arbres en Ocaml
Arbres non modifiables:
type 'a arbre = Vide | Noeud of 'a * 'a arbre * 'a arbre;;
Arbres modifiables:
type 'a arbre = Vide | Noeud of 'a noeud
and 'a noeud = {mutable valeur: 'a;
mutable filsG: 'a arbre;
mutable filsD: 'a arbre} ;;
Planche 5
Représentation des termes
On veut faire du calcul symbolique sur des termes. On représente les
termes par des arbres. Par exemple:
class Terme {
final static int ADD = 0, SUB = 1, MUL = 2, DIV = 3, MINUS = 4,
VAR = 5, CONST = 6;
int op;
int valeur; String nom;
Terme a1, a2, a3;
Terme (int op, Terme a) {this.op = op; a1 = a; }
Terme (int op, Terme a, Terme b) {this.op = op; a1 = a; a2 = b; }
Terme (String s) {op = VAR; nom = s; }
Terme (int v) {op = CONST; valeur = v; }
}
Planche 6
Exemples de termes
Terme t = new Terme (MUL,
new Terme (ADD, new Terme ("x"), new Terme (1)),
new Terme (ADD,
new Terme (MUL, new Terme (3), new Terme ("x")),
new Terme (2)));

Planche 7
Représentation des termes
type terme = Add of terme * terme
| Sub of terme * terme
| Mul of terme * terme
| Div of terme * terme
| Minus of terme
| Var of string
| Const of int;;
let t = Mul (Add (Var "x", Const 1),
Add (Mul (Const 3, Var "x"), Const 2));;
Planche 8
Impression d'un terme
On suit la syntaxe des termes pour imprimer.
public String toString () { return toStrS (this); }
static String toStrS (Terme t) { switch (t.op) {
case ADD: return toStrS (t.a1) + "+" + toStrS (t.a2);
case SUB: return toStrS (t.a1) + "-" + toStrP (t.a2);
default: return toStrP (t);
}
}
static String toStrP (Terme t) { switch (t.op) {
case MUL: return toStrP (t.a1) + "*" + toStrP (t.a2);
case DIV: return toStrP (t.a1) + "/" + toStrF (t.a2);
default: return toStrF (t);
}
}
Planche 9
Impression d'un terme
static String toStrF (Terme t) { switch (t.op) {
case MINUS: return "-" + toStrF (t.a1);
case VAR: return t.nom;
case CONST: return t.valeur + "";
default: return "(" + toStrS (t) + ")";
}
}
... System.out.println (t); ...
Planche 10
Impression d'un terme en ML
open Printf;;
let imprimer t =
let rec impS t = match t with
Add(u,v) -> impS u; printf " + "; impS v
| Sub(u,v) -> impS u; printf " - "; impP v
| _ -> impP t
and impP t = match t with
| Mul(u,v) -> impP u; printf " * "; impP v
| Div(u,v) -> impP u; printf " / "; impF v
| _ -> impF t
and impF t = match t with
Minus (u) -> printf "-"; impF u
| Var (x) -> printf "%s" x
| Const (n) -> printf "%d" n
| _ -> printf "("; impS t; printf ")"
in impS t; printf "\n" ;;
Planche 11
Implémentation avec des sous-classes
class Terme {
int valeur; String nom;
Terme a1, a2, a3;
}
class Add extends Terme { Add (Terme x, Terme y) { a1 = x; a2 = y; } }
class Sub extends Terme { Sub (Terme x, Terme y) { a1 = x; a2 = y; } }
class Mul extends Terme { Mul (Terme x, Terme y) { a1 = x; a2 = y; } }
class Div extends Terme { Div (Terme x, Terme y) { a1 = x; a2 = y; } }
class Minus extends Terme { Minus (Terme x) { a1 = x; } }
class Var extends Terme { Var (String s) { nom = s; } }
class Const extends Terme { Const (int n) { valeur = n; } }
Terme t = new Mul( new Add( new Var ("x"), new Const (1)),
new Add( new Mul( new Const (3), new Var ("x")),
new Const (2)));
Planche 12
Implémentation avec des sous-classes -- bis
public String toString () { return toStrS (this); }
static String toStrS (Terme t) {
if (t instanceof Add)
return toStrS (t.a1) + "+" + toStrS (t.a2);
else if (t instanceof Sub)
return toStrS (t.a1) + "-" + toStrP (t.a2);
else return toStrP (t);
}
static String toStrP (Terme t) {
if (t instanceof Mul)
return toStrP (t.a1) + "*" + toStrP (t.a2);
else if (t instanceof Div)
return toStrP (t.a1) + "/" + toStrF (t.a2);
else return toStrF (t);
}
Planche 13
Implémentation avec des sous-classes -- ter
static String toStrF (Terme t) {
if (t instanceof Minus)
return "-" + toStrF (t.a1);
else if (t instanceof Var)
return t.nom;
else if (t instanceof Const)
return t.valeur + "";
else return "(" + toStrS (t) + ")";
}
Planche 14
Implémentation orientée-objet
abstract class Terme {
int valeur; String nom;
Terme a1, a2, a3;
public abstract String toString ();
abstract String toStr (Terme t, int precedence);
static String norm (int pout, int pin, String s) {
return (pout <= pin) ? s : "(" + s + ")";
}
}
class Add extends Terme {
Add (Terme x, Terme y) { a1 = x; a2 = y; }
public String toString () {return this.toStr(0); }
String toStr (int p) { return norm (p, 0, a1 + "+" + a2); }
}
Planche 15
Implémentation orientée-objet -- bis
class Sub extends Terme {
Sub (Terme x, Terme y) { a1 = x; a2 = y; }
public String toString () {return this.toStr(0); }
String toStr (int p) {
return norm (p, 0, a1 + "-" + a2.toStr(1));
}
}
class Mul extends Terme {
Mul (Terme x, Terme y) { a1 = x; a2 = y; }
public String toString () {return this.toStr(1); }
String toStr (int p) {
return norm (p, 1, a1.toStr(1) + "*" + a2.toStr(1));
}
}
Planche 16
Implémentation orientée-objet -- ter
class Div extends Terme {
Div (Terme x, Terme y) { a1 = x; a2 = y; }
public String toString () {return this.toStr(1); }
String toStr (int p) {
return norm (p, 1, a1.toStr(1) + "/" + a2.toStr(2));
}
}
class Minus extends Terme {
Minus (Terme x) { a1 = x; }
public String toString () {return this.toStr(2); }
String toStr (int p) { return "-" + a2.toStr(2); }
}
Planche 17
Implémentation orientée-objet -- 4
class Var extends Terme {
Var (String s) { nom = s; }
public String toString () {return this.toStr(2); }
String toStr (int p) { return nom; }
}
class Const extends Terme {
Const (int n) { valeur = n; }
public String toString () {return this.toStr(2); }
String toStr (int p) { return valeur + ""; }
}
La programmation procédurale organise le code selon les procédures.
La programmation OO encourage à répartir le code selon les données.
Planche 18
Formes préfixes, infixes ou postfixes
Nous avons vu l'impression d'un terme sous forme infixe. On peut aussi
la faire sous forme (polonaise) préfixe (sans les parenthèses)
static String toStrPref (Terme t) {switch (t.op) {
case ADD: return "+ " + toStrPref (t.a1) + " " + toStrPref (t.a2);
case SUB: return "- " + toStrPref (t.a1) + " " + toStrPref (t.a2);
case MUL: return "* " + toStrPref (t.a1) + " " + toStrPref (t.a2);
case DIV: return "/ " + toStrPref (t.a1) + " " + toStrPref (t.a2);
case MINUS: return "--" + toStrPref (t.a1);
case VAR: return t.nom;
case CONST: return t.valeur + "";
default: return "";
}
}
Planche 19
Forme préfixes, infixes ou postfixes -- 2
Si on veut unifier avec toString(), on peut rendre cette procédure non
statique.
String toStrPref () {switch (op) {
case ADD: return "+ " + a1.toStrPref () + " " + a2.toStrPref ();
case SUB: return "- " + a1.toStrPref () + " " + a2.toStrPref ();
case MUL: return "* " + a1.toStrPref () + " " + a2.toStrPref ();
case DIV: return "/ " + a1.toStrPref () + " " + a2.toStrPref ();
case MINUS: return "--" + a1.toStrPref ();
case VAR: return nom;
case CONST: return valeur + "";
default: return "";
}
}
Planche 20
Forme préfixes, infixes ou postfixes -- 3
Un simple changement d'ordre de parcours de la récursion dans la
procédure donne la forme (polonaise) postfixe
String toStrPost () {switch (op) {
case ADD: return a1.toStrPost () + " " + a2.toStrPost () + " +";
case SUB: return a1.toStrPost () + " " + a2.toStrPost () + " -";
case MUL: return a1.toStrPost () + " " + a2.toStrPost () + " *";
case DIV: return a1.toStrPost () + " " + a2.toStrPost () + " /";
case MINUS: return a1.toStrPost () + "--";
case VAR: return nom;
case CONST: return valeur + "";
default: return "";
}
}
Donc l'impression préfixe, infixe ou postfixe correspond simplement à
des parcours d'arbres différents.
Planche 21
Evaluation des expressions arithmétiques
On donne une a-liste (liste d'association) pour associer une valeur à
chaque variable.
static int eval (Terme t, AListe env) {switch (t.op) {
case ADD: return eval (t.a1, env) + eval (t.a2, env);
case SUB: return eval (t.a1, env) - eval (t.a2, env);
case MUL: return eval (t.a1, env) * eval (t.a2, env);
case DIV: return eval (t.a1, env) / eval (t.a2, env);
case MINUS: return - eval (t.a1, env);
case VAR: return assoc (t.nom, env);
case CONST: return t.valeur;
default: return 0;
}
Planche 22
Dérivation formelle
static Terme der (Terme t, Var x) {
if (t instanceof Const)
return new Const (0);
else if (t instanceof Add)
return new Add (der(t.a1, x), der(t.a2, x));
else if (t instanceof Mul)
return new Add (new Mul(t.a1, der(t.a2, x)),
new Mul(der(t.a1, x), t.a2));
else if (t instanceof Div)
return new Div(new Sub (new Mul(der(t.a1,x), t.a2),
new Mul(t.a1, der(t.a2, x))),
new Mul (t.a2, t.a2));
else if (t instanceof Var)
return new Const (x.equals(t.nom) ? 1 : 0);
else if (t instanceof Minus)
return new Minus (der(t.a1, x));
}
Planche 23
Dérivation formelle en ML
let rec der t x= match t with
| Add (t1, t2) -> Add (der t1 x, der t2 x)
| Mul (t1, t2) ->
Add (Mul (t1, der t2 x), Mul (der t1 x, t2))
| Div (t1, t2) -> Div
(Sub (Mul (der t1 x, t2), Mul (t1, der t2 x)),
Mul (t2, t2))
| Sub (t1, t2) -> Sub (der t1 x, der t2 x)
| Var (y) -> if x = y then Const (1) else Const (0)
| Const (x) -> Const (0)
| Minus (t1) -> Minus (der t1 x)
;;
Le filtrage simplifie bien l'affaire. ML est adapté au calcul
symbolique. Tous les systèmes de démonstration automatiques au monde
(HOL, PVS, Coq, LF, Elf) sont maintenant écrits en (O)Caml.
Planche 24
Simplifications -- Réécritures
Le résultat est peu présentable, car il faut faire des simplifications
et ainsi normaliser le résultat. C'est ce que savent très bien faire
des systèmes de calcul formel comme Axiom, Matematica ou Maple.
Il faut donc appliquer des règles telles que:
| 0 + x |
® |
x |
| x + 0 |
® |
x |
| 0 × x |
® |
0 |
| x × 0 |
® |
0 |
| 1 × x |
® |
x |
| x × 1 |
® |
x |
| x × (y + z) |
® |
x × y + x × z |
| (x + y) × z |
® |
x × z + y × z
|
Planche 25
Théories complètes
On peut résoudre des petits problèmes équationnels. Par exemple, la
théorie des groupes libres est donnée par les 10 règles suivantes:
|
(x + y) + z |
® |
x + (y + z) |
(1) |
|
0 + x |
® |
x |
(2) |
|
(- x) + x |
® |
0 |
(3) |
|
(- x) + (x + y) |
® |
y |
(4) |
|
- 0 |
® |
0 |
(5) |
|
x + 0 |
® |
x |
(6) |
|
- (- x) |
® |
x |
(7) |
|
x + (- x) |
® |
0 |
(8) |
|
x + ((- x) + y) |
® |
y |
(9) |
|
- (x + y) |
® |
(- y) + (- x)
|
(10) |
Toute égalité est prouvée par la mise en forme normale selon les
règles de réécritures précédentes. (La méthode de complétion de Knuth
et Bendix permet d'obtenir systématiquement ces règles.)
Planche 26 Ordres de parcours d'un arbre
Ce sont des manières de numéroter les noeuds (et feuilles) d'un
arbres.
- Dans le parcours préfixe (preorder) , on numérote d'abord la
racine, puis le sous-arbre de gauche, puis le sous-arbre de droite.
- Dans le parcours infixe, on numérote d'abord le sous-arbre de gauche,
puis la racine, puis le sous-arbre de droite.
- Dans le parcours postfixe (postorder), on numérote d'abord
le sous-arbre de gauche, puis le sous-arbre de droite, puis la racine.
Remarque: nous avons vu implicitement ces parcours lors de
l'impression des termes.
Planche 27 Pile
Une pile correspond à la stratégie LIFO (Last In, First Out). On peut
la représenter par un tableau ou une liste (cf poly chapitre 3).
class Pile {
final static int maxP = 100;
int hauteur ;
Element contenu[];
Pile () {hauteur = 0; contenu = new Element[maxP]; }
static void empiler (Element x, Pile p) throws ExceptionPilePleine {
if (p.hauteur == maxP)
throw new ExceptionPilePleine();
p.contenu[p.hauteur++] = x;
}
Planche 28 Pile -- 2
static int depiler (Pile p) throws ExceptionPileVide { {
if (p.hauteur == 0)
throw new ExceptionPile ("vide");
int res = p.contenu [p.hauteur - 1];
-- p.hauteur;
return res;
}
}
Tout programme récursif peut s'écrire itérativement avec une pile
(Randell et Russel, 1960, ont ainsi inventé le concept de compilateur)
Pour l'évaluation des expressions arithmétiques, on peut donc la faire
avec une pile, en empilant les valeurs et opérateurs rencontrées et en
faisant des opérations entre sommet et sous-sommet de la pile.
Planche 29 Arbres de recherche
On revient à un problème standard d'algorithmique: comment faire de la
recherche en table dynamique? Ie sans aucune limitation de taille.
Une technique est de faire des arbres de recherche. Les noeuds d'un
tel arbre sont étiquetées par des clés (ici des entiers) de telle
manière que tout noeud est supérieur aux noeuds de son sous-arbre de
gauche, mais inférieur aux noeuds de son sous-arbre de gauche.
Autrement dit, l'ordre des valeurs stockées sur les noeuds respecte
l'ordre infixe.
Planche 30 Recherche/insertion dans un arbre de recherche
static Arbre recherche (int x, Arbre a) {
if (a == null) return null;
else if (x == a.cle) return a;
else if (x < a.cle) return recherche (a.filsG);
else return recherche (a.filsD);
}
static Arbre ajouter (int x, Arbre a) {
if (a == null) return new Arbre (x, null, null);
else if (x == a.cle) return a;
else if (x < a.cle) return new Arbre (ajouter (a.filsG), a.filsD);
else return new Arbre (a.filsG, ajouter (a.filsD));
}
Complexité?
Planche 31 Recherche/insertion dans un arbre de recherche en ML
let rec recherche x a =
match a with
| Arbre_vide -> Arbre_vide
| Noeud (g, y, d) ->
if x = y then a else
if x < y then recherche x g
else recherche x d;;
let rec ajouter x a =
match a with
Arbre_vide -> Noeud (Arbre_vide, x, Arbre_vide)
| Noeud (g, y, d) ->
if x < y then Noeud (ajouter x g, y, d)
else Noeud (g, y, ajouter x d);;
Planche 32 Arbres équilibrés
Pour garder une bonne efficacité, on essaie de garder les arbres
équilibrés.
Un arbre est parfaitement équilibré ssi pour tout noeud les hauteurs
des fils gauche et droit sont égales. Difficile à réaliser.
Un arbre est équilibré (arbre AVL) ssi pour tout noeud les hauteurs
des fils gauche et droit ne diffèrent pas de plus que 1.
Pour maintenir un arbre équilibré, on est amené à faire des rotations
lors de l'insertion d'un nouvel élément.
A rotG B
/ \ --> / \
a B A c
/ \ <-- / \
b c rotD a b
(cf polycopié)
Planche 33 Arbres 2-3-4
Dans un arbre AVL, il faut stocker les hauteurs ou les différences de
hauteurs entre fils droit et gauche pour tout noeud (au moins 2
bits). La programmation n'est pas simple.
Si on donne un peu de flexibilité aux noeuds, on évite les
rotations. Chaque noeud peut avoir 2, 3 ou 4 fils, l'arbre est
toujours ordonné selon le parcours infixe. On n'insére un nouveau
noeud que dans un arbre dont le père n'a pas 4 noeuds. Au cours de la
recherche, on éclate tous les 4-noeuds
A A C
/ \ --> / | \
a B C D a B D
/ / \ \ /| |\
b c d e b c d e
A B A B D
/ | \ --> / / \ \
a b C D E a b C E
/ / \ \ /| |\
c d e f c d e f
Planche 34 Arbres rouges-noirs, B-trees
Les arbres rouge-noir (ou bicolores) représentent les arbres 2-3-4 par
des arbres binaires dont on colorie les arcs en rouge ou noir. La
couleur rouge veut dire "arc transparent". Autrement dit, deux noeuds
reliés par du rouge forment un même noeud dans l'arbre 2-3-4. Ainsi,
on représente les 4 noeuds par:
A B C == B A B == B ou A
/ / \ \ / \ / | \ / \ / \
a b c d A C a b c A c a B
/| |\ / \ / \
c d e f a b b c
L'éclatement des arbres 2-3-4 correspond à de simples changements de
couleur et à quelques rotations.
Les B-trees sont une version des arbres 2-3-4 ou un noeud peut varier
entre 2 et 2k noeuds. Ils interviennent pour faire des index sur
les fichiers. Par exemple, dans le système de fichiers de MacOS, pour
accéder à un fichier et à sa description dans le système de fenêtres.
Planche 35 Exercices en TD
- Faire une calculette. On donne les valeurs des variables, et on
imprime et calcule l'expression arithmétique.
- Faire une calculette qui sait dériver formellement.
- L'exercice de la prochaine fois consistera à rentrer
l'expression arithmétique sous une forme plus agréable (en faisant une
analyse syntaxique).
This document was translated from LATEX by HEVEA.
|