Option Informatique/cours de l`option informatique

publicité
Option informatique MPSI
Lycée Cézanne.
Année 2016-2017
© MPSI–Joly–Lycée Cézanne–2016-2017
2
Table des matières
1 Quelques idées sur le typage
1.1 Pourquoi des types ? . . . . . . . . . .
1.2 types génériques . . . . . . . . . . . .
1.3 Les constructeurs des types génériques
1.4 Fonctions à profil générique . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
1
1
2
2
2
2 Introduction à Caml(light)
2.1 Premiers pas en Caml . . . . . . . . . . .
2.2 Quelques types de base en Caml . . . . .
2.2.1 Type unit . . . . . . . . . . . . .
2.2.2 Types int et float . . . . . . . .
2.2.3 Type bool . . . . . . . . . . . . .
2.2.4 Les types char et string . . . . .
2.2.5 Produits cartésiens . . . . . . . . .
2.2.6 Deux types définis par l’utilisateur
2.3 Les fonctions, premiers pas . . . . . . . .
2.4 Le filtrage . . . . . . . . . . . . . . . . . .
2.5 Structures de contrôle . . . . . . . . . . .
2.5.1 Structure conditionnelle . . . . . .
2.5.2 Boucle for . . . . . . . . . . . . . .
2.5.3 Les références . . . . . . . . . . . .
2.5.4 Boucle while . . . . . . . . . . . .
2.6 Filtrage et récursivité . . . . . . . . . . .
2.6.1 Principe général . . . . . . . . . .
2.6.2 Remplacement des boucles . . . .
2.6.3 Remplacement des références . . .
2.7 Récursivité terminale . . . . . . . . . . . .
2.8 Tableaux . . . . . . . . . . . . . . . . . .
2.9 Listes . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
4
4
6
6
6
8
8
9
9
11
14
15
16
16
16
19
20
20
21
22
22
24
25
3 Structures de données
3.1 À propos des structures de données
3.2 Piles . . . . . . . . . . . . . . . . .
3.2.1 Spécification . . . . . . . .
3.2.2 Implémentations . . . . . .
3.2.3 intérêt . . . . . . . . . . . .
3.2.4 Une application . . . . . . .
3.3 Files . . . . . . . . . . . . . . . . .
3.3.1 Spécification . . . . . . . .
3.3.2 Implémentations . . . . . .
3.3.3 Files de priorité . . . . . . .
3.4 Dictionnaires . . . . . . . . . . . .
3.4.1 Spécification . . . . . . . .
3.4.2 Implémentations . . . . . .
3.5 Arbres . . . . . . . . . . . . . . . .
3.5.1 Aspects informels . . . . . .
3.5.2 Aspects formels . . . . . . .
3.5.3 Spécification . . . . . . . .
3.5.4 Implémentation . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
29
29
30
30
31
32
33
35
35
36
41
42
42
42
42
43
45
48
49
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
i
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
TABLE DES MATIÈRES
3.5.5
3.5.6
TABLE DES MATIÈRES
Quelques exemples de filtrages . . . . . . . . . . . . . . . . . . . . . . . . .
Parcours d’arbres binaires . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4 Méthodes de programmation
4.1 Récursivité . . . . . . . . . . . . . . . . . . . . . . . . .
4.1.1 Un exemple simple . . . . . . . . . . . . . . . . .
4.1.2 Construction et principe . . . . . . . . . . . . . .
4.1.3 Terminaison et correction . . . . . . . . . . . . .
4.1.4 Terminaison et correction, exemples . . . . . . .
4.1.5 Terminaison et correction, exercices d’application
4.2 Diviser pour régner . . . . . . . . . . . . . . . . . . . . .
4.2.1 Principe de la méthode . . . . . . . . . . . . . .
4.2.2 Exemples d’algorithmes diviser pour régner . . .
4.3 Programmation dynamique . . . . . . . . . . . . . . . .
4.3.1 Principes de la programmation dynamique . . . .
4.3.2 Ordonnancement de tâches pondérées . . . . . .
4.3.3 Distance d’édition . . . . . . . . . . . . . . . . .
49
50
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
58
58
58
58
60
63
64
65
65
65
68
68
69
72
5 Algorithmes, analyse
5.1 Complexité, introduction . . . . . . . . . . . . . . . . . . .
5.1.1 Premières idées . . . . . . . . . . . . . . . . . . . .
5.1.2 Deux exemples . . . . . . . . . . . . . . . . . . . .
5.1.3 Résultats théoriques . . . . . . . . . . . . . . . . .
5.2 Complexité, cas des algorithmes diviser pour régner . . .
5.2.1 Un petit point technique utile . . . . . . . . . . . .
5.2.2 Retour sur la stratégie . . . . . . . . . . . . . . . .
5.2.3 Cas plus général . . . . . . . . . . . . . . . . . . .
5.3 Exemples de complexité d’algorithmes diviser pour régner
5.3.1 L’algorithme d’exponentiation rapide . . . . . . . .
5.3.2 L’algorithme de Knuth . . . . . . . . . . . . . . . .
5.3.3 L’Algorithme de Strassen . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
83
83
83
85
85
86
86
86
87
88
88
89
90
© MPSI–Joly–Lycée Cézanne–2016-2017
ii
Chapitre 1
Quelques idées sur le typage
“
Well-typed programs cannot "go wrong"
”
Robert Milner, A Theory of Type Polymorphism in Programming, 1978
La notion de type est essentielle en informatique. De façon simple et imagée, le typage des
données est la première pierre de la construction d’une grammaire qui permet l’interprétation des
codes exprimés dans un langage de haut niveau par la machine elle même (à bas niveau donc).
Il est possible de faire un parallèle avec les mathématiques. On sait qu’un point clef de la
compréhension d’un problème de mathématique consiste à bien comprendre à quels ensembles les
objets présentés appartiennent. Par exemple l’expression u + v ne peut être comprise que si on sait
à quels ensembles appartiennent u et v. Si u et v sont deux nombres entiers u + v est un nombre
entier, si u et v sont deux applications de R dans C, u + v est une application de R dans C.
Il est intéressant de noter que si u ∈ R et v ∈ CR , l’expression u + v n’a plus de sens !
Exercice 1.0.1
On vient de voir que si u et v n’appartiennent pas au même ensemble, l’expression u + v
n’a pas de sens. Que se passe-t-il si u ∈ C et v ∈ N ? Y-a-t-il un moyen de contourner la
difficulté ?
1.1
Pourquoi des types ?
L’attribution d’un type particulier à chaque élément d’une expression en clarifie le sens. De nombreux langages de programmation disposent ainsi d’un système de types (dits de base, élémentaires
ou prédéfinis). C’est ce que l’on voit avec Python avec les types : int, float, tuple, list,
bool, function. Par ailleurs, ces mêmes langages disposent aussi de règles de construction de
nouveaux types. Cela permet à l’utilisateur ou au langage lui même de définir un nouveau type.
Dans le cas des langages de programmation typés comme Caml , la définition se fait souvent de façon automatique. Ces règles de typage permettent d’exprimer quels opérateurs et quelles fonctions
sont applicables à quelles expressions, éliminant ainsi certaines erreurs de programmation. Comme
en mathématiques, si f est une application de R dans R et si x est un nombre réel, l’expression
f + x n’a pas de sens.
La notation e : T est usuellement utilisée pour exprimer le fait que l’expression e est de type T .
L’expression e : T est vraie ou fausse, ce n’est pas une déclaration (comme dans certains langages
comme C ou C++). Une interprétation simple de e : T consiste à considérer un type comme un
ensemble (parfois infini) de données de même nature. On peut interpréter e : T par e appartient à
T , ce qui signifie que toutes les valeurs possibles de l’expression e appartiennent à l’ensemble T .
Si on distingue le symbole “:” du symbole “∈” en théorie des types abstraits, c’est pour pouvoir
donner un type au symbole “∈” en écrivant ∈: T . On remarque que c’est cette notation qui a été
retenue pour la signature (on dit encore le profil) des applications. Lorsqu’on écrit f : E → F on
exprime le fait que f appartient à l’ensemble des applications de l’ensemble E dans l’ensemble F .
1
1.2. types génériques
1.2
Chapitre 1 : Quelques idées sur le typage
types génériques
Le cours d’informatique de tronc commun a présenté le type list du langage Python qui correspond
en fait à la notion de tableaux. Il peut y avoir des tableaux d’entiers, des tableaux de réels, des
tableaux de booléens, mais aussi des tableaux de tableaux d’entiers, etc. Pour regrouper tous ces
types sous le même concept de tableau, nous dirons que le type tableau est générique (on dit aussi
paramétrique ou polymorphe).
Il faudra se convaincre au cours de l’apprentissage de l’importance de ces types pour une
programmation fiable et logique.
Le chapitre consacré aux structures de données montrera que celles-ci sont en fait la réalisation
pratique, concrète, pour un langage donné, de la notion de type abstrait. On peut dire qu’une
structure de données est l’implémentation pratique d’un type (générique ou non).
Pour désigner un type générique, on emploie une variable substituable par un type quelconque,
c’est-à-dire une variable de type. Dans ce cours, les variables de type seront notées par des lettres
grecques.
Exemple.
On parlera par exemple du type générique LISTE(α) pour signifier que l’on peut substituer la variable α par n’importe quel type (générique ou pas). Par exemple, avec la substitution [α := N], on
obtient le type LISTE(N) des listes de nombres d’entiers naturels. La substitution [α := LISTE(N)]
dans LISTE(α) permet d’obtenir des listes de listes de nombres entiers.
1.3
Les constructeurs des types génériques
Un constructeur d’un type T est une fonction qui explique comment construire certains éléments
du type T . Lorsque le type T est générique, ses constructeurs ont un profil qui dépend de la variable
de type.
Exemple.
Le type générique LISTE(α) dispose des constructeurs suivants :
• Nil : LISTE(α), qui construit la liste vide.
• Cons : α × LISTE(α) → LISTE(α), ou Cons(x, l) désigne la liste obtenue en ajoutant x (de
type α) au début de la liste l.
Un point clef est l’implication suivante :
Cons(x1 , l1 ) = Cons(x2 , l2 ) ⇒ (x1 = x2 ) ∧ (l1 = l2 )
associée au fait que Cons(x, l) 6= Nil, elle permet de justifier que toute liste admet une unique
expression composée de Nil et de Cons.
Par exemple Cons(2, Cons(3, Cons(5, Cons(8, Nil)))) permet de construire la liste (2, 3, 5, 8).
1.4
Fonctions à profil générique
Une fonction peut avoir un profil (signature) générique, tel que α → β ou LISTE(α) → α. Quel
est le type le plus général possible que l’on peut donner à une fonction donnée ? A priori, c’est α,
mais ce n’est pas très informatif. En fait, le type le plus général que l’on cherche à écrire doit être
le plus informatif possible, (donc le plus instancié possible) dans l’ensemble des plus généraux.
Par exemple, la fonction miroir d’une LISTE (celle qui renvoie la LISTE dans l’ordre inverse)
a le profil le plus général LISTE(α) → LISTE(α). C’est une instance de α, de α → β, de α → α,
de LISTE(α) → β, de β → LISTE(α), de LISTE(α) → LISTE(β). Tous ces types seraient plus
généraux que LISTE(α) → LISTE(α). Mais c’est LISTE(α) → LISTE(α) qui est la bonne réponse
car si on l’instancie un peu plus, comme par exemple LISTE(N) → LISTE(N), on a trop perdu en
généralité. La fonction identité a son profil le plus général qui se note α → α, car la fonction id
existe pour n’importe quel type.
© MPSI–Joly–Lycée Cézanne–2016-2017
2
Chapitre 1 : Quelques idées sur le typage
1.4. Fonctions à profil générique
De même, on peut donner à l’égalité le profil le plus général suivant α × α → {vrai, faux}.
Philip Wadler (un des co-auteurs du standard générique Java) a lancé le défi suivant : “Donnez
moi le profil le plus général d’une fonction générique et je vous dirai ce qu’elle est !” Autrement
dit, pas besoin de programme dans ces cas-là. Le type définit le programme ou la fonction ! C’est
ce qu’il faut comprendre par exemple pour la fonction identité. C’est la seule fonction qui peut
avoir α → α comme profil le plus général. Ces notions resteront intuitives dans ce cours. Il faut
toutefois savoir que la théorie des types constitue un cours à part entière en informatique.
© MPSI–Joly–Lycée Cézanne–2016-2017
3
Chapitre 2
Introduction à Caml(light)
“
Science is what we understand well enough to explain to a computer. Art is
everything else we do.
”
Donald Knuth, The art of computer programming
Le but de ce cours n’est pas de devenir virtuose dans la pratique de Caml . En revanche, ne se
consacrer qu’à l’aspect théorique des choses sans programmer n’aurait aucun sens !
Caml est un langage développé par l’INRIA (Institut National de la Recherche en Informatique
et Automatique) depuis 1985. Tous les renseignements et les téléchargements se trouvent à l’adresse
suivante : http://Caml.inria.fr/index.fr.html.
Le premier chapitre consacré au typage est essentiel pour la suite. En effet, Caml est un langage
fortement typé. Certains des exemples qui suivent devraient être convaincants sur ce point.
2.1
Premiers pas en Caml
L’apprentissage en profondeur du langage Caml n’est pas le but du cours de l’option informatique.
Quelques notions liées au langage seront introduites dans les TP lorsqu’elles seront nécessaires. En
revanche les notions exposées dans ce chapitre sont essentielles et le plus souvent possible reliées
aux notions théoriques à connaître.
Dans les exemples qui suivent, on fait l’hypothèse que le contexte de départ est vide :
Aucun identificateur n’est lié à une valeur.
Comme Python, Caml peut être utilisé comme une calculatrice :
2+3;; (* un calcul évident *)
- : int = 7
Caml 2.1.1 – Un calcul simple
On notera la commande d’exécution ;;, cette commande est liée à l’utilisation de Caml en
toplevel, dans un usage plus professionnel, on code et on compile ensuite. On note aussi que les
commentaires sont encadrés par (* *). On note également que Caml présente le type du résultat
avant la valeur de celui-ci. Caml infère le type d’un résultat avant de donner sa valeur.
L’exemple Caml 2.1.2 montre que le typage est poussé en Caml
On peut créer en Caml une liaison entre un identificateur (un nom) et une valeur. Il ne s’agit
pas véritablement d’une affectation même si cela lui ressemble. Caml 2.1.3 illustre cette idée.
4
Chapitre 2 : Introduction à Caml(light)
2.1. Premiers pas en Caml
3.1;; (* 3.1 est du type float *)
- : float = 3.1
3.1+4.2;;
Toplevel input:
>3.1+4.2;;
>
This expression has type float, but is used with type int.
3.1+.4.2;; (* Tout se passe bien : addition définie sur les flottants *)
- : float = 7.3
Caml 2.1.2 – Un calcul simple qui ne fonctionne pas
let
a :
let
b :
a =
int
b =
int
3;; (* identificateur a lié à la valeur 3 *)
= 3
a+1;;
= 4
Caml 2.1.3 – Liaison
let a = 3;;
Les liaisons sont statiques, elles ne sont pas modifiées à moins que soit définie une nouvelle
liaison avec le même identificateur, comme le montre le code Caml 2.1.4.
let
a :
let
b :
let
a :
b;;
- :
a = 3;;
int = 3
b = a+1;;
int = 4
a = 2;; (* identificateur a redéfini *)
int = 2
(* valeur de b non modifiée *)
int = 4
Caml 2.1.4 – Liaison statique
Il est possible de définir des liaisons locales qui sont “oubliées” ensuite : Caml 2.1.5
let c=3 in c+1;;
- : int = 4
c;;
Toplevel input:
>c;;
>The value identifier c is unbound.
Caml 2.1.5 – Liaisons locales
Mais attention ! Les choses peuvent devenir un peu confuses : Caml 2.1.6.
On peut définir plusieurs liaisons locales simultanément : Caml 2.1.7.
Mais les définitions doivent être indépendantes : Caml 2.1.8.
© MPSI–Joly–Lycée Cézanne–2016-2017
5
2.2. Quelques types de base en Caml
let
a :
let
- :
a;;
- :
Chapitre 2 : Introduction à Caml(light)
a = 2;;
int = 2
a = 3 in a+1;; (* Un calcul est effectué, liaison locale *)
int = 4
(* On retrouve la valeur de a définie à la ligne 1 *)
int = 2
Caml 2.1.6 – Liaisons locales : Attention !
let a = 2 and b = 3 in a*b+2 ;; (* deux liaisons simultanées *)
- : int = 8
Caml 2.1.7 – Liaisons locales simultanées
let d = 2 and b = 3*d in b+b ;; (* deux liaisons simultanées dépendantes *)
Toplevel input:
>let d = 2 and b = 3*d in b+d ;;
> The value identifier d is unbound.
Caml 2.1.8 – Liaisons locales simultanées : Attention !
Pour utiliser deux liaisons locales dépendantes on peut procéder comme montrer dans Caml 2.1.9.
let a = 2 in
let b = a+1 in
a*b;;
- : int = 6
Caml 2.1.9 – Liaisons locales simultanées dépendantes
2.2
Quelques types de base en Caml
Les types de bases de Caml correspondent aux ensembles des objets les plus utilisés. Ils permettent
par ailleurs de construire d’autres types.
2.2.1
Type unit
Le type unit est le plus simple, l’ensemble correspondant à ce type est un singleton dont la valeur
est () (appelée void). Son utilité n’est pas très claire au départ, mais comment définir le type du
résultat d’une fonction qui ne renvoie rien, ou le type de l’élément de départ d’une fonction sans
argument ?
Le code Caml 2.2.1 illustre le propos.
2.2.2
Types int et float
Comme en Python, int et float correspondent au codage en machine de certains nombres entiers
(pas tous !) et de certains nombres réels (avec les mêmes astuces que ceux développés dans le cours
d’informatique pour tous). Il faut bien retenir que ces deux types sont incompatibles et qu’il existe
© MPSI–Joly–Lycée Cézanne–2016-2017
6
Chapitre 2 : Introduction à Caml(light)
2.2. Quelques types de base en Caml
print_newline();; (* la function print_newline ( ) ne renvoie pas de valeur *)
- : unit = ()
print_int(5);;
5- : unit = () (* print_int ( 5 ) affiche 5 mais ne renvoie pas de valeur *)
Caml 2.2.1 – Type unit
une addition et une multiplication pour les objets de type int (+ et ∗) et une addition et une
multiplication pour les objets de type float (+. et ∗.).
On peut retenir que la plupart des fonctions usuelles sont définies pour le type float. Les
fonctions les plus courantes sont : sqrt, cos, sin, asin, acos, atan, log, exp. Il faut noter que
a ∗ ∗b permet comme en Python de définir l’exponentiation mais que a et b doivent être de type
float.
Le code Caml 2.2.2
exp;;
- : float -> float = <fun>
#exp(3);;
Toplevel input:
exp(3);;
> This expression has type int,but is used with type float.
exp(3.);;
- : float = 20.0855369232
log(2.71828);; (* log désigne le logarithme népérien *)
(* noté ln dans le cours de math *)
- : float = 0.999999327347
Caml 2.2.2 – Fonctions
On note bien, à la ligne 2, le profil de la fonction exp, Caml précise qu’il s’agit bien d’une
fonction.
En revanche / et mod, qui donnent le quotient et le reste d’une division euclidienne, fonctionnent
évidemment avec des nombres entiers !
2014/5;; (* quotient de la division euclidienne *)
- : int = 402
2014 mod 5;; (* reste de la division euclidienne *)
- : int = 4
Caml 2.2.3 – Les fonctions / et mod
q
y
Un nombre entier n en Caml est représenté par r ∈ −262 , 262 − 1 où n = p · 263 + r (attention,
il ne s’agit pas forcément de la division euclidienne !), vous avez reconnu la représentation en
complément à 263 .1
On se souvient que 210 = 1024 ce qui permet d’atteindre 260 par 1024 ∗ 1024 ∗ 1024 ∗ 1024 ∗
1024 ∗ 1024.
On a :
1 Il
se peut que sur une vieille machine la représentation se fasse en complément à 231
© MPSI–Joly–Lycée Cézanne–2016-2017
7
2.2. Quelques types de base en Caml
Chapitre 2 : Introduction à Caml(light)
1024*1024*1024*1024*1024*1024*2*2;;
(* On dépasse la limite supérieure de 1 *)
- : int = -4611686018427387904
-1024*1024*1024*1024*1024*1024*2*2-1;;
(* On dépasse la limite inférieure ( par le bas ) de 1 *)
- : int = 4611686018427387903
-1024*1024*1024*1024*1024*1024*2*2;;
(* On atteint la limite inférieure *)
- : int = -4611686018427387904
1024*1024*1024*1024*1024*1024*2*2-1;;
(* On atteint la limite supérieure *)
- : int = 4611686018427387903
Caml 2.2.4 – Limitation de int
2.2.3
Type bool
Comme pour Python le type bool correspond à un ensemble de deux valeurs true et false.
La conjonction, la disjonction et la négation sont respectivement notées &&, || et not. Puisque
Caml évalue une expression booléenne de gauche à droite et arrête son évaluation dès que la lecture
du reste de l’expression n’est pas logiquement nécessaire (on parle d’évaluation paresseuse du
booléen) , Caml ne détecte pas nécessairement les erreurs de syntaxe d’une expression booléenne.
Le code Caml 2.2.5 illustre le propos.
2=3;; (* le signe = a le même sens que == en Python *)
- : bool = false
(2=3) && (1/0=2);; (* une erreur non détectée *)
- : bool = false
not(2=3);;
- : bool = true
2<3 || 1./.0.> exp(2.) ;; (* une autre erreur non détectée *)
- : bool = true
Caml 2.2.5 – Le type bool
2.2.4
Les types char et string
Les caractères sont de type char. Les fonctions char_of_int et int_of_char permettent de
passer respectivement d’un caractère à son code ASCII et réciproquement.
char_of_int;; (* pour confirmer le profil de la fonction *)
- : int -> char = <fun>
int_of_char;; (* pour confirmer le profil de la fonction *)
- : char -> int = <fun>
char_of_int(63);;
- : char = `?`
int_of_char(`b`);;
- : int = 98
Caml 2.2.6 – Le type char
Les chaînes de caractères sont comme en Python de type string. La manipulation des chaînes
ressemble beaucoup à ce qui se passe en Python où l’indiçage des éléments commence aussi par 0 :
© MPSI–Joly–Lycée Cézanne–2016-2017
8
Chapitre 2 : Introduction à Caml(light)
2.2. Quelques types de base en Caml
L’utilisation de l’opérateur de concaténation des chaînes en Caml est ˆ : Caml 2.2.7 :
let chaîne2 = "CPGE";;
chaîne2 : string = "CPGE"
chaîne2^" "^chaîne;; (* ^ : concaténation des chaînes *)
- : string = "CPGE Cezanne"
Caml 2.2.7 – Le type string, concaténation
Comme en Python, il faut se méfier des phénomènes d’aliasing, comme l’illustre le code Caml 2.2.8.
let chaîne1 = "CPGE";;
chaîne1 : string = "CPGE"
let chaîne2 = chaîne1;; (* deux identifiants sont liés à la même donnée *)
chaîne2 : string = "CPGE"
chaîne2.[2] <- `P`;;
- : unit = ()
chaîne1;;
- : string = "CPPE"
Caml 2.2.8 – Le type string, aliasing
2.2.5
Produits cartésiens
Caml permet de définir des types obtenus par produit cartésien d’ensembles/types de natures hétérogènes. On peut définir les données correspondantes avec des parenthèses (si c’est nécessaire) et
séparer les différents éléments par des virgules. Ces notions sont illustrées par Caml 2.2.9.
("papa",23,"maman",exp(2.));;
- : string * int * string * float = "papa", 23, "maman", 7.38905609893
"mes freres",4,"mes soeurs", sqrt(25.);;
- : string * int * string * float = "mes freres", 4, "mes soeurs", 5.0
Caml 2.2.9 – Produits cartésiens
2.2.6
Deux types définis par l’utilisateur
Caml permet à l’utilisateur de définir ses propres types de deux façons différentes :
• Une première façon consiste à définir le type personnalisé par une liste constituées du nom
des éléments et du type (déjà existant ou déjà défini) de chacun de ces éléments. On parle
alors de type produit ou enregistrement.
• Une deuxième façon de procéder consiste simplement à définir le nouveau type par l’union
disjointe des éléments qui le composent. On parle dans ce cas de type somme même si le terme
ne semble pas très bien choisi.
On peut par exemple définir le type complexe qui servira à représenter les nombres complexes.
On peut alors utiliser ce nouveau type pour en définir un autre (le type type_inutile de notre
exemple qui n’a qu’un intérêt pédagogique) : Caml 2.2.10.
© MPSI–Joly–Lycée Cézanne–2016-2017
9
2.2. Quelques types de base en Caml
Chapitre 2 : Introduction à Caml(light)
type complexe = {x:float;y:float};;
Type complexe defined.
type type_inutile = {z : complexe ; yy : float};;
Type type_inutile defined.
let Z ={x=2.0;y=1.0};;
Z : complexe = {x = 2.0; y = 1.0}
let machin = {z=Z;yy=Z.x**2.+.Z.y**2.};;
machin : type_inutile = {z = {x = 2.0; y = 1.0}; yy = 5.0}
Caml 2.2.10 – Le type enregistrement
Remarques.
• Il est important de choisir des identifiants différents pour les différents enregistrements susceptibles d’être utiles. Dans l’exemple Caml 2.2.10 les champs de l’enregistrement complexe
sont x et y, ceux de type_inutile sont z et yy. C’est ce qui permet au langage de reconnaître
les instances de chacun des enregistrements, ici de différencier les objets de type complexe
des objets de type type_inutile.
• On note dans Caml 2.2.10 que Z.x et Z.y permettent d’accéder aux valeurs de x et y dans Z.
Il peut être utile et souhaitable de pouvoir modifier la valeur d’un champ d’un objet de type
enregistrement. Pour cela il suffit de le rendre mutable :
type complexe = {mutable x:float;y:float};; (* x est défini mutable *)
Type complexe defined.
let z = {x=1./.2.;y=sqrt(3.)/.2.};;
z : complexe = {x = 0.5; y = 0.866025403784}
z.x<-1.;;
- : unit = ()
z;;
- : complexe = {x = 1.0; y = 0.866025403784}
Caml 2.2.11 – Le type enregistrement, champ mutable
En mathématiques on peut distinguer les ensembles C et Z[i], l’ensemble des entiers de Gauss
def défini par Z[i] = x + iy, (x, y) ∈ Z2 . En terme d’implémentation informatique sous forme d’enregistrements, on peut proposer : Caml 2.2.12.
type
Type
type
Type
complexe = {x:float;y:float};;
complexe defined.
entierGauss = {xx:int;yy:int};;
entierGauss defined.
Caml 2.2.12 – Le type enregistrement, entiers de Gauss
Cette solution n’est pas très pratique, en effet les deux enregistrements fonctionnent sur le même
modèle, il serait idéal de laisser le choix du type des champs x et y dans l’enregistrement complexe
qui deviendrait alors polymorphe. Caml permet de réaliser ce projet en utilisant des variables de
type ’a qui représentent les variables de type que nous avons évoquées dans le premier chapitre.
Il est alors possible d’instancier ’a par un type bien défini :
Le type somme peut s’utiliser de façon très simple : Caml 2.2.14
© MPSI–Joly–Lycée Cézanne–2016-2017
10
Chapitre 2 : Introduction à Caml(light)
2.3. Les fonctions, premiers pas
type ('a,'b) complexe = {x:'a ; y:'b};;
Type complexe defined.
let Z = {x=1.1;y=2.1};;
Z : (float, float) complexe = {x = 1.1; y = 2.1}
let Zg = {x=1;y=2};;
Zg : (int, int) complexe = {x = 1; y = 2}
Caml 2.2.13 – Le type enregistrement, polymorphisme
type matieres = math | physique | SI | LV | philo;;
Type matieres defined.
physique;;
- : matieres = physique
Caml 2.2.14 – Le type somme
On peut construire des types somme plus élaborés en précisant le type des constructeurs (notés
alors en majuscule comme dans l’exemple Caml 2.2.15 :
type matieres =
| Math of string*int
| Physique of string*int
| SI of string*int
| LV of string*int
| philo of string*int;;
Type matieres defined.
let phy=Physique("Defosseux",9);;
phy : matieres = Physique ("Defosseux", 9)
Caml 2.2.15 – Constructeur
On peut enfin noter que l’utilisateur peut créer des homonymies de type grâce au double signe
égal : ==.
Les sorties peuvent se révéler un peu curieuses parfois. L’observation des résultats de Caml 2.2.16
permet d’entrevoir comment Caml se sort des problèmes d’inférence de types.
2.3
Les fonctions, premiers pas
On a déjà évoqué la notion de fonction grâce aux applications déjà définies dans le langage (exp,
log, sin, etc...). L’utilisateur peut évidemment définir ses propres fonctions. Plusieurs syntaxes
sont possibles comme le montre les exemples de Caml 2.3.1.
Remarques.
• Dans Caml 2.3.1, la syntaxe de la première ligne est sans doute la plus simple à utiliser.
• On note dans Caml 2.3.1 que le mot clef function à la ligne 2 impose un seul argument.
Il faut employer le mot clef fun pour pouvoir utiliser plusieurs arguments avec une syntaxe
du type ->.
Quelques exemples sont donnés par Caml 2.3.2, 2.3.3, 2.3.4 et 2.3.5.
© MPSI–Joly–Lycée Cézanne–2016-2017
11
2.3. Les fonctions, premiers pas
Chapitre 2 : Introduction à Caml(light)
type entier == int;;
Type entier defined.
let f (x:entier) = (x:entier)*2;;
f : entier -> int = <fun>
f 5;;
- : int = 10
let g (x:entier) = (2*x : entier);;
g : entier -> entier = <fun>
g 5;;
- : entier = 10
Caml 2.2.16 – Type et homonymie
let f arguments = resultat ;;
let f = function argument -> resultat ;;
let f = fun arguments -> resultat;;
Caml 2.3.1 – Trois façons de définir une fonction en Caml
let f1 x = x+1;;
f1 : int -> int = <fun>
let f2 x = x +. 1.;;
f2 : float -> float = <fun>
Caml 2.3.2 – La définition de la fonction fournit un typage non ambigu
let f3 x y = x + y ;;
f3 : int -> int -> int = <fun>
let f4 x y = x +. y;;
f4 : float -> float -> float = <fun>
Caml 2.3.3 – Des cas plus élaborés, des fonctions qui donnent des fonctions...
Remarques.
• Notez bien dans l’exemple Caml 2.3.2 que le profil des fonctions f1 et f2 sont bien définis.
Les opérateurs + et +. permettent en effet de définir les profils sans ambiguïté. On dit que
Caml synthétise les types.
• On peut en revanche s’interroger sur le profil de la fonction f3 de Caml 2.3.3 qui peut paraître
curieux en première lecture. En fait on peut interpréter f3 x y comme la fonction f3 x
d’argument y, l’image de y, de type int, est l’application qui à x, de type int, associe x+y.
à y on associe donc une application de profil int -> int.
Le profil de f3 serait donc int -> (int -> int). Dans le résultat donné par Caml il suffit
de penser à “associer à droite” pour mieux comprendre.
• Dans Caml 2.3.4, on note que la fonction conjugue ne renvoie pas de résultat, elle se contente
de modifier le complexe passé en argument. Il n’y a pas de résultat, le type de la fonction
est donc complexe -> unit. On parle dans ce cas d’effet de bord car la fonction modifie un
état autre que sa valeur de retour (qui dans ce cas n’existe pas !).
© MPSI–Joly–Lycée Cézanne–2016-2017
12
Chapitre 2 : Introduction à Caml(light)
2.3. Les fonctions, premiers pas
type complexe = {mutable x : float; mutable y : float};;
Type complexe defined.
(* On peut alors définir la conjugaison d'une première façon *)
let conjugue z = z.y <- -. z.y;;
conjugue : complexe -> unit = <fun>
(* Test de la fonction conjugue *)
let Z = {x=1.;y=2.};;
Z : complexe = x = 1.0; y = 2.0
conjugue Z;;
- : unit = ()
Z;;
- : complexe = x = 1.0; y = -2.0
(* Une autre façon de faire... *)
let conjugue2 {x=a;y=b} = {x=a;y= -.b};;
conjugue2 : complexe -> complexe = <fun>
(* Test de la fonction conjugue2 *)
let ZZ={x=2.;y=3.4};;
ZZ : complexe = x = 2.0; y = 3.4
conjugue2 ZZ;;
- : complexe = x = 2.0; y = -3.4
Caml 2.3.4 – Des fonctions sur des types définis par l’utilisateur
let bizarre x y = x,y;;
bizarre : 'a -> 'b -> 'a * 'b = <fun>
Caml 2.3.5 – Une fonction qui ne présente pas beaucoup d’intérêt a priori
• La deuxième façon de définir la conjugaison dans Caml 2.3.4 montre comment définir la
conjugaison pour en faire une fonction qui retourne un résultat. Il est très important de
comprendre la différence entre les fonctions conjugue et conjugue2.
• L’exemple Caml 2.3.5 illustre un cas dans lequel Caml ne peut réaliser la synthèse de type. Le
langage se sort de cette colle en passant par les types non définis.
On peut noter, Caml 2.3.6, que l’on peut forcer le typage des fonctions dans le cas où celui-ci
est a priori ambigu.
(* définition d'une fonction de type non déterminé *)
let ambigu x = x;;
ambigu : 'a -> 'a = <fun>
(* comment lever l'ambiguïté *)
let nonAmbigu (x:int) = (x:int);;
nonAmbigu : int -> int = <fun>
Caml 2.3.6 – Comment lever l’ambiguïté de typage ?
© MPSI–Joly–Lycée Cézanne–2016-2017
13
2.4. Le filtrage
Chapitre 2 : Introduction à Caml(light)
L’usage des parenthèses peut se révéler parfois obligatoire. Comme le montre l’exemple Caml 2.3.7,
dans lequel les deux évaluations conduisent à des résultats différents.
let f x = 2*x;;
f : int -> int = <fun>
f 3 + 2;;
- : int = 8
f(3+2);;
- : int = 10
Caml 2.3.7 – Importance du parenthésage
Une dernière remarque au sujet de l’effet de bord : cette notion peut aussi s’entendre comme
effet secondaire. Une fonction qui ne renvoie pas de résultat et qui réalise néanmoins une action,
agit par effet de bord dans la mesure où, comme pour la fonction conjugue, l’action de la fonction
n’est pas visible lors de l’utilisation immédiate de la fonction. Une fonction peut néanmoins avoir
un effet de bord même si elle renvoie un résultat, comme le montre l’exemple Caml 2.3.8.
let moduleEffetBord z =
z.y <- -.z.y; (* création artificielle d'un effet de bord *)
sqrt(z.x ** 2. +. z.y ** 2.);;
moduleEffetBord : complexe -> float = <fun>
let Z = {x=3. ; y = 4.};;
Z : complexe = x = 3.0; y = 4.0
let ZZ = moduleEffetBord Z;;
ZZ : float = 5.0
(* L'effet de bord ne s'observe qu'au rappel de la variable Z *)
Z;;
- : complexe = x = 3.0; y = -4.0
Caml 2.3.8
Remarque.
L’effet de Bord n’est observé dans Caml 2.3.8 qu’au rappel de la variable passée en argument.
2.4
Le filtrage
Caml permet de définir des fonctions en envisageant différents cas correspondant à différents motifs.
On considère par exemple l’application f définie de Z dans Z par :


0 si n = 0
f : n 7→ −1 si n = 1


2n + 5 sinon
Une façon d’implémenter cette application f en Caml est le suivant :
Remarques.
• Dans Caml 2.4.1, le filtrage est défini par le mot clef match. L’argument n à filtrer est comparé
avec chacun des motifs successivement. Si l’argument correspond, l’expression de droite est
© MPSI–Joly–Lycée Cézanne–2016-2017
14
Chapitre 2 : Introduction à Caml(light)
let
| 0
| 1
| _
f :
2.5. Structures de contrôle
f n = match n with
-> 0
-> -1
-> 2*n+5;;
int -> int = <fun>
Caml 2.4.1 – Premier exemple de filtrage
renvoyée, dans le cas contraire on passe au motif suivant. Le dernier motif _ correspond au
“sinon”, placé à la fin il correspond donc à tous les autres motifs qui n’ont pas été évoqués
avant lui.
• Tous les motifs du filtrage doivent correspondre au type de l’argument du filtrage.
• Toutes les évaluations des filtrages doivent également être du même type !
Il est possible de définir la même fonction f avec les mots clefs fun ou function :
let
| 0
| 1
| n
f :
f = fun
-> 0
-> -1
-> 2*n+5;;
int -> int = <fun>
Caml 2.4.2 – Deuxième exemple de définition d’un filtrage
Remarque.
Dans Caml 2.4.2, l’identificateur n est indispensable dans le dernier motif.
Il est également possible de filtrer sur des couples comme dans l’exemple suivant :
let somme_bizarre x y = match (x,y) with
| (_,0) -> 0
| (0,_) -> 0
| _ -> x + y ;;
somme_bizarre : int -> int -> int = <fun>
Caml 2.4.3 – Filtrage sur un couple
Exercice 2.4.1
Que réalise la fonction somme_bizarre de l’exemple Caml 2.4.3 ?
Quelques règles de filtrage s’ajoutent à celles déjà évoquées dans la remarque qui suit l’exemple
Caml 2.4.1, l’exemple Caml 2.4.4 montre successivement les soucis posés par le fait de lier plusieurs
fois la même variables, les messages d’alerte qui suivent un filtrage redondant ou non exhaustif.
2.5
Structures de contrôle
Caml dispose des structures de contrôle usuelles.
© MPSI–Joly–Lycée Cézanne–2016-2017
15
2.5. Structures de contrôle
Chapitre 2 : Introduction à Caml(light)
let egalite = fun
| (x,x) -> true
| _ -> false;;
Toplevel input:
>| (x,x) -> true
>
ˆ
The variable x is bound several times in this pattern.
let sommeBizarreBis x y = match (x,y) with
| (_,0) -> 0
| (0,_) -> 0
| (0,0) -> 0
| _ -> 0;;
Toplevel input:
>| (0,0) -> 0
>
ˆˆˆ
Warning: this matching case is unused.
sommeBizarreBis : int -> int -> int = <fun>
let nonHexaustif x y = match (x,y) with
| (1,2) -> true
| (0,2) -> false
| (_,4) -> true;;
Toplevel input:
>........................match (x,y) with
>| (1,2) -> true
>| (0,2) -> false
>| (_,4) -> true..
Warning: this matching is not exhaustive.
nonHexaustif : int -> int -> bool = <fun>
Caml 2.4.4 – Les messages d’alerte qui suivent les erreurs de filtrage classiques
2.5.1
Structure conditionnelle
La syntaxe de la structure conditionnelle est la suivante :
if condition then instruction1 else instruction 2;;
Le langage impose à instruction1 et instruction2 d’être de même type. En particulier en
cas d’absence du mot clef else on interprète l’absence de instruction2 comme une instruction
de type unit, dans ce cas instruction1 doit lui aussi être du type unit.
On peut utiliser les balises begin end pour délimiter les instructions instruction1 et instruction2,
comme le montre les exemples de Caml 2.5.1.
2.5.2
Boucle for
La boucle for utilise les deux balises do done, la variable de boucle est locale et il est possible de
décrémenter comme le montrent Caml 2.5.2 et 2.5.3
Il faut bien noter que les programmeurs Caml évitent ce style de programmation en utilisant
conjointement le filtrage et la récursivité. Nous verrons comment dans la section 2.6.
2.5.3
Les références
On s’attendrait à trouver après la description de la boucle for celle de la boucle while. Cependant,
l’usage de la boucle while suppose l’usage de la notion de référence.
© MPSI–Joly–Lycée Cézanne–2016-2017
16
Chapitre 2 : Introduction à Caml(light)
2.5. Structures de contrôle
(* Test conditionnel, exemple élémentaire *)
let test x = if x>0 then print_string "positif" else print_string "négatif";;
test : int -> unit = <fun>
test 2;;
positif- : unit = ()
(* Test conditionnel, usage de begin end *)
let testBE x =
if x>0 then
begin
print_string "positif";
2 * x
end
else
begin
print_string "négatif";
x-2
end;;
testBE : int -> int = <fun>
testBE 3;;
positif- : int = 6
testBE (-2);;
négatif- : int = -4
Caml 2.5.1 – Quelques exemples de tests conditionnels
for i = 1 to 3 do
print_int i ;
print_newline();
done;;
1
2
3
- : unit = ()
(* La variable i est locale, son appel conduit à une erreur *)
i;;
Toplevel input:
>i;;
>ˆ
The value identifier i is unbound.
Caml 2.5.2 – Une boucle for élémentaire
Remarque.
On verra que l’on peut parfaitement se passer de la notion de référence en Caml . On considère
même parfois que son usage est une preuve de mauvaise programmation. Certains concours imposent même de ne pas utiliser cet outil.
En Python il y a une ambiguïté entre l’identificateur d’une variable et sa valeur. Cela permet
des artifices de programmation intéressant :
(Python)
© MPSI–Joly–Lycée Cézanne–2016-2017
a=a+1
17
(2.1)
2.5. Structures de contrôle
Chapitre 2 : Introduction à Caml(light)
for i = 3 downto 1 do
print_int i ;
print_newline();
done;;
3
2
1
- : unit = ()
Caml 2.5.3 – On peut décrémenter dans une boucle for...
Dans l’expression 2.1, le a à gauche du signe = est un identificateur, a à droite du signe = désigne
une valeur.
Caml interdit cette ambiguïté mais aussi les facilités de programmation telles que celles évoquées
en 2.1. La syntaxe est la suivante :
let identificateur = ref valeur
On accède alors à la valeur de identificateur par !identificateur, le signe := permet
d’affecter une nouvelle valeur :
let coursTop = ref "Physique";;
coursTop : string ref = ref "Physique"
!coursTop;;
- : string = "Physique"
coursTop := !coursTop ^ "-Chimie";;
- : unit = ()
!coursTop;;
- : string = "Physique-Chimie"
Caml 2.5.4 – Utilisation de référence
Les références sont donc parfaitement adaptées pour construire des compteurs sur les entiers.
Notez les mots clefs qui permettent d’incrémenter un compteur. Caml 2.5.5
Remarque.
Il faut noter que dans Caml 2.5.5, à la ligne 11, l’utilisation du mot clef incr se fait sur l’identificateur de la référence.
On peut efficacement utiliser une référence dans une fonction. L’exemple suivant montre comment calculer les puissances entières d’un nombre entier : Caml 2.5.6
On peut aussi retrouver un programme très proche de celui que l’on pourrait proposer en
Python pour calculer les termes de la suite de Fibonacci Caml 2.5.7.
Remarque.
Le code Caml 2.5.7 propose une programmation dans le style impératif de la suite de Fibonacci.
Le programme est en effet proche du fonctionnement de l’ordinateur dans la mesure où on spécifie
dans l’ordre une série d’instructions que l’ordinateur doit effectuer. Il faut tout de même garder à
l’esprit que Caml n’est pas un langage adapté au style impératif (c’est un langage fonctionnel). Il
n’est donc pas étonnant que le programme proposé en Caml 2.5.7 soit assez lourd.
© MPSI–Joly–Lycée Cézanne–2016-2017
18
Chapitre 2 : Introduction à Caml(light)
2.5. Structures de contrôle
let compteur = ref 0;; (* déclaration et initialisation référence compteur *)
compteur : int ref = ref 0
compteur := !compteur + 1;; (* syntaxe simple *)
- : unit = ()
!compteur;;
- : int = 1
compteur := succ !compteur;; (* utilisation du mot clef succ *)
- : unit = ()
!compteur;;
- : int = 2
incr compteur;; (* utilisation du mot clef incr *)
- : unit = ()
!compteur;;
- : int = 3
Caml 2.5.5 – Définition d’un compteur
let puissance x n =
let resultat = ref 1 in (* on définit une référence initialisée à 1 *)
for k = 1 to n do
resultat := x * !resultat
done;
!resultat;;
puissance : int -> int -> int = <fun>
puissance 2 3;;
- : int = 8
Caml 2.5.6 – Définition d’une fonction puissance à l’aide d’une référence
let fibonacci n =
let u = ref 1 and v = ref 1 and a = ref 0 in
for i = 1 to n do
a := !u + !v;
u := !v;
v := !a;
done;
!u;;
fibonacci : int -> int = <fun>
fibonacci 5;;
- : int = 8
fibonacci 10;;
- : int = 89
Caml 2.5.7 – La suite de Fibonacci dans le style Python
Comme pour les boucles, nous verrons dans la section 2.6 les bonnes pratiques en Caml .
2.5.4
Boucle while
La syntaxe de la boucle while est la suivante :
© MPSI–Joly–Lycée Cézanne–2016-2017
19
2.6. Filtrage et récursivité
Chapitre 2 : Introduction à Caml(light)
while condition do instructions done;;
On cherche par exemple à calculer la plus petite puissance de 2 qui soit supérieure ou égale au
nombre entier x : Caml 2.5.8.
let petitexpo x =
let n = ref 0 and test = ref 1 in (* n et test sont des références *)
while !test < x do (* on compare la valeur de test à x *)
test := 2 * !test;
incr n;
done;
!n;; (* on renvoie la valeur de n *)
petitexpo : int -> int = <fun>
petitexpo 1025;;
- : int = 11
Caml 2.5.8 – Un exemple élémentaire de boucle while
Remarque.
Il est à noter que la boucle while est très peu utilisée en Caml , elle ne correspond pas au style
impératif que l’on a déjà évoqué. Dans certains concours, comme pour les références, on précise
même que l’on souhaite des programmes sans boucle while.
2.6
2.6.1
Filtrage et récursivité
Principe général de l’utilisation conjointe du filtrage et de la récursivité
On reviendra sur la notion de récursivité. Le but de cette section est d’introduire quelques idées
sur la réalisation pratique de la récursivité en Caml .
L’instruction clef est let rec.
L’exemple le plus classique est celui de la fonction factorielle : Caml 2.6.1
let rec fact n = match n with
| 0 -> 1
| n -> n * fact (n-1) ;; (* le parenthésage est indispensable *)
fact : int -> int = <fun>
Caml 2.6.1 – La fonction factorielle définie de façon récursive
Tout aussi classique est l’exemple de la suite de Fibonacci : Caml 2.6.2
Il est également possible de programmer les suites récurrentes arithmético-géométriques qui
def
def
sont définies par une récurrence croisée, a0 = α, b0 = β et :
∀n ∈ N,
def
an+1 =
an + bn
,
2
def
bn+1 =
p
an · bn
L’idée consiste à utiliser le mot clef and, le calcul est ensuite réalisé en fixant des valeurs à α
et β : Caml 2.6.3
© MPSI–Joly–Lycée Cézanne–2016-2017
20
Chapitre 2 : Introduction à Caml(light)
2.6. Filtrage et récursivité
let rec fibo n = match n with
| 0 -> 1
| 1 -> 1
| n -> fibo (n-1) + fibo (n-2);;
fibo : int -> int = <fun>
fibo 10;;
- : int = 89
Caml 2.6.2 – La suite de Fibonacci définie de façon récursive
let
|
|
and
|
|
a :
b :
a
b
-
rec a alpha beta n = match n with
0 -> alpha
n -> ((a alpha beta (n-1)) +. (b alpha beta (n-1)))/. 2.
b alpha beta n = match n with
0 -> beta
n -> sqrt((a alpha beta (n-1)) *. (b alpha beta (n-1)));;
float -> float -> int -> float = <fun>
float -> float -> int -> float = <fun>
14. 2000.
: float =
14. 2000.
: float =
5;;
494.878890974
5;;
494.878890911
Caml 2.6.3 – Définition de deux suites par récurrence croisée
Remarque.
La récursivité ne règle pas tout. En particulier, elle peut masquer des soucis de complexité qui
conduisent parfois à des programmes d’une très grande lenteur.
La section 2.7 aborde ces questions à travers la notion de récursivité terminale.
2.6.2
Remplacement des boucles
L’usage conjoint du filtrage et de la récursivité permet d’éviter l’usage de boucles. Le code 2.6.4
est celui d’une fonction qui affiche les nombres entiers de 1 à n.
let affiche n =
let rec loop k =
match k with
| k when k=n -> print_int n
| _ -> print_int k; print_newline(); loop (k+1)
in loop 1;;
Caml 2.6.4 – Remplacement d’une boucle
Dans 2.6.4 on voit comment faire porter la récursivité par une fonction auxiliaire (la fonction
loop), on voit aussi l’usage du filtrage gardé par la condition when. On comprend aussi comment
adapter cette technique au remplacement des boucles while.
© MPSI–Joly–Lycée Cézanne–2016-2017
21
2.7. Récursivité terminale
2.6.3
Chapitre 2 : Introduction à Caml(light)
Remplacement des références
On se propose de réécrire dans 2.6.5, le code 2.5.8 sans faire appel à des références ni à des boucles.
let petitexpoRefBouclefree x =
let rec aux n test =
if test < x then aux (n+1) (2*test) else n
in aux 0 1;;
Caml 2.6.5 – La fonction petitexpo sans boucle while ni référence
Dans 2.6.5, on a une lecture très simple du code. La récursivité est embarquée dans la fonction
auxiliaire aux qui traite les variables test et n qui apparaissaient dans le programme en version
impérative 2.5.8. Cette technique est classique en Caml . On peut aussi noter que le test conditionnel
aurait peu être remplacé par un filtrage.
2.7
Récursivité terminale
On parle de récursivité terminale pour une fonction définie par un code récursif, lorsque l’appel
récursif est la dernière instruction à être évaluée. La valeur renvoyée est donc celle calculée à partir
du dernier appel à la fonction sans qu’il y ait la moindre opération sur cette valeur.
Plus clairement, le code d’une fonction récursive terminale se présente en pseudo-code de la
façon suivante :
foncRecursiveTerm(a,b,n){
//....
renvoie foncRecursiveTerm(aa,bb,n-1)
}
Le code d’une fonction qui n’est pas récursive terminale se présente de la façon suivante :
h : fonction dont un des paramètre est du type du
résultat cherché;
foncNonRecursiveTerm(a,b,n){
//....
renvoie h(c,d,foncNonRecursiveTerm(aa,bb,n-1))
}
Voilà pour les idées, dans la pratique les choses peuvent se montrer un peu plus difficiles.
à ce stade du cours, nous nous contenterons de deux exemples.
Commençons par étudier la version non terminale Caml 2.7.1, de la fonction qui à f , n et x
associe f n (x), où f n désigne la composée f ◦ f ◦ · · · ◦ f , n fois.
Remarque.
On voit dans Caml 2.7.1 que les appels récursifs conduisent à un dépassement de mémoire. En effet,
dans cette version, le calcule de compose f 4 x revient à :
f(f(f(f(compose f 0 x)))
Pour n = 500000 l’encombrement en mémoire devient conséquent.
La version terminale de cette fonction peut être donnée par Caml 2.7.2
© MPSI–Joly–Lycée Cézanne–2016-2017
22
Chapitre 2 : Introduction à Caml(light)
2.7. Récursivité terminale
let rec compose f n x = match n with
| 0 -> x
| n -> f(compose f (n-1) x);;
compose : ('a -> 'a) -> int -> 'a -> 'a = <fun>
compose sin 5000 9.;;
- : float = 0.0244458924507
compose sin 500000 9.;;
Uncaught exception: Out_of_memory
Caml 2.7.1 – Un exemple de récursivité non-terminale qui conduit à un dépassement de mémoire
let rec composeTerm f n x = match n with
| 0 -> x
| n -> composeTerm f (n-1) (f x);;
composeTerm : ('a -> 'a) -> int -> 'a -> 'a = <fun>
composeTerm sin 5000 9.;;
- : float = 0.0244458924507
composeTerm sin 500000 9.;;
- : float = 0.00244943382979
Caml 2.7.2 – Une version récursive terminale
Remarque.
Ici le dernier calcul de Caml 2.7.2, va à son terme. En effet, dans cette version, le calcul de
compose f 4 x revient à évaluer d’abord
composeTerm f 3 (f x)
Dans cette première expression (f x) est calculé, en notant y sa valeur, on est conduit à calculer :
composeTerm f 2 y
Ainsi ne garde-t-on pas les calculs intermédiaires en mémoire.
Pour n = 500000, le calcule ne conduit pas à un dépassement de mémoire.
Le deuxième exemple met en lumière une astuce courante qui consiste à introduire un accumulateur dans le code. Cet accumulateur est destiné à recevoir les résultats des calculs intermédiaires.
n
X
On commence par une version naïve et non terminale de la fonction qui à n associe
k : Caml 2.7.3
k=1
Remarque.
On comprend que le mécanisme de dépassement de mémoire est le même que dans Caml 2.7.1.
En revanche le remède sera plus subtile. Il consiste à introduire une fonction auxiliaire et un accumulateur qui garde en mémoire le résultat intermédiaire : Caml 2.7.4
Remarque.
En toute rigueur ce n’est pas la fonction sommeTerm qui est récursive terminale mais la fonction aux.
© MPSI–Joly–Lycée Cézanne–2016-2017
23
2.8. Tableaux
Chapitre 2 : Introduction à Caml(light)
let rec somme n = match n with
| 0 -> 0
| n -> n + somme (n-1);;
somme : int -> int = <fun>
somme 5;;
- : int = 15
somme 500000;;
Uncaught exception: Out_of_memory
Caml 2.7.3 – Version récursive non-terminale de n 7→
Pn
k=1
k
let sommeTerm n =
let rec aux n acc = match n with
| 0 -> acc
| n -> aux (n-1) (n + acc)
in aux n 0;;
sommeTerm : int -> int = <fun>
sommeTerm 5;;
- : int = 15
sommeTerm 500000;;
- : int = 446198416
Caml 2.7.4
Exercice 2.7.1
Donner des versions impératives, récursives terminales, de la fonction factorielle, de la
fonction puissance de la page 19 ainsi que de la suite de Fibonacci.
2.8
Tableaux
Cette section propose une brève introduction au type vect de Caml qui correspond à la notion de
tableau. Ce type se rapproche du type list de Python.
Les exemples de Caml 2.8.1, 2.8.2 et 2.8.3, permettent de comprendre les opérations essentielles sur
les tableaux.
Remarques.
• Il faut noter que les éléments d’un tableau sont tous de même type.
• Si le type vect est mutable, en revanche la taille d’un tableau est fixée.
• Il est possible de construire des tableaux de tableaux. Ce qui constitue en particulier un
moyen de manipuler des matrices.
On peut noter que le type string fonctionne un peu comme un tableau de caractères. Mais les
opérations sont un peu différentes Caml 2.8.4.
Il faut retenir que dans un tableau ou une chaîne, l’accès à un élément se fait en un temps
constant.
© MPSI–Joly–Lycée Cézanne–2016-2017
24
Chapitre 2 : Introduction à Caml(light)
2.9. Listes
(* construction artisanale d'un tableau *)
let tableau1 = [|1;2;3;4|];;
tableau1 : int vect = [|1; 2; 3; 4|]
(* utilisation de la fonction make_vect *)
let tableau2 = make_vect 7 "ab";;
tableau2 : string vect = [|"ab"; "ab"; "ab"; "ab"; "ab"; "ab"; "ab"|]
(* utilisation de la fonction int_vect pour construire un tableau de int *)
let f x = 2*x;;
f : int -> int = <fun>
init_vect 5 f;;
- : int vect = [|0; 2; 4; 6; 8|]
Caml 2.8.1 – Créations de tableaux
(* longueur d'un tableau *)
vect_length tableau2;;
- : int = 7
(* copie physique d'un tableau *)
let tableau3 = copy_vect tableau2;;
tableau3 : string vect = [|"ab"; "ab"; "ab"; "ab"; "ab"; "ab"; "ab"|]
Caml 2.8.2 – Longueur d’un tableau, copie physique
(* accès aux éléments d'un tableau *)
tableau1.(0);;
- : int = 1
tableau1.(1);;
- : int = 2
(* modification d'un élément *)
tableau2.(3) <- "zx";;
- : unit = ()
tableau2;;
- : string vect = [|"ab"; "ab"; "ab"; "zx"; "ab"; "ab"; "ab"|]
(* le tableau3 n'est pas modifié *)
tableau3;;
- : string vect = [|"ab"; "ab"; "ab"; "ab"; "ab"; "ab"; "ab"|]
Caml 2.8.3 – Accès et modification après copie physique
2.9
Listes
Ici encore il s’agit de proposer une brève introduction au type list. L’exemple qui suit permet de
comprendre les opérations essentielles sur ce type.
Remarque.
On reviendra sur le type list dans la section consacrée aux structures de données.
© MPSI–Joly–Lycée Cézanne–2016-2017
25
2.9. Listes
Chapitre 2 : Introduction à Caml(light)
let langage = "Python";;
langage : string = "Python"
string_length langage;;
- : int = 6
(* l'accès aux éléments de fait avec [] *)
langage.[1];;
- : char = `y`
langage.[0] <- `C`;;
- : unit = ()
langage;;
- : string = "Cython"
Caml 2.8.4 – Les chaînes de caractères, des tableaux un peu particuliers
Beaucoup d’applications pour manipuler les listes existent déjà dans le langage. Il ne s’agit
pas de toutes les connaître dans la mesure où ce cours n’est pas un cours portant sur Caml . En
revanche, nous aurons besoin de l’une de ces fonctions dans la suite à la section 3.3.1, la fonction
miroir rev dont le fonctionnement se comprend bien dans l’exemple Caml 2.9.2.
Le code de la fonction compte, Caml 2.9.3, donne un exemple typique de filtrage sur les listes.
On peut proposer un code similaire, Caml 2.9.4, dans lequel on retrouve le mot clef when.
Exercice 2.9.1
La version compteb, Caml 2.9.4, propose une solution récursive qui n’est pas terminale.
À l’aide d’un accumulateur, donner un code récursif terminal de la même fonction.
© MPSI–Joly–Lycée Cézanne–2016-2017
26
Chapitre 2 : Introduction à Caml(light)
2.9. Listes
(* les éléments d'une liste sont séparés par ; *)
let liste1 = [1;2;3;4;5];;
liste1 : int list = [1; 2; 3; 4; 5]
let liste2 = ["ab";"Python";"Caml"];;
liste2 : string list = ["ab"; "Python"; "Caml"]
let liste3 = [exp;sin;cos;tan;log];;
liste3 : (float -> float) list = [<fun>; <fun>; <fun>; <fun>; <fun>]
(* on peut ajouter un élément en tête de liste par :: *)
let liste1 = 0::liste1;;
liste1 : int list = [0; 1; 2; 3; 4; 5]
(* on peut accéder au premier élément d'une liste par hd ( head ) *)
let tete = hd liste2;;
tete : string = "ab"
(* on peut accéder à la queue d'une liste par tl ( rail ) *)
let queue = tl liste2;;
queue : string list = ["Python"; "Caml"]
(* longueur d'une liste *)
list_length liste1;;
- : int = 6
(* On peut concaténer les listes *)
let liste1 = [1;2;3];;
liste1 : int list = [1; 2; 3]
let liste2 = [4;5;6;7];;
liste2 : int list = [4; 5; 6; 7]
let liste3 = liste1 @ liste2;;
liste3 : int list = [1; 2; 3; 4; 5; 6; 7]
Caml 2.9.1 – Quelques exemples élémentaires sur les listes
let liste = [1;2;3];;
liste : int list = [1; 2; 3]
(* la liste miroir de liste *)
let listeMiroir = rev liste1;;
listeMiroir : int list = [3; 2; 1]
Caml 2.9.2 – La fonction rev, miroir d’une liste
© MPSI–Joly–Lycée Cézanne–2016-2017
27
2.9. Listes
let rec compte
| [] -> 0
| t::q -> if
else compte
compte : 'a ->
Chapitre 2 : Introduction à Caml(light)
a liste = match liste with
t=a then 1 + (compte a q)
a q;;
'a list -> int = <fun>
compte 1 [1;2;5;1;5;1;8];;
- : int = 3
Caml 2.9.3 – Une fonction qui compte le nombre d’occurrences d’un élément dans une liste
let
|
|
|
rec compteb a liste = match liste with
[] -> 0
t::q when t=a -> 1 + (compteb a q)
t::q -> compteb a q;;
Caml 2.9.4 – Une fonction qui compte le nombre d’occurrences d’un élément dans une liste, utilisation du mot clef when
© MPSI–Joly–Lycée Cézanne–2016-2017
28
Chapitre 3
Structures de données
“
I will, in fact, claim that the difference between a bad programmer and a good
one is whether he considers his code or his data structures more important.
Bad programmers worry about the code. Good programmers worry about data
structures and their relationships.
”
Linus Torvalds,
3.1
À propos des structures de données
Comme on l’a déjà aperçu au chapitre 1, une structure de données est la réalisation pratique d’un
type abstrait comme le type générique LISTE(α).
De manière générale, une structure de données est l’association d’un (ou plusieurs) types,
servant à contenir des données (ce qui explique la terminologie) et de fonctions ou d’actions qui
servent à manipuler les données typées.
L’ensemble de ces actions constituent l’interface de la structure de données. Les fonctions de
l’interface sont regroupées en trois catégories :
• Les constructeurs modifient l’état de la structure.
• Les sélecteurs renseignent sur l’état de la structure en renvoyant un résultat qui n’est pas un
booléen.
• Les prédicats renseignent sur l’état de la structure en renvoyant un booléen.
Il existe un autre catégorie particulière un peu à part, les itérateurs qui parcourent la structure
dans sa globalité.
En Caml l’interface des structure de données est définie dans un fichier d’extension .mli.
L’ensemble des profils (ou signatures) des fonctions de l’interface, s’appelle la signature de la
structure de données.
La spécification abstraite de la structure de données correspondant aux tableaux (le type vect
en Caml ), a une interface décrite dans le fichier vect.mli dont on donne une idée très partielle
dans Caml 3.1.1.
type 'a vect
(* tableau d'éléments de type 'a *)
value make_vect : int -> 'a -> 'a t
(* création : constructeur *)
value vect_length : 'a t -> int
(* longueur : sélecteur *)
Caml 3.1.1 – Constructeur de tableau, exemple
29
3.2. Piles
Chapitre 3 : Structures de données
Pour une même spécification, plusieurs implémentations concrètes peuvent exister. En toute
rigueur, une structure de données est justement cette réalisation concrète. Mais dans le langage
courant, l’expression prend parfois le sens de type générique abstrait. On peut lever l’ambiguïté
en parlant de structure de données abstraite. On peut noter qu’en matière d’algorithmes il est
possible de faire la même distinction entre les spécifications abstraites qui définissent ce que fait
l’algorithme et sa réalisation pratique.
En Python, nous avons déjà vu plusieurs exemples de structures de données simples : tableaux
et listes, qui servent à stocker une collection d’éléments dans un ordre précis, à obtenir un élément
de la collection par son indice, à ajouter un élément.
On distingue deux grandes familles de structures de données : les structures impératives et les
structures persistantes. Les premières fournissent des opérations qui modifient en place la structure.
Par exemple, l’opération a.(i) <- x qui assigne la valeur x à la i-ième case d’un tableau,
modifie le tableau en place et l’ancienne valeur est perdue.
Dans le cas des structures de données persistantes, en revanche, chaque structure est dite
immutable (immuable en bon français), on ne peut pas la modifier. Les opérations de l’interface
renvoient de nouvelles structures sans modifier en place la structure, les versions antérieures sont
donc préservées.
Cette distinction est généralisable : Wikipédia, par exemple, fournit un historique des modifications, ce qui permet de revenir en arrière, pour accéder à une ancienne version qui n’est donc
pas perdue. De même, un livre de comptes, un bulletin scolaire, ne sont-ils pas effacées et réécrits
mais minutieusement archivés !
3.2
Piles
La pile est un type omniprésent en informatique. De façon intuitive les piles sont imaginées comme
un empilement vertical d’objets associé à trois opérations :
• empiler : ajout au sommet d’un élément.
• dépiler : accès à la pile privée de son sommet.
• sommet : sélection du sommet de pile.
Remarques.
• Une pile apparaît donc en particulier comme une interprétation particulière d’une liste pour
laquelle la tête de liste serait le sommet. En Caml les trois opérations décrites précédemment
correspondent donc respectivement à l’opérateur ::, aux fonctions tl et hd.
• On accède au dernier élément empilé, le “dernier arrivé est le premier traité”. En anglais on
parle de structure LIFO (last in first out).
3.2.1
Spécification
La structure de pile est inductive, on part de la pile vide pour construire toutes les autres piles.
La signature du type pile est la suivante :
• Deux constructeurs :
∗ pile_vide : unit -> pile, qui crée une pile vide
∗ empile : élément × pile -> pile, qui empile un objet de type élément au sommet
de la pile prise en argument.
• Un prédicat
∗ est_vide : pile -> bool, qui teste si une pile est vide.
• Deux opérations de sélection :
∗ sommet : pile -> élément, qui retourne l’objet de type élément qui se trouve au
sommet de la pile prise en argument (si la pile n’est pas vide).
© MPSI–Joly–Lycée Cézanne–2016-2017
30
Chapitre 3 : Structures de données
3.2. Piles
∗ dépile : pile -> pile, qui renvoie la pile prise en argument privée de son sommet
(si la pile n’est pas vide).
En désignant par e un élément et par p une pile d’élément de même type que e, on a :
• est_vide pile_vide = vrai
• est_vide (empile e p) = faux
• sommet pile_vide = erreur
• sommet (empile e p) = e
• depile pile_vide = erreur
• depile (empile e p) = p
3.2.2
Implémentations
Caml propose sa propre implémentation de la structure de pile. L’interface proposée par le fichier
stack.mli est suffisamment simple et courte pour que l’on puisse la reproduire dans ce cours dans
le code 3.2.1.
(* Stacks *)
(* This module implements stacks ( LIFOs ) , with in-place modification. *)
type 'a t;;
(* The type of stacks containing elements of type ['a]. *)
exception Empty;;
(* Raised when [pop] is applied to an empty stack. *)
value new: unit -> 'a t
(* Return a new stack, initially empty. *)
and push: 'a -> 'a t -> unit
(* [push x s] adds the element [x] at the top of stack [s]. *)
and pop: 'a t -> 'a
(* [pop s] removes and returns the topmost element in stack [s],
or raises [Empty] if the stack is empty. *)
and clear : 'a t -> unit
(* Discard all elements from a stack. *)
and length: 'a t -> int
(* Return the number of elements in a stack. *)
and iter: ('a -> unit) -> 'a t -> unit
(* [iter f s] applies [f] in turn to all elements of [s], from the
element at the top of the stack to the element at the
bottom of the stack. The stack itself is unchanged. *)
;;
Caml 3.2.1 – Implémentation de la structure de pile native
Dans l’implémentation native 3.2.1, on voit que toutes les fonctions auxquelles on a pensé de
façon théoriques ne sont pas implémentées. On ne peut pas par exemple accéder au sommet de la
pile. On pourrait éventuellement ajouter cette fonction au fichier stack.mli. On propose dans la
suite des implémentations maison de la structure de pile.
On peut proposer plusieurs implémentations personnelles de la structure de pile.
© MPSI–Joly–Lycée Cézanne–2016-2017
31
3.2. Piles
Chapitre 3 : Structures de données
(* définition du type pile par homonymie *)
type 'a pile == 'a list;;
Type pile defined.
(* la fonction pile_vide est sans argument *)
let pile_vide () = ([] : 'a pile);;
pile_vide : unit -> 'a pile = <fun>
(* On force le typage des éléments *)
let empile e (p : 'a pile) = ((e::p) : 'a pile);;
empile : 'a -> 'a pile -> 'a pile = <fun>
(* test de pile vide *)
let est_vide (p : 'a pile) = match p with
| [] -> true
| s::q -> false;;
est_vide : 'a pile -> bool = <fun>
(* sommet de pile, on capture l'erreur par une exception *)
let sommet (p : 'a list) = match p with
| [] -> failwith "pile vide"
| s::q -> s;;
sommet : 'a list -> 'a = <fun>
(* queue de pile, on capture l'erreur par une exception *)
let depile (p : 'a list) = match p with
| [] -> failwith "pile vide"
| s::q -> q;;
depile : 'a list -> 'a list = <fun>
Caml 3.2.2 – Premier exemple d’implémentation de la structure de pile
Les types pile et list sont isomorphes
On s’appuie sur le fait que les deux types sont isomorphes pour implémenter l’interface du type
pile : Caml 3.2.2
Il faut noter que dans ce cas, les opérations depile et empile ne modifient pas en place la pile
passée en argument, il s’agit donc d’une version persistante de l’implémentation : Caml 3.2.3
Le type pile implémenté à l’aide d’un enregistrement
L’idée ici est de légèrement modifier le point de vue pour faire utiliser un enregistrement, les
choses sont vraiment les mêmes à une importante différence prés : dans cette implémentation les
opérations depile et empile agissent alors par effet de bord, elles modifient en effet en place la
pile passée en argument. Il s’agit donc d’une version impérative de l’implémentation : Caml 3.2.4
3.2.3
intérêt
Les piles se retrouvent dans les gestions d’historique des pages visitées dans un navigateur, la
sauvegarde incrémentale d’un disque, l’exécution récursive d’un programme.
Dans cette la section suivante on propose, conformément au programme, de réaliser l’évaluation
d’une expression arithmétique postfixée à l’aide d’une pile.
© MPSI–Joly–Lycée Cézanne–2016-2017
32
Chapitre 3 : Structures de données
3.2. Piles
let (pile1 : 'a pile) = ["tete";"q1";"q2";"q3"];;
pile1 : string pile = ["tete"; "q1"; "q2"; "q3"]
depile pile1;;
- : string list = ["q1"; "q2"; "q3"]
pile1;;
- : string pile = ["tete"; "q1"; "q2"; "q3"]
empile "head" pile1;;
- : string pile = ["head"; "tete"; "q1"; "q2"; "q3"]
pile1;;
- : string pile = ["tete"; "q1"; "q2"; "q3"]
Caml 3.2.3 – Implémentation sans effet de bord
Expression
1
Notation postfixe
1
1+2
1␣2␣+
1 + (2 × 3)
1␣2␣3␣ × ␣+
(2 + 3)/(3 − 4)
2␣3␣ + ␣3␣4␣ − ␣/
Table 3.1 – Correspondance avec la notation postfixée
3.2.4
Une application
On s’intéresse à l’ensemble des expressions arithmétiques. Il s’agit d’un ensemble inductif dans le
sens où il est défini d’abord par ses éléments de bases et des règles de construction.
L’ensemble des éléments de base est l’ensemble des nombres entiers relatifs. Les règles de
construction sont simples : si e1 et e2 sont deux expressions arithmétiques, alors pour tout # ∈ {+, −, ×, /}
(où / désigne la division entière sur les nombres entiers relatifs) l’expression e1 #e2 est une expression arithmétique.
Sans rentrer dans le détail de la théorie des ensembles inductifs, on comprend par exemple que
les expressions suivantes sont des expressions arithmétiques :
12 + 3, 12 + 3 × 5, −2 × 3 + 4/3, −2 × 3 + 4/3 × 8 + 4
On comprend que dans cette affaire le souci soit lié à l’évaluation des expressions. En effet, le
résultat de 12 + 3 × 5, par exemple, dépend de l’ordre de priorité des opérations. On trouve
(12 + 3) × 5 = 75 ou 12 + (3 × 5) = 27.
On peut bien évidemment ajouter les parenthèses à nos expressions arithmétiques. Mais il
existe un moyen plus simple de représenter les opérations qui est en plus parfaitement adapté à
l’évaluation de l’expression par une pile. C’est la notation postfixée 1 .
L’ensemble des expressions arithmétiques postfixées est lui aussi un ensemble inductif. Les
éléments de base sont les nombres entiers relatifs et les règles de construction sont simples : si e1
et e2 sont deux expressions arithmétiques, alors pour tout # ∈ {+, −, ×, /} (où / désigne encore la
division entière sur les nombres entiers relatifs) l’expression e1 e2 # est une expression arithmétique
postfixée.
Sans entrer dans les détails, toute expression arithmétique parenthésée peut s’exprimer par une
expression arithmétique postfixée. Le tableau 3.1, dans lequel on a rendu les espaces visibles, donne
quelques exemples.
1 on
parle aussi pour des raisons historiques de notation polonaise inverse
© MPSI–Joly–Lycée Cézanne–2016-2017
33
3.2. Piles
Chapitre 3 : Structures de données
type 'a pile = {mutable elements : 'a list};;
Type pile defined.
let pile_vide () = {elements = []};;
pile_vide : unit -> 'a pile = <fun>
let empile e p = p.elements <- e::p.elements;;
empile : 'a -> 'a pile -> unit = <fun>
let est_vide p = match p.elements with
| [] -> true
| _ -> false;;
est_vide : 'a pile -> bool = <fun>
(* depile renvoie la tête de pile, cela peut être pratique *)
let depile p = match p.elements with
| [] -> failwith "pile vide"
| s::q -> p.elements <- q; s;;
depile : 'a pile -> 'a = <fun>
let sommet p = match p.elements with
| [] -> failwith "pile vide"
| s::q -> s;;
sommet : 'a pile -> 'a = <fun>
let pile2 ={elements = ["tete";"q1";"q2";"q3"]};;
pile2 : string pile = {elements = ["tete"; "q1"; "q2"; "q3"]}
depile pile2;;
- : string = "tete"
pile2;;
- : string pile = {elements = ["q1"; "q2"; "q3"]}
empile "head" pile2;;
- : unit = ()
pile2;;
- : string pile = {elements = ["head"; "q1"; "q2"; "q3"]}
Caml 3.2.4 – Effets de bord pour depile et empile
Pour réaliser l’évaluation d’une expression arithmétique postfixée, il suffit de partir d’une pile
vide et de lire les différents éléments de l’expression de gauche à droite en effectuant l’opération
dés que l’on atteint un opérateur.
Appliquons ce principe au calcul de 4 + ((2 + 3) × (5 − 6)). En notation postfixe on obtient :
4␣2␣3␣ + ␣5␣6␣ − ␣ × ␣+
Le traitement du calcul par pile peut s’illustrer ainsi :
4
2
−→
3
→
4
2
4
→ 3
2
4
+
5
→
5
4
→ 5
5
4
6
6
→ 5
5
4
−
→ -1
5
4
Le pseudo-code de l’évaluation d’une expression e est le suivant :
© MPSI–Joly–Lycée Cézanne–2016-2017
34
×
+
→
→
-5
4
-1
Chapitre 3 : Structures de données
3.3. Files
TANT QUE e non terminée FAIRE
lire l'élément op de e suivant
SI op est un nombre alors
empiler op sur la pile
SINON (* op est un opérateur *)
x <- sommet de pile
depiler la pile
y<- sommet de pile
depiler la pile
empiler (y op x) sur la pile
FIN SI
FIN TANT QUE
renvoyer le sommet de la pile
Remarques.
• Nous réaliserons en pratique en TD cet algorithme.
• De façon générale, les piles sont systématiquement utilisées pour gérer les environnements
d’exécution des fonctions. Il faut retenir que les piles sont à la base du fonctionnement des
langages de programmation.
3.3
Files
Comme la pile, la file est une structure omniprésente en informatique. La file diffère de la pile par sa
gestion de la priorité, en effet l’élément “le plus ancien” est traité en premier, on parle de structure
de données FIFO (first in first out). C’est ainsi une file qui gère les caractères tapés au clavier,
l’envoi des fichiers vers une imprimante. La file d’attente est une bonne image de la structure de
file !
3.3.1
Spécification
La structure de file est également inductive, sa signature est la suivante :
• Deux constructeurs :
∗ file_vide : unit -> file, qui crée une file vide
∗ ajoute : élément × file -> file, qui ajoute un objet de type élément à la fin de
la file prise en argument
• Un prédicat :
∗ est_vide : file -> bool, qui teste si une file est vide
• Deux opérations de sélection :
∗ premier : file -> élément, qui renvoie le premier élément de la file prise en argument
(si cette file est non vide)
∗ queue : file -> file, qui renvoie la file passée en argument privée de son premier
élément (si cette file est non vide).
En désignant par e un éléments et par f une file d’élément de même type que e, on a :
• est_vide file_vide = vrai
• est_vide (ajoute e file) = faux
• premier file_vide = erreur
• premier (ajoute e f) = si (est_vide f) alors e sinon (premier f)
• queue file_vide = erreur
• queue (ajoute e f) = si (est_vide f) alors file_vide sinon (ajoute e (queue f))
© MPSI–Joly–Lycée Cézanne–2016-2017
35
3.3. Files
Chapitre 3 : Structures de données
Remarque.
On notera les définitions récursives des opérations premier et queue.
3.3.2
Implémentations
L’implémentation du type file est prévu par Caml dans la bibliothèque queue.
Comme pour la structure de pile, on donne dans le code 3.3.1 l’interface native prévue en Caml .
(* Queues *)
(* This module implements queues ( FIFOs ) , with in-place modification. *)
type 'a t;;
(* The type of queues containing elements of type ['a]. *)
exception Empty;;
(* Raised when [take] is applied to an empty queue. *)
value new: unit -> 'a t
(* Return a new queue, initially empty. *)
and add: 'a -> 'a t -> unit
(* [add x q] adds the element [x] at the end of the queue [q]. *)
and take: 'a t -> 'a
(* [take q] removes and returns the first element in queue [q],
or raises [Empty] if the queue is empty. *)
and peek: 'a t -> 'a
(* [peek q] returns the first element in queue [q], without removing
it from the queue, or raises [Empty] if the queue is empty. *)
and clear : 'a t -> unit
(* Discard all elements from a queue. *)
and length: 'a t -> int
(* Return the number of elements in a queue. *)
and iter: ('a -> unit) -> 'a t -> unit
(* [iter f q] applies [f] in turn to all elements of [q], from the
least recently entered to the most recently entered.
The queue itself is unchanged. *)
;;
Caml 3.3.1 – Implémentation de la structure de pile native
On voit que tout y est sauf le test de file vide. Comme pour la structure de pile, on se propose
d’implémenter la structure de file en utilisant les ressources de base du langage.
Implémentation à l’aide de deux listes
On peut penser au type list pour l’implémentation du type file, mais alors il faut choisir où
placer un nouvel élément :
• Si on le place en tête de liste la fonction ajoute se fait en temps constant. En revanche,
premier suppose alors le parcours de toute la liste.
• Si on place en fin de liste le nouvel élément, c’est alors la fonction ajoute qui doit parcourir
toute la liste.
Une astuce consiste à couper la file en deux et la représenter par deux listes. La tête de la
première liste est le début de la file, la tête de la seconde est la fin de la file :
© MPSI–Joly–Lycée Cézanne–2016-2017
36
Chapitre 3 : Structures de données
3.3. Files
Début
Fin
Début
Fin
On applique cette idée dans l’implémentation Caml 3.3.2
type 'a file={mutable debut :'a list ; mutable fin : 'a list};;
Type file defined.
let file_vide() = {debut=[];fin=[]};;
file_vide:unit->'afile=<fun>
let ajoute e f = f.fin <- e::f.fin ; f;;
ajoute : 'a -> 'a file -> 'a file = <fun>
let est_vide f = (f.debut=[]) && (f.fin=[]);;
est_vide : 'a file -> bool = <fun>
let premier f=
if (est_vide f) then failwith "premier"
else
begin
if (f.debut = []) then begin
f.debut <- rev f.fin ;
f.fin <- []
end;
hd f.debut
end;;
premier : 'a file -> 'a = <fun>
let queue f =
if (est_vide f ) then failwith "queue"
else begin
if (f.debut = []) then begin
f.debut <- rev f.fin ;
f.fin <- []
end;
f.debut <- tl f.debut ;
f
end;;
queue : 'a file -> 'a file = <fun>
Caml 3.3.2 – Typefile,implémentation
Remarque.
Les opérations premier et queue utilisent la fonction miroir rev afin de pouvoir accéder facilement
au premier élément de la file ou à la queue de file. Il faut bien comprendre que la même file peut
être représentée de différentes façons en répartissant les éléments qui la compose de différemment
dans les champs debut et fin. évidemment, cette astuce enferme dans une boite noire (la fonction
rev) une partie de l’implémentation. On ne connaît en effet pas très bien le coût de cette opération.
Caml 3.3.3 aide à comprendre le fonctionnement de l’implémentation.
© MPSI–Joly–Lycée Cézanne–2016-2017
37
3.3. Files
Chapitre 3 : Structures de données
(* Pour mieux comprendre *)
let coda = file_vide();;
coda : '_a file = debut = []; fin = []
let coda = ajoute "premier" coda;;
coda : string file = {debut = []; fin = ["premier"]}
let coda = ajoute "deuxieme" coda;;
coda : string file = {debut = []; fin = ["deuxieme"; "premier"]}
let coda = ajoute "troisieme" coda;;
coda : string file = {debut = []; fin = ["troisieme"; "deuxieme"; "premier"]}
let coda = ajoute "quatrieme" coda;;
coda : string file =
{debut = []; fin = ["quatrieme"; "troisieme"; "deuxieme"; "premier"]}
let first = premier coda;;
first : string = "premier"
coda;;
- : string file =
{debut = ["premier"; "deuxieme"; "troisieme"; "quatrieme"]; fin = []}
let fila = queue coda;;
fila : string file =
{debut = ["deuxieme"; "troisieme"; "quatrieme"]; fin = []}
Caml 3.3.3 – Type file, comprendre l’implémentation
Remarque.
Dans Caml 3.3.3, à la ligne 18 on applique la fonction premier à la file coda entièrement représentée dans le champ fin. Afin de pouvoir accéder facilement au premier élément, la fonction rev
retourne la file dans le champ debut afin d’accéder facilement au premier élément. À la sortie de la
la ligne 18, les éléments de la file coda sont entièrement représentés dans le champ debut, comme
on le constate à la ligne 23.
Implémentation à l’aide d’un tableau circulaire
L’idée consiste ici à représenter la liste dans un tableau. Les deux variables debut et fin pointent
respectivement sur l’indice du début de la file et sur l’indice qui suit immédiatement l’indice de fin
de file. Lorsque debut = fin c’est que la liste est vide.
On considère par exemple la liste a, b, c dont le premier élément est a et le dernier c.
Pour représenter cette liste on a besoin d’un tableau de longueur 3 au moins comme illustré sur la
figure 3.1
a
0
1
2
b
3
debut
c
4
5
6
fin
Figure 3.1 – Une liste représentée par un tableau
Ajouter un élément à la file revient à le placer dans le tableau au point de coordonnée fin et
à incrémenter le nombre entier fin.
© MPSI–Joly–Lycée Cézanne–2016-2017
38
Chapitre 3 : Structures de données
3.3. Files
Sélectionner la queue de la file revient à incrémenter debut.
a
debut
On peut représenter la même file de différentes façons (modulo la longueur du tableau) sur un
même tableau circulaire comme le montre la figure 3.2.
b
sens de la file
c
fin
Figure 3.2 – Une file de trois éléments représenté par un “vecteur circulaire”
La même file peut donc être représentée par différentes listes, comme le montre la figure 3.3.
a
0
1
b
3
2
c
4
debut
5
fin
c
b
0
a
1
2
3
4
5
fin
a
0
6
debut
c
b
2
1
6
3
debut
4
5
6
fin
Figure 3.3 – Représentation de la même file de trois éléments par différents tableaux
Il faut néanmoins penser à un souci lié au tableau saturé :
f
0
g
a
1
2
debut
b
3
c
4
d
5
e
6
fin
Le tableau est ambigu, représente-t-il la file vide ou la file (pleine) (a, b, c, d, e, f, g) ?
Pour lever l’ambiguïté on introduit un champ booléen vide dans la spécification de la structure.
Un autre souci est lié à la définition d’un tableau vide. Une solution consiste à définir un type
intermédiaire : VideOuNon : Caml 3.3.4
Pour mieux comprendre le fonctionnement de la nouvelle implémentation : Caml 3.3.5
© MPSI–Joly–Lycée Cézanne–2016-2017
39
3.3. Files
Chapitre 3 : Structures de données
type 'a videOuNon =
| Vide
| Elem of 'a;;
Type videOuNon defined.
let extract (Elem b) = b;;
Toplevel input:
>let extract (Elem b) = b;;
>
ˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆ
Warning: this matching is not exhaustive.
extract : 'a videOuNon -> 'a = <fun>
(* définition du type file *)
type 'a file = {contenu : 'a vect ; mutable debut : int ; mutable fin : int ;
mutable vide : bool ; taille : int};;
Type file defined.
let file_vide t = {contenu = make_vect t Vide ; debut = 0 ; fin = 0 ;
vide = true ; taille = t };;
file_vide : int -> 'a videOuNon file = <fun>
let ajoute e f =
if (f.fin = f.debut) && not(f.vide)
then failwith "file pleine"
else begin
f.contenu.(f.fin) <- (Elem e);
f.fin <- (succ f.fin) mod f.taille;
f.vide <- false;
f
end;;
ajoute : 'a -> 'a videOuNon file -> 'a videOuNon file = <fun>
let est_vide f = f.vide;;
est_vide : 'a file -> bool = <fun>
let premier f =
if (f.vide) then failwith "file vide"
else extract f.contenu.(f.debut);;
premier : 'a videOuNon file -> 'a = <fun>
let queue f =
if (f.vide) then failwith "file vide"
else begin
f.debut <- (succ f.debut) mod f.taille;
if (f.debut = f.fin) then f.vide <- true;
f
end;;
queue : 'a file -> 'a file = <fun>
Caml 3.3.4 – Type file, représentation par un tableau
Remarque.
On remarquera que les deux implémentations proposées conduisent à des structures de données
impératives.
© MPSI–Joly–Lycée Cézanne–2016-2017
40
Chapitre 3 : Structures de données
3.3. Files
let coda = file_vide 5;;
coda : '_a videOuNon file =
{contenu = [|Vide; Vide; Vide; Vide; Vide|]; debut = 0; fin = 0;vide = true;
taille = 5}
let coda = ajoute "premier" coda;;
coda : string videOuNon file =
{contenu = [|Elem "premier"; Vide; Vide; Vide; Vide|]; debut = 0; fin = 1;
vide = false; taille = 5}
let coda = ajoute "deuxieme" coda;;
coda : string videOuNon file =
{contenu = [|Elem "premier"; Elem "deuxieme"; Vide; Vide; Vide|]; debut = 0;
fin = 2; vide = false; taille = 5}
let coda = ajoute "troisieme" coda;;
coda : string videOuNon file =
{contenu = [|Elem "premier"; Elem "deuxieme"; Elem "troisieme"; Vide; Vide|];
debut = 0; fin = 3; vide = false; taille = 5}
let coda = ajoute "quatrieme" coda;;
coda : string videOuNon file =
{contenu =
[|Elem "premier"; Elem "deuxieme"; Elem "troisieme"; Elem "quatrieme"; Vide|];
debut = 0; fin = 4; vide = false; taille = 5}
let first = premier coda;;
first : string = "premier"
coda;;
- : string videOuNon file =
{contenu =
[|Elem "premier"; Elem "deuxieme"; Elem "troisieme"; Elem "quatrieme"; Vide|];
debut = 0; fin = 4; vide = false; taille = 5}
let fila = queue coda;;
fila : string videOuNon file =
{contenu =
[|Elem "premier"; Elem "deuxieme"; Elem "troisieme"; Elem "quatrieme"; Vide|];
debut = 1; fin = 4; vide = false; taille = 5}
coda;;
- : string videOuNon file =
{contenu =
[|Elem "premier"; Elem "deuxieme"; Elem "troisieme"; Elem "quatrieme";Vide|];
debut = 1; fin = 4; vide = false; taille = 5}
Caml 3.3.5 – Type file, représentation par un tableau, exemple d’utilisation
3.3.3
Files de priorité
Les files de priorités sont une variante du type file. À chacun des objets de la file est associé
une priorité de traitement. La séquence des objets est maintenue triée : on supprime l’élément
de priorité maximale, on insère un nouvel objet suivant sa priorité (après les objets de priorités
supérieures, mais avant les objets de priorités strictement inférieures).
Nous verrons en exercices différentes implémentations et utilisations possibles.
© MPSI–Joly–Lycée Cézanne–2016-2017
41
3.4. Dictionnaires
3.4
Chapitre 3 : Structures de données
Dictionnaires
Le cours d’informatique pour tous nous a permis de découvrir le type dictionnaire et l’intérêt
de son utilisation. Nous ne développerons donc pas ici. Notons du moins que l’on parle aussi, à la
place de dictionnaire, de tableau associatif. On impose toutefois parfois à l’ensemble K des clefs
d’être totalement ordonné. Par ailleurs, la relation qui associe l’ensemble des clefs K et l’ensemble
des valeurs V est fonctionnelle : à une clef ne correspond qu’une seule valeur.
3.4.1
Spécification
La signature de la structure de données dict est la suivante :
• Trois constructeurs :
∗ dict_vide : unit -> dict, qui crée un dictionnaire vide
∗ inserer : clé × élément × dict -> dict, qui ajoute un objet de type élément
avec la clef de type clé au dictionnaire pris en argument
∗ supprimer : clé × dict -> dict, qui supprime l’objet de type élément ayant la
clef de type clé passée en argument, du dictionnaire passé en argument (si l’association
existe dans le dictionnaire passé en argument)
• Une opération de sélection :
∗ rechercher : clé × dict -> élément, qui renvoie l’élément associé à la clef passée
en argument dans le dictionnaire passé en argument (si l’association existe dans le
dictionnaire passé en argument)
En désignant par v und valeur, k une clef, et par d un dictionnaire dont les valeurs et les clefs
sont respectivement de même types que v et k, on a :
• rechercher k creer = erreur
• rechercher k (inserer k v d) = v
• if k 6= k0 then rechercher k’ (inserer k v d) = rechercher ké d
• rechercher k (supprimer k d) = erreur
• if k 6= k0 then rechercher k’ (supprimer k d) = rechercher k’ d
3.4.2
Implémentations
On peut réaliser l’implémentation de la structure dict à l’aide d’un tableau de taille N (“grande”)
fixée. Cela ne correspond pas à l’idée qu’un dictionnaire n’a pas de taille fixée, mais en pratique,
l’utilisateur est tout de même limité par la taille de la mémoire ! On impose alors aux clefs d’appartenir à l’ensemble J0, N − 1K.
Un autre souci consiste à initialiser un tableau vide. On utilise la même astuce que dans la
section 3.3.2 dans laquelle on spécifie le type liste à l’aide de tableaux : Caml 3.4.1
On montre le fonctionnement de l’implémentation dans Caml 3.4.2. Il apparaît que la structure
définie est impérative : Caml 3.4.2
3.5
Arbres
On répétera encore que la structure d’arbre est essentielle en informatique. Donald Knuth, lui
même, dans son oeuvre de référence, The art of computer programming, évoque the most fundamental structure in computer science. Toutes les notions de ce cours apparaissent donc comme
fondamentales !
© MPSI–Joly–Lycée Cézanne–2016-2017
42
Chapitre 3 : Structures de données
3.5. Arbres
(* Définition du vide par un type polymorphe *)
type 'a videOuNon =
| Vide
| Elem of 'a;;
*Type videOuNon defined.
Elem "aa";;
- : string videOuNon = Elem "aa"
(* Une fonction utile pour la suite *)
let extract (Elem b) = b;;
Toplevel input:
>let extract (Elem b) = b;;
>
ˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆ
Warning: this matching is not exhaustive.
extract : 'a videOuNon -> 'a = <fun>
extract (Elem 5);;
- : int = 5
(* Définition du type dictionnaire *)
type 'a dict = {contenu : 'a videOuNon vect; taille : int};;
Type dict defined.
(* Dictionnaire vide *)
let dict_vide t = {contenu = make_vect t Vide ; taille = t};;
dict_vide : int -> 'a dict = <fun>
(* La fonction inserer *)
let inserer clef element dico =
if clef >= dico.taille-1 then failwith "depassement"
else dico.contenu.(clef) <- (Elem element);;
inserer : int -> 'a -> 'a dict -> unit = <fun>
(* La fonction supprimer *)
let supprimer clef dico =
if clef >= dico.taille-1 then failwith "depassement"
else dico.contenu.(clef) <- Vide;;
supprimer : int -> 'a dict -> unit = <fun>
(* La fonction rechercher *)
let rechercher clef dico =
let el = dico.contenu.(clef) in
if clef >= (dico.taille-1) then failwith "depassement"
else
if (el = Vide) then failwith "element absent"
else extract dico.contenu.(clef);;
rechercher : int -> 'a dict -> 'a = <fun>
Caml 3.4.1 – Type dictionnaire, implémentation
3.5.1
Aspects informels
On rencontre des arbres dans de nombreux domaines, en informatique, en mathématiques, mais
aussi dans la vie courante. La figure 3.4 donne quelques exemples.
Remarques.
• On peut toutefois remarquer que 3 des arbres de la figure 3.4 “poussent vers le bas”...
© MPSI–Joly–Lycée Cézanne–2016-2017
43
3.5. Arbres
Chapitre 3 : Structures de données
(* illustration du fonctionnement *)
let Dico = dict_vide 10;;
Dico : '_a dict =
{contenu = [|Vide; Vide; Vide; Vide; Vide; Vide; Vide; Vide; Vide; Vide|];
taille = 10}
inserer 0 "physique" Dico;;
- : unit = ()
Dico;;
- : string dict =
{contenu = [|Elem "physique"; Vide; Vide; Vide; Vide; Vide; Vide; Vide;
Vide; Vide|];taille = 10}
inserer 1 "chimie" Dico;;
- : unit = ()
Dico;;
- : string dict =
{contenu =[|Elem "physique"; Elem "chimie"; Vide; Vide; Vide; Vide; Vide;
Vide;Vide; Vide|]; taille = 10}
inserer 2 "philo" Dico;;
- : unit = ()
Dico;;
- : string dict =
{contenu = [|Elem "physique"; Elem "chimie"; Elem "philo"; Vide; Vide; Vide;
Vide;Vide; Vide; Vide|];taille = 10}
rechercher 2 Dico;;
- : string = "philo"
supprimer 1 Dico;;
- : unit = ()
Dico;;
- : string dict =
{contenu = [|Elem "physique"; Vide; Elem "philo"; Vide; Vide; Vide; Vide;
Vide; Vide;Vide|]; taille = 10}
Caml 3.4.2 – Type dictionnaire, implémentation, fonctionnement
• Un arbre est en fait, un graphe un peu particulier 2 . Il s’agit en effet d’un ensemble de
sommets, dont certains sont reliés par des arêtes (cas non orienté) ou des flèches (cas orienté).
D’un sommet donné, peuvent partir zéro, une ou plusieurs flèches. En revanche, un sommet
donné n’est visé que par au plus une flèche.
• Dans le cas des arbres orientés, si une flèche relie un sommet p à un sommet f, on dit que
p est le père de f ou que f est un fils de p. Un sommet qui n’a pas de fils est une feuille de
l’arbre, un sommet qui n’est pas une feuille est un nœud. Il existe un seul nœud sans père, il
s’agit de la racine de l’arbre.
2 Plus
précisément, un arbre est un graphe orienté ou non, sans cycle, connexe
© MPSI–Joly–Lycée Cézanne–2016-2017
44
Chapitre 3 : Structures de données
Pépé
Papy
Mémé
3.5. Arbres
Urne 1
4B, 3N
Mamie
B
N
4
7
Papa
Urne 2
4B, 5N
Maman
Urne 2
3B, 6N
B
N
4
9
4
7
Bibi
·
4
9
N
3
9
4
7
·
5
9
3
7
6
9
3
9
·
3
7
6
9
/
etc
bin
∗
−
c
X11
boot
opt
dev
home
usr
bin
lib
d
local
shin share src
(c) arbre syntaxique, (a · b) + (c − d)
(d) arborescence linux (partielle) de dossiers et fichiers
Figure 3.4 – Exemples d’arbres
3.5.2
·
(b) arbre de probabilité
+
b
B
5
9
(a) arbre généalogique
a
3
7
Aspects formels
Définitions
On donne dans cette section des définitions formelles portant sur la notion d’arbre enraciné fini
(orienté).
Définition 3.5.1 (arbre enraciné fini)
Un arbre enraciné est un ensemble A muni d’une relation binaire notée ←, vérifiant :
• Il existe un unique r ∈ A tel que pour tout a ∈ A, r 8 a (r n’est pas en relation
avec a)
• Pour tout x ∈ A\{r}, il existe un unique y ∈ A tel que x ← y
• Pour tout x ∈ A\{r}, il existe un unique nombre entier naturel n et une unique
famille (x1 , · · · , xn ) d’éléments de A (éventuellement vide si n = 0) tels que :
x ← x1 ← x2 ← · · · ← xn = r
Les éléments de A sont appelés nœuds, on appelle racine de A le nœud r.
Remarque.
On peut définir la notion d’arbre enraciné comme un ensemble inductif. Rien n’empêche alors de
© MPSI–Joly–Lycée Cézanne–2016-2017
45
3.5. Arbres
Chapitre 3 : Structures de données
définir des arbres infinis. Dans la pratique, en informatique, les arbres utilisés sont toujours finis.
On se limitera donc de la définition 3.5.1 dont certains aspects sont en fait des propriétés de la
définition inductive qui n’est pas développée dans ce cours.
Exemple.
Dans l’arbre 3.4a la racine est le nœud étiqueté Bibi , dans l’arborescence de fichiers la racine /
de l’arbre s’appelle le répertoire racine !
Définition 3.5.2 (père et fils)
On considère A un arbre enraciné, x un élément de A, y l’unique élément de A tel que
x ← y. On dit que y est le père de x et que x est un fils de y.
Exemple.
Dans l’arbre généalogique 3.4a la terminologie père/fils est inversée !
Dans l’arborescence 3.4b, le nœud home est un fils du nœud dev. Le père du nœud share est le
nœud local.
Définition 3.5.3 (feuille, nœud interne, taille)
On considère A un arbre enraciné, on appelle feuille de A un nœud de A qui n’a pas de
fils, tous les autres nœuds sont appelés nœuds internes. La taille d’un arbre est |A| le
cardinal de l’ensemble A.
Exemple.
Les feuilles de l’arbre 3.4c sont les nœuds qui ne sont pas des opérations binaires.
Les feuilles de l’arbre 3.4b sont les probabilités calculées en résultat.
La taille de l’arbre 3.5.2 est 15 (en réalité elle est beaucoup plus grande !).
Définition 3.5.4 (arité, arbre binaire)
On considère A un arbre enraciné, x ∈ A. On appelle arité de x le cardinal de l’ensemble
de ses fils.
Un arbre binaire (resp. binaire entier) est un arbre dont les nœuds sont d’arité au plus 2
(resp. exactement 2).
Remarque.
Les définitions ne sont pas universelles. Il arrive souvent que la notion d’arbre binaire corresponde
à celle que l’on a défini ici sous le nom d’arbre binaire entier.
Exemple.
Les arbres 3.4a, 3.4b et 3.4c sont binaires entiers.
Le nœud étiqueté usr dans l’arbre 3.5.2 est d’arité 3 (dans l’arborescence complète cette arité est
plus grande !).
© MPSI–Joly–Lycée Cézanne–2016-2017
46
Chapitre 3 : Structures de données
3.5. Arbres
Définition 3.5.5 (profondeur et hauteur)
On considère A un arbre enraciné. La profondeur est une application de l’ensemble A
dans N, la profondeur de la racine r est 0, la profondeur des éléments de A\{r} est
l’unique nombre entier non-nul n tel qu’il existe une (x1 , · · · , xn ) d’éléments de A tels
que :
x ← x1 ← x2 ← · · · ← xn = r
Remarque.
L’unicité de la famille (x1 , · · · , xn ) est en fait liée à l’unicité du père pour chaque nœud de A\{r}.
Exemple.
Les arbres 3.4a, 3.4b et 3.4c sont de profondeurs 2, l’arbre 3.4c est de profondeur 4.
Aspects combinatoires
Les propositions qui suivent donnent des résultats fondamentaux à propos de la hauteur d’un arbre
enraciné fini.
Proposition 3.5.1 (hauteur, |A|)
def
On considère A un arbre enraciné fini d’arité maximale a, n = |A| le nombre de nœuds
de A, h la hauteur de A. Alors :
h+16n6
ah+1 − 1
a−1
Démonstration
On peut raisonner par induction sur la hauteur h de l’arbre. Un arbre de hauteur 0 est réduit à sa
racine.
Un arbre de hauteur h + 1 est composé d’au plus a sous-arbres de hauteur h enracinés en r.
On peut aussi démontrer (par induction) que pour tout nombre entier naturel l inférieur à h, il y
a au moins 1 et plus al nœuds de profondeur l.
Corollaire 3.5.1 (hauteur, |A|)
Avec les mêmes notations que dans la proposition 3.5.1, on a :
loga ((a − 1)n) − 1 6 h 6 n − 1
Remarque.
Dans les inégalités du corollaire 3.5.1, les valeurs d’encadrement ne sont pas entières et on peut
donc remplacer les inégalités par des inégalités strictes.
Corollaire 3.5.2 (hauteur, |A|)
La hauteur h d’un arbre binaire à n nœuds vérifie :
blog2 (n)c 6 h 6 n − 1
© MPSI–Joly–Lycée Cézanne–2016-2017
47
3.5. Arbres
Chapitre 3 : Structures de données
Proposition 3.5.2 (hauteur, arbres binaires)
On considère A un arbre binaire à n nœuds internes. Alors A admet au plus n+1 feuilles.
Si A est binaire entier, alors A admet exactement n + 1 feuilles.
Démonstration
On peut faire une démonstration par récurrence sur la hauteur h de l’arbre :
Initialisation :
Pour h = 1, l’arbre est réduit à sa racine, il compte 0 nœud interne et une feuille.
Hérédité :
On considère n ∈ N et on suppose que pour tout arbre de hauteur h 6 n, le nombre de feuilles est
inférieur ou égal à n + 1 où n est le nombre de nœuds internes de l’arbre.
On considère alors un arbre binaire A de hauteur h + 1 et de racine r. Les deux sous-arbres
gauche et droit enracinés respectivement en fg et fr les fils gauche et droit de r (l’un des deux
est éventuellement vide), sont de hauteur inférieure à h. On note ng et nd le nombre de nœuds
internes de chacun des deux arbres. Le nombre de nœuds internes de A est donc n = ng + nd + 1
(il faut ajouter la racine r de A aux nœuds internes des sous-arbres). L’ensemble des feuilles de A
est la réunion (disjointe) des feuilles des sous-arbres. Le nombre f de feuilles de A vérifie donc :
ng + 1 + nd + 1
ce qui assure l’hérédité de la propriété.
Pour démontrer le résultat portant sur des arbres binaires entiers, il suffit de remplacer les
inégalités de la démonstration précédente par des égalités.
3.5.3
Spécification
On ne spécifiera ici que la structure d’arbre binaire entier arbre. On peut noter que comme dans
le cas de l’arbre syntaxique 3.4a, les étiquettes des feuille et des nœuds internes, ne sont pas
nécessairement de même type. Dans le cas le plus général on considère deux types différents pour
les étiquettes des feuilles et des nœuds internes.
Une signature possible est donc la suivante :
• Trois constructeurs :
∗ arbre_vide : unit -> arbre, qui renvoie un arbre vide
∗ feuille : feuille -> arbre, qui à une étiquette e de type feuille renvoie l’arbre
réduit à la feuille étiquetée par e.
∗ noeud : knot × arbre × arbre − > arbre, qui à un nœud n de type knot, deux arbres
a1 et a2 associe l’arbre dont la racine est étiquetée par n, le fils gauche est a1 , le fils
droit a2 .
• Un prédicat :
∗ est_vide : arbre -> bool, qui renvoie vrai si l’arbre passé en paramètre est vide,
faux sinon.
• Quatre opérations de sélection :
∗ fils_gauche : arbre -> arbre, qui renvoie le sous-arbre de gauche de l’arbre passé
en argument.
∗ fils_droit : arbre -> arbre, qui renvoie le sous-arbre de droite de l’arbre passé en
argument.
∗ étiquette_interne : arbre -> knot, qui renvoie la valeur de l’étiquette de la racine
de l’arbre passé en argument (é condition que cet arbre ne soit pas vide ou réduit à une
feuille).
∗ étiquette_feuille : arbre -> feuille, qui renvoie la valeur de l’étiquette de l’arbre
(réduit à une feuille) passé en argument.
En notant f, n, des éléments de types feuille et knot, a1 et a2 deux éléments de type arbre,
on a :
© MPSI–Joly–Lycée Cézanne–2016-2017
48
Chapitre 3 : Structures de données
3.5. Arbres
• est_vide arbre_vide = vrai
• est_vide (feuille f) = faux
• est_vide(n÷ud n a1 a2 ) = faux
• fils_gauche arbre_vide = erreur
• fils_gauche (feuille f) = erreur
• fils_gauche(n÷ud n a1 a2 ) = a1
• fils_droit arbre_vide = erreur
• fils_droit (feuille f) = erreur
• fils_droit(n÷ud n a1 a2 ) = a2
• étiquette_interne arbre_vide = erreur
• étiquette_interne (feuille f) = erreur
• étiquette_interne(n÷ud n a1 a2 ) = n
• étiquette_feuille arbre_vide = erreur
• étiquette_feuille (feuille f) = f
• étiquette_feuille(n÷ud n a1 a2 ) = erreur
3.5.4
Implémentation
Comme pour la spécification, on propose ici une implémentation du type arbre qui correspond
aux arbres binaires entiers. Il faut noter que dans le type somme défini, le champ Vide ne sert pas
beaucoup, le constructeur d’arbre vide n’est en effet pas très utile, on pourrait s’en passer. Cette
dernière remarque est valable dans toute la suite. On a néanmoins gardé ce champ dans la suite
afin de “coller” rigoureusement à la spécification proposée : Caml 3.5.1
Remarque.
Les constructeurs sont inutiles, on peut utiliser les différents champs de la somme comme le
montre l’exemple développé dans Caml 3.5.2 où
expression est une implémentation de l’arbre cicontre qui correspond à l’expression arithmétique
8
−
5
+
3
∗
(2 ∗ 3) + ((5/7) − 8)
3.5.5
7
/
2
Quelques exemples de filtrages
8
On présente ici quelques fonctions sur les arbres
construites par filtrage. Les exemples sont donnés avec l’arbre expression de la section
précédente qui compte 9 sommets, 5 feuilles et
4 nœuds (internes).
−
7
/
5
+
3
∗
2
• Nombre de sommets :
• Nombre de nœuds :
• Nombre de feuilles :
© MPSI–Joly–Lycée Cézanne–2016-2017
49
3.5. Arbres
Chapitre 3 : Structures de données
(* un arbre est vide, une feuille ou un nœud *)
type ('f,'n) arbre =
| Vide
| Feuille of 'f
| nœud of 'n * ('f,'n) arbre * ('f,'n) arbre;;
type arbre defined
(* les constructeurs, inutiles... *)
let arbre_vide () = Vide;;
arbre_vide : unit -> ('a, 'b) arbre = <fun>
let feuille f = Feuille f;;
feuille : 'a -> ('a, 'b) arbre = <fun>
let nœud (knot:'n) (arbre1:('f,'n) arbre) (arbre2:('f,'n) arbre) =
(knot,arbre1,arbre2);;
nœud : 'a -> ('b, 'a) arbre -> ('b, 'a) arbre ->
'a * ('b, 'a) arbre * ('b, 'a) arbre = <fun>
(* prédicat *)
let est_vide tree = if tree = Vide then true else false;;
est_vide : ('a, 'b) arbre -> bool = <fun>
(* les sélecteurs *)
let fils_gauche tree = match tree with
| Vide -> failwith "arbre vide"
| Feuille b -> failwith "arbre feuille"
| nœud (_,g,_) -> g;;
fils_gauche : ('a, 'b) arbre -> ('a, 'b) arbre = <fun>
let fils_droit tree = match tree with
| Vide -> failwith "arbre vide"
| Feuille b -> failwith "arbre feuille"
| nœud (_,_,d) -> d;;
fils_droit : ('a, 'b) arbre -> ('a, 'b) arbre = <fun>
let etiquette_interne tree = match tree with
| Vide -> failwith "arbre vide"
| Feuille b -> failwith "arbre feuille"
| nœud (n,_,_) -> n;;
etiquette_interne : ('a, 'b) arbre -> 'b = <fun>
let etiquette_feuille tree = match tree with
| Vide -> failwith "arbre vide"
| Feuille b -> b
| nœud (_,_,_) -> failwith "nœud interne";;
etiquette_feuille : ('a, 'b) arbre -> 'a = <fun>
Caml 3.5.1 – Type arbre, implémentation
3.5.6
Parcours d’arbres binaires
Parcourir un arbre consiste à visiter successivement tous ses nœuds et toutes ses feuilles (il s’agit
donc de visiter tous les sommets de l’arbre !) en effectuant au passage un certain travail (de comptage, de modification...). On distingue deux types de parcours :
• parcours en profondeur
• parcours en largeur
© MPSI–Joly–Lycée Cézanne–2016-2017
50
Chapitre 3 : Structures de données
3.5. Arbres
let expression =
nœud("+",nœud("*",Feuille 2,Feuille 3),
nœud("-",nœud("/",Feuille 5, Feuille 7),Feuille 8));;
expression : (int, string) arbre =
nœud("+", nœud ("*", Feuille 2, Feuille 3),
nœud ("-", nœud("/",Feuille 5, Feuille 7), Feuille 8))
fils_gauche expression;;
- : (int, string) arbre = nœud ("*", Feuille 2, Feuille 3)
fils_droit expression;;
- : (int, string) arbre = nœud ("-",nœud("/",Feuille 5, Feuille 7), Feuille 8)
etiquette_interne expression;;
- : string = "+"
etiquette_feuille (fils_droit(fils_gauche expression));;
- : int = 3
Caml 3.5.2 – Type arbre, implémentation, exemple
let rec nb_sommets tree = match tree with
| Vide -> 0
| Feuille _ -> 1
| nœud (_,g,d) -> 1 + nb_sommets g + nb_sommets d;;
nb_nœuds : ('a, 'b) arbre -> int = <fun>
nb_sommets expression;;
- : int = 9
Caml 3.5.3 – Comptage de tous les sommets
let rec nb_nœuds tree = match tree with
| Vide -> 0
| Feuille _ -> 0
| nœud (_,g,d) -> 1 + nb_nœuds g + nb_nœuds d;;
nb_nœuds_internes : ('a, 'b) arbre -> int = <fun>
nb_nœuds_internes expression;;
- : int = 4
Caml 3.5.4 – Nombre de Nœuds
Parcours en profondeur
Le principe du parcours en profondeur consiste à visiter entièrement le sous-arbre de gauche avant
le sous-arbre de droite (on peut évidemment prendre la convention contraire !). Cela conduit à
la fonction suivante qui ne sert à rien puisqu’elle se contente de visiter l’arbre sans rien faire !
Caml 3.5.6
La visite d’un arbre en profondeur proposée par la fonction précédente est illustrée par la figure
3.5 dans laquelle les flèches étiquetées indiquent le chemin suivi pour visiter les nœuds.
© MPSI–Joly–Lycée Cézanne–2016-2017
51
3.5. Arbres
Chapitre 3 : Structures de données
let rec nb_feuilles tree = match tree with
| Vide -> 0
| Feuille _ -> 1
| nœud (_,g,d) -> nb_feuilles g + nb_feuilles d;;
nb_feuilles : ('a, 'b) arbre -> int = <fun>
nb_feuilles expression;;
- : int = 5
Caml 3.5.5 – Nombre de feuilles
let rec visite_prof tree = match tree with
| Vide -> ()
| Feuille _ -> ()
| nœud (_,g,d) -> visite_prof g ; visite_prof d;;
visite_prof : ('a, 'b) arbre -> unit = <fun>
Caml 3.5.6 – Parcours en profondeur
a
16
1
7
6
c
8
13
e
g
f
12
11
10
9
d
14
4
3
15
5
2
b
i
h
Figure 3.5 – Parcours en profondeur d’un arbre binaire
Remarque.
Le parcours en profondeur propose, comme sur la figure 3.5, une “navigation autour de l’arbre” en
partant de la racine, dans le sens qui correspond à un parcours des sous-arbres gauches en premier.
On parle parfois de circumnavigation.
Parcours en profondeur, traitement préfixe, infixe, postfixe
On remarque sur la figure 3.5 que tous les nœuds différents de la racine (les nœuds sur fond blanc
de la figure) admettent 3 flèches entrantes. Ils sont donc visités 3 fois :
• une première fois, en arrivant sur le nœud considéré depuis le nœud père,
© MPSI–Joly–Lycée Cézanne–2016-2017
52
Chapitre 3 : Structures de données
3.5. Arbres
• une deuxième fois, lorsque l’on a fini d’explorer le sous-arbre gauche et que l’on s’apprête à
explorer le sous-arbre droit,
• une troisième fois, lorsque l’on a fini d’explorer le sous-arbre droit.
Définition 3.5.6 (traitement préfixe, infixe, postfixe)
Lors d’un parcours en profondeur d’un arbre, si on décide d’appliquer à chaque nœud un
traitement, on dit que celui-ci est :
• préfixe, s’il est effectué lors du premier passage,
• infixe, s’il est effectué au deuxième passage,
• postfixe, s’il est effectué au troisième passage.
Pour illustrer ces idées définissons un type d’arbre très simple dont les nœuds comme les feuilles
sont des nombres entiers. On se propose d’afficher simplement les étiquettes des nœuds internes :
Caml 3.5.7
Les trois parcours peuvent se suivre sur la figure 3.6 où les flèches en gras indiquent les traitements.
1
1
16
7
6
3
4
13
6
15
5
14
5
8
3
2
2
7
12
11
10
9
4
8
9
(a) traitement prefixe
16
7
3
9
10
12
10
7
11
8
11
12
9
2
13
3
15
6
9
(b) traitement infixe
(c) traitement postfixe
Figure 3.6 – Traitements, prefixe, infixe, postfixe
© MPSI–Joly–Lycée Cézanne–2016-2017
5
53
14
4
14
4
8
7
5
6
3
4
5
2
15
5
8
3
2
2
4
7
6
13
6
16
1
1
8
1
1
9
3.5. Arbres
Chapitre 3 : Structures de données
(* définition dé'éun type dé'éarbre dé'éentiers *)
type arbre = Feuille of int| nœud of int*arbre*arbre;;
Type arbre defined.
(* fonction de traitement *)
let traitement i = print_int i;print_char `*`;;
traitement : int -> unit = <fun>
traitement 2;;
2*- : unit = ()
(* traitement, sous trois parcours *)
let rec parcours_prefixe tree = match tree with
| Feuille i -> traitement i
| nœud (i,g,d) -> traitement i; parcours_prefixe g; parcours_prefixe d;;
parcours_prefixe : arbre -> unit = <fun>
let rec parcours_infixe tree = match tree with
| Feuille i -> traitement i
| nœud (i,g,d) -> parcours_infixe g; traitement i;parcours_infixe d;;
parcours_infixe : arbre -> unit = <fun>
let rec parcours_postfixe tree = match tree with
| Feuille i -> traitement i
| nœud (i,g,d) -> parcours_postfixe g;parcours_postfixe d;traitement i;;
parcours_postfixe : arbre -> unit = <fun>
(* Exemple d'utilisation avec un arbre particulier *)
let paletuvier = nœud(1,nœud(2, Feuille 4, Feuille 5),
nœud(3,nœud(6,Feuille 8,Feuille 9),Feuille 7));;
paletuvier : arbre =
nœud(1, nœud (2, Feuille 4, Feuille 5),
nœud (3, nœud (6, Feuille 8, Feuille 9), Feuille 7))
parcours_prefixe paletuvier;;
1*2*4*5*3*6*8*9*7*- : unit = ()
parcours_infixe paletuvier;;
4*2*5*1*8*6*9*3*7*- : unit = ()
parcours_postfixe paletuvier;;
4*5*2*8*9*6*7*3*1*- : unit = ()
Caml 3.5.7 – Traitement préfixe, infixe, postfixe
Parcours en largeur
Le parcours en largeur d’un arbre correspond au parcours des nœuds profondeur par profondeur.
On commence donc par la racine, on poursuit par ses fils, en prenant la convention d’un parcours
de gauche à droite, puis les fils des nœuds de profondeur 1... C’est le sens de parcours des nœuds
de l’arbre de la figure 3.7 dans l’ordre de leur numérotation.
Remarque.
Si on symétrise l’arbre de la figure 3.7 pour que sa racine soit en bas, on peut en faire un arbre généalogique, comme le montre la figure 3.8. Si l’arbre est complet, comme sur cette figure jusqu’é la
profondeur 2, la maman de la personne de numéro n porte le numéro 2n, son papa le numéro 2n+1.
La programmation en Caml d’un parcours en largeur est un peu périlleuse. Lorsque le niveau de
profondeur p est énuméré, il faut garder en mémoire tous les sous-arbres rencontrés afin d’accéder
© MPSI–Joly–Lycée Cézanne–2016-2017
54
Chapitre 3 : Structures de données
3.5. Arbres
1
2
3
4
5
6
7
8
9
Figure 3.7 – Parcours en largeur d’un arbre
9, grand pépé
7, pépé
8, grand mémé
5, papy
6, mémé
3, papa
4, mamie
2, maman
1, Bibi
Figure 3.8 – Parcours en largeur d’un arbre généalogique
au niveau p + 1. Le parcours s’arrête lorsque la liste des sous-arbres en attente est vide. On propose
le code suivant qui sera commenté en détails. Les fonctions ftnœud et ftfeuille sont les fonctions
de traitements des appliquées aux nœuds et aux feuilles. Caml 3.5.8
Le parcours en largeur proposé en dans l’exemple Caml 3.5.8 est celui de l’arbre de la figure 3.9
sur lequel les sommets de même profondeur sont de même couleur.
Le parcours en largeur peut se suivre de la façon suivante :
• L’arbre sapin est passé en argument de parcours_largeur avec les fonctions de traitement
ftnœud et ftfeuille instanciées par print_char et print_int qui consistent simplement à
afficher les les sommets.
• On passe à traite_file la liste composée du seul arbre passé en argument :
traite_file (ajoute tree (file_vide())) (ligne 29 de Caml 3.5.8) ainsi que les fonctions
de traitement print_char et print_int.
• Au premier appel récursif, on affiche la racine (ftnœud y, ligne 23) et on passe à traite_file
la nouvelle liste d’arbres composées des deux sous-arbres droit et gauche, le premier de la
liste étant l’arbre gauche :
© MPSI–Joly–Lycée Cézanne–2016-2017
55
3.5. Arbres
Chapitre 3 : Structures de données
(* Reprise de la spécification dé'éun type file *)
type 'a file = {mutable debut : 'a list ; mutable fin : 'a list};;
let file_vide () = {debut = [] ; fin = []};;
let ajoute e f = f.fin <- e :: f.fin; f;;
let est_vide f = (f.debut = []) && (f.fin =[]);;
let premier f = ...;;
let queue f =...;;
(* définition d'un type d'arbre *)
type ('n,'f) arbre =
| Feuille of 'f
| nœud of 'n * ('n,'f) arbre * ('n,'f) arbre;;
Type arbre defined.
(* le premier argument de traite_file est un objet de type file
dans lequel apparaîtront une liste d'arbres et de sous-arbres *)
let rec traite_file f ftfeuille ftnœud =
if (est_vide f)
then ()
else let e = premier f
and q = queue f in
match e with
| Feuille x -> ftfeuille x; traite_file q ftfeuille ftnœud
| nœud (y,g,d) -> ftnœud y; traite_file (ajoute d (ajoute g q))
ftfeuille ftnœud;;
traite_file : ('a, 'b) arbre file -> ('b -> 'c) -> ('a -> 'd) -> unit = <fun>
(* parcours_largeur consiste à passer en argument la file
dont le seul objet, au départ, est l'arbre dont on veut réaliser le parcours *)
let parcours_largeur tree ftfeuille ftnœud =
traite_file (ajoute tree (file_vide())) ftfeuille ftnœud;;
parcours_largeur : ('a, 'b) arbre -> ('b -> 'c) -> ('a -> 'd) -> unit = <fun>
(* On crée un arbre dont les nœuds sont des caractères,
les feuilles des entiers *)
let sapin = nœud(char_of_int 65,nœud(char_of_int 66, Feuille 0, Feuille 1),
nœud(char_of_int 67,nœud(char_of_int 68,Feuille 2,Feuille 3),
nœud(char_of_int 69, Feuille 4, Feuille 5)));;
sapin : (char, int) arbre = nœud(`A`, nœud (`B`, Feuille 0, Feuille 1),
nœud(`C`, nœud (`D`, Feuille 2, Feuille 3),
nœud (`E`, Feuille 4, Feuille 5)))
parcours_largeur sapin print_int print_char;;
ABC01DE2345- : unit = ()
Caml 3.5.8 – Parcours en largeur, implémentation Caml
C
D
traite_file
2
E
3
4
B
5
,
0
1
print_int, print_char
On passe donc une liste d’arbre en argument (une forêt donc) dont la tête est à droite, c’est
le prochain arbre traité au deuxième appel récursif. à ce stade, c’est A qui est imprimé.
• Lors des appels récursifs, la file passée en argument de traite_file a ensuite les différents
contenus décrits par le tableau 3.2.
© MPSI–Joly–Lycée Cézanne–2016-2017
56
Chapitre 3 : Structures de données
3.5. Arbres
A
B
C
0
1
D
E
2
3
4
5
Figure 3.9 – Exemple de parcours en largeur, cas pratique : sapin
file passée en argument
print
C
D
1
0
,
E
2
,
3
E
4
5
AB
D
4
5
2
,
3
E
,
1
,
0
ABC
D
4
5
2
,
3
E
,
1
ABC0
D
4
5
2
,
3
ABC01
,
ABC01D
E
3
2
,
5
,
5
4
,
4
,
5
,
4
,
5
3
,
,
3
4
2
ABC01DE
ABC01DE2
ABC01DE23
5
ABC01DE234
file_vide
ABC01DE2345
Table 3.2 – état de la file/forêt passée en argument de traite_file
© MPSI–Joly–Lycée Cézanne–2016-2017
57
Chapitre 4
Méthodes de programmation
“
Pète et Répète sont sur un bateau, Pète tombe à l’eau, il reste ?
”
Claude Ponti, Blaise et le Château d’Anne Hiversère, 2004
4.1
4.1.1
Récursivité
Un exemple simple
Le terme de récursion est lié à l’idée de retour en arrière. Une fonction est dite récursive lorsque
la définition de la fonction fait appel à son propre identificateur. Il peut sembler a priori curieux
d’utiliser le mot que l’on cherche à définir dans la définition de ce mot ! C’est pourtant une idée
qui s’applique à des notions courantes. Par exemple si l’on souhaite donner une définition précise
des descendants de Leonhard Euler, on est conduit à dire qu’il s’agit de ses enfants, des enfants de
ses enfants, des enfants des enfants de ses enfants... ad libitum.
Un autre moyen consiste à dire que les descendants de Leonhard Euler sont ses enfants et
les descendants de ses enfants ! On conçoit qu’il s’agit d’une définition puisqu’elle permet par un
calcul fini de vérifier si un individu est descendant de Leonhard Euler1 . Cette définition est pourtant
récursive !
Nous avons déjà vu des exemples de fonctions définies récursivement par exemple la fonction
somme de Caml 2.7.3 :
let rec somme n = match n with
| 0 -> 0
| n -> n + somme (n-1);;
Caml permet de suivre la façon dont est mené le calcul gréce à la fonction trace, comme le
montre Caml 4.1.1.
La sortie du calcul peut s’interpréter par le tableau suivant 4.1.
On distingue dans le tableau 4.1 les appels récursifs à la fonction somme caractérisés par les
flèches <-, des phases de remontées des calculs repérées par les flèches ->.
4.1.2
Construction et principe
De façon informelle, la construction d’un algorithme récursif ressemble à la celle d’une suite par
récurrente en mathématiques. Ainsi, il faut :
1. un (ou plusieurs) cas de base, pour lesquels l’algorithme retourne directement un résultat sans
s’appeler lui même (le cas 0 pour somme ou les enfants de Leonhard Euler). Sinon l’algorithme
ne peut pas terminer.
1 évidemment,
il faut pour cela disposer d’un arbre généalogique universel
58
Chapitre 4 : Méthodes de programmation
4.1. Récursivité
let rec somme n = match n with
| 0 -> 0
| n -> n + somme (n-1);;
ésomme : int -> int = <fun>é
trace "somme";;
The function somme is now traced.
- : unit = ()
somme 3;;
somme <-somme <-somme <-somme <-somme -->
somme -->
somme -->
somme -->
- : int =
3
2
1
0
0
1
3
6
6
untrace "somme";;
The function somme is no longer traced.
- : unit = ()
Caml 4.1.1 – La fonction trace
Sortie trace
somme <– 3
somme <– 2
somme <– 1
somme <– 0
somme –> 0
somme –> 1
somme –> 3
somme –> 6
Calculs réalisés
somme 3
3 + somme 2
3 + (2 + somme 1)
3 + (2 + (1 + somme 0))
3 + (2 + (1 + 0))
3 + ( 2 + 1)
3 + 3
6
Table 4.1 – Tableau de suivi du calcul de somme 3
2. si le(s) paramètre(s) ne correspondent pas à un cas de base, on parle de cas inductif (tout
nombre entier différent de 0 pour somme, les petits-enfants de Leonhard Euler), l’algorithme
fait appel à lui-même (appel récursif).
Afin que l’algorithme termine, chaque appel récursif doit en principe se à rapprocher à d’un
cas de base.
Il faut noter que chaque appel récursif dispose de ses propres variables locales.
L’appel des fonctions fonctionne à l’aide d’une pile d’exécution qui conserve les contextes
d’appel :
1. À chaque appel de fonction, on empile le contexte : lieu de l’appel, variables locales, adresses
de retour des fonctions en cours d’exécution,
2. À chaque retour de fonction, on dépile le contexte, ce qui permet de revenir au point d’appel.
C’est la gestion mémoire de la pile d’exécution qui est la clef de la complexité en temps et
en espace des algorithmes récursifs
© MPSI–Joly–Lycée Cézanne–2016-2017
59
4.1. Récursivité
4.1.3
Chapitre 4 : Méthodes de programmation
Terminaison et correction
Il ne suffit pas d’utiliser let rec pour obtenir un programme qui termine ! La terminaison du
célèbre algorithme Caml 4.1.2 n’a pas encore été prouvée 2 .
let rec collatz n = match n with
| n when n <= 1 -> 0
| n when n mod 2 = 0 -> collatz (n/2)
| _ -> collatz (3 * n + 1);;
Caml 4.1.2 – Algorithme de Collatz
Un résultat théorique célèbre assure de l’indécidabilité de terminaison, ce qui se traduit en
disant qu’il est impossible de construire une fonction qui teste si un algorithme passé en argument
s’arrêtera ou non. On peut se convaincre de ce résultat par l’absurde. Supposons en effet qu’il
existe une fonction termine qui décide de l’arrêt. On considère alors la fonction absurde définie
par : Caml 4.1.3
let rec absurde () =
match termine code_absurde with
| true -> absurde()
| false -> 1;;
Caml 4.1.3 – fonction absurd
• Si la fonction absurde se termine, alors termine ”absurde” vaut true et la fonction absurde
ne se termine pas !
• Si la fonction absurde ne se termine pas, alors termine ”absurde” vaut false et la fonction
absurde se termine !
La conclusion de cette étude est la suivante :
La terminaison d’un algorithme doit être démontrée !
Des exemples de démonstration de terminaison de programme ont été vus dans le cours d’informatique pour tous, le principe consiste à déterminer une applicationtaille définie sur l’ensemble
des arguments de la fonction et à valeurs dans N. Si on parvient à démontrer que les valeurs de
l’application taille décroissent strictement au cours des appels récursifs, on obtient ainsi une suite
strictement décroissante de nombres entiers naturels. Cette suite est donc finie, les appels récursifs
sont donc en nombre fini, l’algorithme termine donc.
Sur les couples de nombres entiers, les applications suivantes peuvent être de bonnes candidates
pour définir une application taille :
taille1 : (n, p) 7→ n + p,
taille2 : (n, p) 7→ max(n, p)
Les preuves de terminaison de programme s’appuient plus généralement sur des notions mathématiques que nous exposons brièvement ici :
Définition 4.1.1 (Ordre bien fondé)
On considère E un ensemble muni d’une relation d’ordre (pas nécessairement totale) .
On note ≺ l’ordre strict correspondant :
∀x, y ∈ E,
(x ≺ y) ⇔ (x y ∧ x 6= y)
On dit que est bien fondé s’il n’existe pas de suite infinie d’éléments de E strictement
décroissante.
2 au
moment où ces lignes sont écrites
© MPSI–Joly–Lycée Cézanne–2016-2017
60
Chapitre 4 : Méthodes de programmation
4.1. Récursivité
Définition 4.1.2 (élément minimal)
On considère (E, ) un ensemble ordonné. Un élément m de E est dit minimal si et
seulement si :
∀e ∈ E, (e m) ⇔ (a = m)
Remarque.
Attention, il ne faut pas confondre minimum et minimal ! Néanmoins, si l’ordre est total, les
notions de minimum, minimal et plus petit élément coéncident.
Exemple.
Sur N\{0, 1}, la relation d’ordre définie par :
p n ⇔ p|n
est une relation d’ordre partielle dont les éléments minimaux sont les nombres premiers. Il n’y a
pas de plus petit élément pour cet ordre dans cet ensemble.
Théorème 4.1.1 (Caractérisation des ordres bien fondés)
On considère (E, ) un ensemble ordonné. l’ordre est bien fondé si et seulement si
toute partie non-vide de E admet un élément minimal.
Exercice 4.1.1
Démontrer le théorème 4.1.1.
Exercice 4.1.2
On considère sur N2 la relation binaire définie par :
(n, p) (n0 , p0 ) ⇔ (n 6 n0 )
Montrer que est une relation d’ordre sur N2 , déterminer ses éléments minimaux.
Théorème 4.1.2 (Induction sur un ensemble bien fondé)
On considère (E, ), un ensemble bien fondé, P (x) un prédicat portant sur les éléments
x de E. On note B les éléments minimaux de E. Si :
1. ∀x ∈ B,
P (x),
2. ∀x ∈ E,
(∀y ∈ E, (y ≺ x) ⇒ P (y)) ⇒ P (x)
Alors pour tout x ∈ E, P (x).
Remarque.
L’idée de ce théorème consiste à dire que si une propriété P est vérifiée pour les éléments minimaux
d’un ensemble E bien fondé et si par ailleurs, pour tout élément x de E, le fait que P (y) soit vraie
pour tous les éléments y strictement inférieurs à x implique P (x) alors P est vérifiée pour tout
élément de E. Il s’agit en fait d’une généralisation de la notion récurrence.
© MPSI–Joly–Lycée Cézanne–2016-2017
61
4.1. Récursivité
Chapitre 4 : Méthodes de programmation
Théorème 4.1.3 (Justification de la terminaison d’une fonction récursive)
On considère (E, ), un ensemble bien fondé, f : A → B une fonction récursive, ϕ : A →
E une application. On définit l’ensemble M par :
def
M = {x ∈ A,
ϕ(x) minimal dans ϕ(A)}
Si :
1. le calcul de f (x) se termine pour tous les éléments x de M,
2. pour tout a ∈ A, le calcul de f (a) n’utilise qu’un nombre fini de résultats f (y1 ),
· · · , f (yk ) tels que :
∀i ∈ J1, kK , ϕ(yi ) ≺ ϕ(a)
Alors le calcul de f (x) se termine pour toute valeur a de A.
Théorème 4.1.4 (Preuve de correction d’une fonction inductive)
On considère (E, ), un ensemble bien fondé, f : A → B une fonction récursive, ϕ : A →
E une application. On définit l’ensemble M par :
def
M = {x ∈ A,
ϕ(x) minimal dans ϕ(A)}
On considère Pf (a) un prédicat, dépendant de f , portant sur les éléments a de A tels
que :
1. Pour tout x ∈ M, Pf (x) est vrai,
2. pour tout a ∈ A, le calcul de f (a) n’utilise qu’un nombre fini de résultats f (y1 ),
· · · , f (yk ) tels que :
∀i ∈ J1, kK , ϕ(yi ) ≺ ϕ(a)
et :
(Pf (y1 ), · · · , Pf (yk )) ⇒ Pf (a)
Exercice 4.1.3
Démontrer les théorèmes 4.1.2, 4.1.3 et 4.1.4
Remarque.
Dans la pratique le plan est le suivant :
1. trouver un ordre bien fondé adapté au problème,
2. les éléments de M correspondent-ils à des cas de base pour lesquels le calcul termine et
correspond au résultat attendu (correction) ? Tous les résultats donnés par les cas de bases
sont-ils les résultats attendus (correction) ?
3. On considère a un élément arbitraire de l’ensemble des arguments possibles de la fonction f .
En supposant que l’on sache calculer f (y) pour tous les éléments y tels que y ≺ a, comment
en déduire le calcul de f (a) en utilisant qu’un nombre fini d’éléments dont l’image par ϕ est
strictement inférieure à ϕ(a) ? Si le résultat renvoyé pour ces éléments est le résultat attendu,
le résultat renvoyé pour a est-il le résultat précédent ?
En répondant par l’affirmative aux questions précédentes, on prouve la terminaison et la correction
de f simultanément. La section suivante propose deux exemples d’application.
© MPSI–Joly–Lycée Cézanne–2016-2017
62
Chapitre 4 : Méthodes de programmation
4.1.4
4.1. Récursivité
Terminaison et correction, exemples
Fonction factorielle
On cherche à montrer la terminaison et la correction de la fonction récursive factorielle définie
par :
let rec factorielle n = match n with
| 0 -> 1
| n -> n * fact (n-1);;
Dans ce cas, A = B = E = N, l’ordre est l’ordre naturel de N qui est bien fondé. Par ailleurs
ϕ = idN . Il faut bien avoir en tête ce que signifie démontrer la correction ici. Il s’agit de montrer
que :
def
factorielle(0) = 1 ∧ ∀n ∈ N\{0}, factorielle(n) = n · factorielle(n − 1)
En effet 4.1 est la définition inductive de
n
Y
(4.1)
k.
k=1
Il y a un seul élément minimal qui est en fait 0, le plus petit élément de N pour lequel le résultat
renvoyé est bien celui qui est attendu.
Par ailleurs, pour tout nombre entier naturel n, le calcul de factorielle(n) fait intervenir
que factorielle(n-1) et n − 1 < n. La terminaison de factorielle est donc démontrée.
Finalement, si factorielle(n-1) réalise le calcul défini par l’équation 4.1, il est immédiat que
factorielle(n) réalise le calcul défini par la même équation. La correction de factorielle est
donc immédiate.
Remarque.
Le cas de la fonction factorielle est assez immédiat !
Exercice 4.1.4
On considère la fonction hauteur dont l’argument est un arbre binaire :
let rec hauteur tree = match tree with
| Feuille _ -> 0
| nœud (_,g,d) -> 1 + max (hauteur g) (hauteur d);;
Prouver la correction et la terminaison de la fonction hauteur en vous appuyant sur
le fait que la hauteur d’un arbre binaire est le maximum des profondeurs de ses sommets.
pgcd, version récursive
On cherche à prouver la correction et la terminaison de la fonction récursive pgcd définie par :
let rec pgcd a b = match (a,b) with
| (a,b) when a = b -> a
| (a,b) when a > b -> pgcd (a-b) b
| _ -> pgcd a (b-a);;
Il s’agit de démontrer que pgcd calcule bien le plus grand commun diviseur de deux nombres entiers naturels strictement positifs passés en argument. Dans la suite on notera N + l’ensemble
défini par :
def
N + = N\{0}
L’idée consiste ici à considérer A = E = N + × N + , B = N + , avec E muni soit de l’ordre
def
lexicographique, soit de l’ordre produit pour lesquels il est bien fondé, ϕ = idN + ×N + .
© MPSI–Joly–Lycée Cézanne–2016-2017
63
4.1. Récursivité
Chapitre 4 : Méthodes de programmation
On a M = {(1, 1)} qui correspond bien à un cas de base pour lequel le résultat est bien celui
qui est attendu.
Tous les autres cas de bases donnent bien le résultat attendu.
Si le couple (a, b) ne correspond pas à un cas de base alors le calcul de (pgcd a b) passe par
celui de (pgcd a-b b) si a > b, puisque les nombres entiers considérés sont strictement positifs on
a bien :
(a, b) ≺ (a − b, b)
que cela soit pour l’ordre lexicographique ou l’ordre produit.
Dans le cas où a < b, le calcul de (pgcd a b) passe par celui de (pgcd a b-a) et (a, b) ≺ (a, b − a).
Puisque dans le premier cas a ∧ b = (a − b) ∧ b et dans le second a ∧ b = a ∧ b − a la preuve
de terminaison et de correction est terminée.
Remarques.
• Lorsque l’on rédige la démonstration on se demande bien pourquoi on ne prend pas comme
seul cas de base (1, 1) pour proposer le code suivant :
let rec pgcd2 a b = match (a,b) with
| (1,1) -> 1
| (a,b) when a > b -> pgcd2 (a-b) b
| _ -> pgcd2 a (b-a);;
Il faut bien voir qu’il s’agit la d’une mauvaise idée, en effet les cas de bases de pgcd permettent
de garder les couples de nombres entiers passés en argument dans N + × N + ce qui est
indispensable. Réfléchissez à ce qui se passe lorsque vous lancez (pgcd2 5 5)...
• On aurait pu aussi considérer l’application ϕ définie par :
ϕ:
N+ × N+
(a, b)
→
N
7
→
a+b
def
c’est-à-dire poser E = N, muni de son ordre usuel pour lequel il est bien fondé.
4.1.5
Terminaison et correction, exercices d’application
Exercice 4.1.5
Montrer que si (E, ) est bien fondé, alors pour toute partie non-vide A de E, l’ensemble
A muni de l’ordre induit par est bien fondé.
Exercice 4.1.6
Montrer que N2 muni de l’ordre lexicographique ou de l’ordre produit est bien fondé.
Pourquoi peut-on en déduire que N + × N + uni de l’ordre lexicographique ou de l’ordre
produit est bien fondé ?
Exercice 4.1.7
On considère la fonction récursive binom définie par :
let rec binom n p = match (n,p) with
| (n,0) -> 1
| (n,p) -> if (p >n) then 0
else binom (n-1) (p-1) + binom (n-1) p;;
© MPSI–Joly–Lycée Cézanne–2016-2017
64
Chapitre 4 : Méthodes de programmation
4.2. Diviser pour régner
Démonter la terminaison de l’application binom et démontrer que pour tout couple
(n, p) de nombres entiers naturels :
n
binom n p =
p
Pourquoi n’est-il pas possible de remplacer le cas de base par |(0,0) -> 1 ?
Exercice 4.1.8
La fonction d’Ackermann est très célèbre parce qu’elle donne un exemple de fonction
récursive qui n’est pas primitive3 . Cette fonction est définie par :
let rec ackermann n p = match (n,p) with
| (0,p) -> p+1
| (n,0) -> ackermann (n-1) 1
| (n,p) -> ackermann (n-1) (ackermann n (p-1));;
Prouver la terminaison de la fonction.
Exercice 4.1.9
La fonction morris termine-t-elle ?
let rec morris n p = match (n,p) with
| 0,_ -> 1
| (n,p) -> morris (n-1) (morris n p);;
4.2
4.2.1
Diviser pour régner
Principe de la méthode
Les algorithmes ou les fonctions qui s’appuient sur le principe de récursivité illustre souvent une
approche de programmation dite diviser pour régner. Cette méthode consiste à séparer le problème
posé en sous-problèmes semblables au problème initial mais de taille plus petite. L’algorithme
résout les sous-problèmes de façon récursive et combine les solutions trouvées pour renvoyer la
réponse au problème initial.
Le modèle de programmation diviser pour régner s’appuie donc sur trois étapes à chaque appel
récursif :
• Diviser le problème en un certain nombre de sous-problèmes.
• Régner sur les sous-problèmes en les résolvant de façon récursive. Pour un problème élémentaire la solution est immédiate (pour assurer la terminaison de l’algorithme).
• Combiner les solutions des sous-problèmes afin de construire la solution du problème initial.
4.2.2
Exemples d’algorithmes diviser pour régner
Tri rapide
Le tri rapide (quicksort) repose sur le principe suivant :
• Si la liste est vide ou réduite à un seul élément, l’algorithme laisse la liste inchangée.
© MPSI–Joly–Lycée Cézanne–2016-2017
65
4.2. Diviser pour régner
Chapitre 4 : Méthodes de programmation
• Si la liste l est de longueur au moins 2, on peut distinguer sa tête h de sa queue q. On
sépare q en deux sous-listes, celle des éléments supérieurs à h et celle des éléments inférieurs
(strictement) à h.
• On trie suivant les deux premières règles les deux sous-listes obtenues. On assemble finalement
pour obtenir la liste triée.
L’implémentation en Caml peut être la suivante, elle passe par la définition de la fonction
separe qui permet d’obtenir les deux sous-listes à partir de la queue de liste et de la tête de liste :
Caml 4.2.1, 4.2.2
En traçant la fonction tri_rapide on peut suivre son fonctionnement sur un exemple : Caml 4.2.3
On peut suivre les différentes étapes de type diviser, régner et combiner dans le tableau 4.2.
Listes à trier
[3;4;2;1;5]
[4;5] [2;1]
[5] [] [2;1]
[] [2;1]
[2;1]
[2;1]
[] [1]
[1]
-
Construction du résultat
tri_rapide [3;4;2;1;5] = tri_rapide [2;1]-3-tri_rapide [4;5]
tri_rapide [4;5] = tri_rapide []-4-tri_rapide [5]
tri_rapide [5] = [5]
tri_rapide [] = []
tri_rapide [4;5] = [4;5]
tri_rapide [2;1] = tri_rapide [1]-2-tri_rapide []
tri_rapide [] = []
tri_rapide [1] = [1]
tri_rapide [2;1] = [1;2]
tri_rapide [3;4;2;1;5] = [1;2;3;4;5]
Table 4.2 – diviser, régner, combiner
Exercice 4.2.1
Démontrer la terminaison et la correction de la fonction separe puis de la fonction
tri_rapide.
Tri par partition fusion, merge sort
Le tri par fusion (mergesort) repose sur le principe suivant :
• On divise en deux moitié la liste à trier.
• On tire les listes issues de la division.
• On fusionne les listes triées obtenues pour obtenir le résultat final.
On peut envisage l’implémentation du tri fusion à partir de listes ou à partir de tableaux. Nous
présentons les deux solutions.
Implémentation par les listes
La première étape de l’implémentation par des listes passe par la fonction divise qui permet
de couper en deux une liste en deux listes de longueurs égales ou presque (dans le cas d’une liste
de départ de longueur impaire). La méthode consiste à répartir chacun des éléments de la liste de
départ alternativement dans la première et la seconde liste : Caml 4.2.4
La seconde étape, Caml 4.2.5, passe par la fonction fusion qui fusionne deux listes déjà triées.
La fonction tri_fusion se définit alors facilement : Caml 4.2.6
Remarque.
On note que cette implémentation correspond à une programmation dans laquelle les structures
de données sont persistantes, la liste passée en argument n’est pas modifiée.
On peut suivre les différentes étapes du fonctionnement de la fonction tri_fusion dans le
tableau 4.3.
Implémentation par les tableaux
© MPSI–Joly–Lycée Cézanne–2016-2017
66
Chapitre 4 : Méthodes de programmation
Listes à trier
[3;4;2;1;5]
[4;1] [3;2;5]
[1] [4] [4;1] [3;2;5]
[4] [4;1] [3;2;5]
[4;1] [3;2;5]
[3;2;5]
[2] [3;5]
[3;5]
[5] [3]
[3]
-
4.2. Diviser pour régner
Construction du résultat
tri_rapide [3;4;2;1;5] = fusion tri_fusion [3;2;5] tri_fusion [4;1]
tri_fusion [4;1] = fusion tri_fusion [4]-tri_rapide [1]
tri_fusion [1] = [1]
tri_fusion [4] = [4]
tri_fusion [4;1] = [1;4]
tri_fusion [3;2;5] = fusion tri_fusion [3;5] tri_fusion [2]
tri_fusion [2] = [2]
tri_fusion [3;5] = fusion tri_fusion [3] tri_fusion [5]
tri_fusion [5] = [5]
tri_fusion [3] = [3]
tri_fusion [3;5] = [3;5]
tri_fusion [3;2;5] = [2;3;5]
tri_fusion [3;4;2;1;5] = [1;2;3;4;5]
Table 4.3 – Tri par partition et fusion
L’accès à un élément d’un tableau se faisant en temps constant dans Caml , il est facile de
diviser un tableau en deux. Cette structure de données est donc bien adaptée à l’implémentation
du tri par partition et fusion.
On propose l’architecture de Caml 4.2.7 la fonction principale. La fonction fusion_vect n’a
pas été définie...
Le code Caml proposé en Caml 4.2.8 est un peu technique est mérite que l’on se penche dessus.
Multiplication des nombres entiers, algorithme de Karatsuba
Le principe général de l’algorithme de Karatsuba s’appuie sur une formule qui permet le calcul du
produit de deux grands nombres entiers x et y en utilisant 3 multiplications de nombres entiers
plus petits dans le sens où ces nombres entiers s’écrivent approximativement avec deux fois moins
de chiffres.
On suppose que x et y s’écrivent comme une chaîne de n digits dans une certaine base B. Pour
tout nombre entier naturel non-nul m plus petit que n, la division euclidienne de x et y par B m
peut s’écrire :
x = x1 · B m + x0 , y = y1 · B m + y0
On a alors :
xy = (x1 · B m + x0 ) (y1 · B m + y0 ) = z2 B 2m + z1 B m + z0
(4.2)
oé :
z2 = x1 y1 ,
z1 = x1 y0 + x0 y1 ,
z0 = x0 y0
(4.3)
La formule 4.2 requiére donc 4 multiplications pour obtenir le résultat de x · y.
Anatoly Alexeevitch Karatsuba proposa en 1962 une méthode qui permet d’obtenir le produit
xy en se limitant à 3 multiplications (et quelques additions). En effet, en reprenant les notations
de 4.3, on a :
z1 = (x1 + x0 ) · (y1 + y0 ) − z2 − z0
Exercice 4.2.2
Démontrer le résultat de l’égalité 4.4
© MPSI–Joly–Lycée Cézanne–2016-2017
67
(4.4)
4.3. Programmation dynamique
Chapitre 4 : Méthodes de programmation
Exemple.
On cherche à calculer le produit de x et y avec x = 12345 et y = 6789, en base 10. Ici n = 5 et on
pose m = 3. On a :
B m = 1000,
12345 = 12 · 1000 + 345,
6789 = 6 · 1000 + 789
On a donc :
z2
=
12 · 6 = 72
z0
=
345 × 789 = 272205
z1
=
(12 + 345) · (6 + 789) − z2 − z0 = 11538
finalement :
x · y = z2 · B 2m + z1 · B m + z0 = 83810205
L’algorithme peut bien évidemment être implémenté de façon récursive.
On propose ici une implémentation dans le cas de la base 2. Cela n’est pas très cohérent dans
la mesure où la multiplication des nombres binaires ne pose pas de gros souci (il faut faire des
additions de nombres binaires obtenus par décalage). Le programme s’adapte toutefois à d’autres
situations et permet, en théorie, de traiter des nombres entiers qui dépassent la représentation en
machine des entiers.
L’architecture générale est celle proposée dans Caml 4.2.9, elle repose sur la représentation des
nombres entiers binaires sous forme de tableaux.
Les fonctions à définir en amont sont explicitées dans Caml 4.2.10.
4.3
Programmation dynamique
4.3.1
Principes de la programmation dynamique
La programmation dynamique peut être apparaître d’un certain point de vue comme un prolongement de la stratégie diviser pour régner. Lorsque la programmation dynamique peut être mise en
place, elle peut s’avérer plus efficace qu’un algorithme récursif.
C’est typiquement le cas lorsque la solution récursive “passe” plusieurs fois par le même calcul
lors des appels récursifs.
Le terme de programmation dans l’expression programmation dynamique n’est pas très adapté
puisque la programmation dynamique consiste en fait à conserver dans un tableau les résultats
intermédiaires. L’accès à ces résultats se faisant en temps constant, il est préférable de les chercher
dans le tableau lorsque l’on en a besoin plutôt que de les recalculer ! On pourrait ainsi parler de
tabulation plutôt que de programmation.
Définition 4.3.1 (Mémoïsation)
La technique qui consiste à réduire le temps d’exécution d’une fonction récursive en
mémorisant les résultats intermédiaires susceptibles d’être réutilisés s’appelle la mémoïsation.
Le principe de la programmation dynamique s’attache à ne calculer qu’une seul fois le résultat
d’un sous-sous-problème du problème initial.
La programmation dynamique est souvent appliquée à des problèmes d’optimisation : on cherche
la “meilleure” solution parmi un grand nombre de solutions. Il peut exister plusieurs solutions de
coût optimal, il s’agit alors de donner une des solutions optimales. Pour ces problèmes, il existe
évidemment une programmation en force brute qui consiste à faire la liste de toutes les solutions
© MPSI–Joly–Lycée Cézanne–2016-2017
68
Chapitre 4 : Méthodes de programmation
4.3. Programmation dynamique
(si cette liste est finie) puis à ordonner ces solutions suivant leur coût. C’est ce type de solution,
très coûteuse en temps, que cherche à contourner la programmation dynamique.
Un algorithme de programmation dynamique peut se diviser en quatre grandes étapes :
1. caractériser la structure d’une solution optimale ;
2. définir récursivement la valeur d’une solution optimale ;
3. calculer la valeur d’une solution optimale en remontant progressivement jusqu’à l’énoncé du
programme initial ;
4. construire une solution optimale pour les informations calculées. Les 3 premiers points sont
incontournables pour la résolution du problème posé. En revanche, le point 4 n’a d’intérêt que
lorsque l’on souhaite, en plus de sa valeur, construire explicitement une solution optimale.
Les sections suivantes donnent des exemples qui illustrent cette méthode.
4.3.2
Ordonnancement de tâches pondérées
Ce problème classique peut se présenter sous la forme qui suit. On vous propose de réaliser une
partie d’un ensemble de tâches Tj qui ont chacune une date de début dj , une date de fin fj , et une
valeur vj (que l’on peut définir comme la rétribution de la tâche Tj ). Vous cherchez évidemment
à optimiser votre gain en choisissant judicieusement une partie soit réalisable : les tâches de ce
sous-ensemble ne doivent pas se superposer4 .
La figure 4.1 illustre le propos.
a
b
c
d
e
f
g
h
0
1
2
3
4
5
6
7
8
9
10
11
temps
Figure 4.1 – Principe des tâches pondérées
La technique en force brute consisterait ici à définir tous les sous-ensembles possibles et à calculer la valeur de chacun des sous-ensembles pour lesquels les tâches sont mutuellement compatibles.
Si l’ensemble de départ compte n travaux, il faudra chercher dans une ensemble de cardinal 2n . Ce
qui devient vite déraisonnable.
Le principe de l’algorithme va consister d’abord à ordonner et indicer les tâches par dates de
fin croissantes. Pour chaque indice j strictement positif, on définit alors p(j) par :
p(j) = max {i < j,
Ti , Tj compatibles}
On obtient, en partant de l’exemple de la figure 4.1, le résultat illustré par 4.2
4 C’est typiquement le genre de soucis que gére le proviseur du lycée tous les jours, il a plusieurs rendez-vous de
différentes importances qui s’entrechoquent, lesquels garder pour que la journée soit la plus utile possible ?
© MPSI–Joly–Lycée Cézanne–2016-2017
69
4.3. Programmation dynamique
Chapitre 4 : Méthodes de programmation
1
2
j
0
1
2
3
4
5
6
7
8
3
4
5
6
7
8
0
1
2
3
4
5
6
7
8
9
10
p(j)
−
0
0
0
1
0
2
3
5
11
temps
Figure 4.2 – Principe des tâches pondérées
Dans le chapitre suivant nous verrons pourquoi l’algorithme, en programmation dynamique, de
recherche d’une solution optimale est plus efficace qu’une recherche sans enregistrement.
Le problème à traiter peut se présenter sous la forme suivante.
On suppose que n tâches T1 , · · · , Tn sont ordonnées dans l’ordre de leur dates de fin. Pour tout
j ∈ J1, nK, on note opt(j) le poids maximal atteignable en ne considérant que les tâches T1 , · · · , Tj .
def
On pose opt(0) = 0. L’idée qui fait apparaître opt(j) comme constitué des sous-problèmes, consiste
à discuter de la présence ou non de la tâche Tj dans la solution qui permet d’obtenir opt(j).
• Si Tj fait partie de la solution optimale alors : opt(j) = vj + opt(p(j − 1)),
• si Tj ne fait pas partie de la solution optimale alors : opt(j) = opt(j − 1)
Ainsi donc le problème récursif est-il défini par :

 vj + opt (p(j))
opt(j − 1)
opt(j) = max

0
(4.5)
L’implémentation passe par la définition du type tache et la définition de la fonction p dans
laquelle on suppose que le vecteur de tâches tache_vect est ordonné suivant les dates de fin et qui
retourne le tableau des valeurs de p(j) (Caml 4.3.1).
Il reste à définir la fonction ordonnancement, Caml ??, en s’appuyant sur l’équation 4.5. La
fonction memoisation permet l’enregistrement des calculs intermédiaires.
On peut suivre sur Caml 4.3.3 la façon d’utiliser la fonction ordonnancement.
Il faut toute fois noter que le calcul de opt(j) ne nécessite que les valeurs de opt(k) pour des
indices k strictement plus petits que j, on peut donc envisager une programmation ascendante 5
de la mémoésation. C’est ce que propose Caml 4.3.4.
Évidemment, les deux fonctions ordonnancement et ordo_bottom_up, donnent la même réponse, mais il ne s’agit que du gain réalisé ! On ne sait pas comment réaliser ce gain6 . Il faut bien
avoir en tête s’il n’existe peut être pas qu’une seule solution optimale, le but reste néanmoins d’en
déterminer une. L’idée simple de calcul d’une solution optimale repose sur le fait que l’on peut
prendre l’intervalle j dans cette solution dans le cas où :
vj + opt (p(j)) > opt(j − 1)
5 bottom-up
6 Il
diraient les anglo-saxons
faut que le proviseur sache aussi organiser son agenda !
© MPSI–Joly–Lycée Cézanne–2016-2017
70
(4.6)
Chapitre 4 : Méthodes de programmation
4.3. Programmation dynamique
Pour bien comprendre la méthode de mise en oeuvre qui s’appuie sur l’équation 4.6, en revenant
en arrière 7 , nous allons illustrer l’idée sur la figure 4.3 où l’on reprend l’exemple de cette section.
Le principe consiste déterminer d’abord si la dernière tâche, d’indice n, fait ou non partie de la
solution. Il s’agit simplement de répondre à la question :
vn + opt (p(n)) > opt(n − 1)
On étudie le sous-problème suivant, c’est-é-dire celui de l’ensemble d’indices {1, · · · , p(n)} ou
{1, · · · , n − 1}.
Dans le cas qui nous occupe n = 8. Sur la figure 4.3, on lit dans la rangée 8 : (9 > 6), la tâche
8 fait donc partie d’une solution optimale.
On examine alors la rangée 5, qui comme l’indique la flèche correspond à p(8).
À la rangée 5, on a : (2 < 3), on peut donc éliminer la tâche 5 pour construire une solution
optimale.
Sur le même principe, on retient la tâche 6 et enfin la tâche 2.
v=2
v=3
v=2
v=4
v=2
v=3
v=3
v=3
0 2 2 3 6 6 6 6 9
1
2
3
4
5
6
7
8
vj + opt (p(j)) 2
3
2
6
2
6
6
9
2
3
3
3
6
6
6
opt (j − 1)
0
Figure 4.3 – Principe de calcul d’une solution optimale
L’implémentation du calcul d’une solution optimale peut donc être réalisée comme en Caml 4.3.5.
On commence par construire une fonction ordo_bottom_up_vect tache_vect qui permet d’obtenir tous les résultats intermédiaires nécessaire. L’idée de l’équation 4.6 est alors directement
implémentée dans la fonction backtrack par chercheSol. La solution est donnée sous la forme
d’une liste.
Exercice 4.3.1
Proposer un code qui transforme une liste de tâches en un tableau trié de tâches suivant
les dates de fin croissantes.
7 En
anglais, on parle de backtracking
© MPSI–Joly–Lycée Cézanne–2016-2017
71
4.3. Programmation dynamique
4.3.3
Chapitre 4 : Méthodes de programmation
Distance d’édition
Le principe de la distance d’édition8 consiste à trouver le nombre minimal d’opérations d’éditions
qui transforme une chaîne de caractères a en une chaîne de caractères b. Cette distance est utilisée
en particulier dans les correcteur orthographiques ou dans les moteurs de recherche afin de rendre
les mots clefs résistants aux erreurs de frappe (ou d’orthographe !).
Les opérations d’édition sont les suivantes :
• Insertion d’un caractère à la position i.
• Effacement d’un caractère à la position i.
• Remplacement d’un caractère à la position i.
L’idée consiste à nouveau à définir le problème de façon récursive. On considère par exemple
les deux chaînes de caractères suivantes :
a = fonctionbijective,
b = applicationinjective
a est de longueur 17 et b est de longueur 20. Pour i et j deux nombres entiers naturels respectivement plus petits que 17 et 20, on note a1..i et b1..j les préfixes de a et b de longueurs respectives i
et j. Par exemple :
a10 = fonctionbi, b5 = appli
On définit alors D(i, j) comme la distance de a1..i à b1..j . En notant la| et |b| la longueur des
chaînes a et b, le but est le calcul de D(|a|, |b|). La fonction D est récursivement définie par :

j
si i = 0




si
j=0
 i 

D(i − 1, j) + 1
(4.7)
D(i, j) =

 min  D(i, j − 1) + 1

sinon



D(i − 1, j − 1) + (1 − δai ,bj )
Où δx,y désigne le symbole de Kronecker :
(
1 si x = y
δx,y =
0 sinon
Dans l’équation 4.7, les deux premiers cas correspondent à la distance d’édition entre une chaîne
et la chaîne vide. Le troisième cas se décompose en :
• D(i − 1, j) + 1 : on efface un caractère dans a.
• D(i, j − 1) + 1 : on ajoute un caractère dans a.
• D(i − 1, j − 1) + (1 − δai ,bj ) : si ai = bj on calcule la distance d’édition entre a1..(i−1) et
b1..(j−1) , sinon, on remplace le i-ième caractère ai de a par bj .
Une première implémentation peut être envisagée comme indiqué en Caml 4.3.6.
Il faut toute fois noter que comme pour le calcul de opt(j), le calcul de D(i, j) ne nécessite
que les valeurs de D(k, l) pour des couples d’indices (k, l) tels que k < i et l < j. On peut donc à
nouveau envisager une programmation bottom-up. C’est ce que propose Caml 4.3.7.
8 on
parle aussi de distance de Levenshtein
© MPSI–Joly–Lycée Cézanne–2016-2017
72
Chapitre 4 : Méthodes de programmation
4.3. Programmation dynamique
(* la fonction separe est celle qui réalise la séparation en deux sous-listes
en fonction de l'élément de tête *)
let rec separe (e:int) liste = match liste with
| [] -> ([],[])
| d::r -> let l1,l2 = separe e r in
if (d < e) then (d::l1,l2) else (l1,d::l2);;
separe : int -> int list -> int list * int list = <fun>
(* Pour comprendre la fonction separe *)
trace "separe";;
The function separe is now traced.
- : unit = ()
separe 18 [3;10;25;9;3;11;13;23;8];;
separe <-- 18
separe --> <fun>
separe* <-- [3; 10; 25; 9; 3; 11; 13; 23; 8]
separe <-- 18
separe --> <fun>
separe* <-- [10; 25; 9; 3; 11; 13; 23; 8]
separe <-- 18
separe --> <fun>
separe* <-- [25; 9; 3; 11; 13; 23; 8]
separe <-- 18
separe --> <fun>
separe* <-- [9; 3; 11; 13; 23; 8]
separe <-- 18
separe --> <fun>
separe* <-- [3; 11; 13; 23; 8]
separe <-- 18
separe --> <fun>
separe* <-- [11; 13; 23; 8]
separe <-- 18
separe --> <fun>
separe* <-- [13; 23; 8]
separe <-- 18
separe --> <fun>
separe* <-- [23; 8]
separe <-- 18
separe --> <fun>
separe* <-- [8]
separe <-- 18
separe --> <fun>
separe* <-- []
separe* --> [], []
separe* --> [8], []
separe* --> [8], [23]
separe* --> [13; 8], [23]
separe* --> [11; 13; 8], [23]
separe* --> [3; 11; 13; 8], [23]
separe* --> [9; 3; 11; 13; 8], [23]
separe* --> [9; 3; 11; 13; 8], [25; 23]
separe* --> [10; 9; 3; 11; 13; 8], [25; 23]
separe* --> [3; 10; 9; 3; 11; 13; 8], [25; 23]
- : int list * int list = [3; 10; 9; 3; 11; 13; 8], [25; 23]
Caml 4.2.1 – Le tri rapide, principe
© MPSI–Joly–Lycée Cézanne–2016-2017
73
4.3. Programmation dynamique
Chapitre 4 : Méthodes de programmation
(* l'algorithme de tri rapide lui même *)
let rec tri_rapide liste = match liste with
| [] -> []
| [e] -> [e]
| e::r -> let l1,l2 = separe e r in
(tri_rapide l1) @ (e :: (tri_rapide l2));;
tri_rapide : int list -> int list = <fun>
Caml 4.2.2 – Le tri rapide, l’algorithme lui même
untrace "separe";;
The function separe is no longer traced.
- : unit = ()
trace "tri_rapide";;
The function tri_rapide is now traced.
- : unit = ()
tri_rapide [3;4;2;1;5];;
tri_rapide <-- [3; 4; 2; 1; 5]
tri_rapide <-- [4; 5]
tri_rapide <-- [5]
tri_rapide --> [5]
tri_rapide <-- []
tri_rapide --> []
tri_rapide --> [4; 5]
tri_rapide <-- [2; 1]
tri_rapide <-- []
tri_rapide --> []
tri_rapide <-- [1]
tri_rapide --> [1]
tri_rapide --> [1; 2]
tri_rapide --> [1; 2; 3; 4; 5]
- : int list = [1; 2; 3; 4; 5]
Caml 4.2.3 – le tri rapide, fonctionnement
let rec divise liste = match liste with
| [] -> ([],[])
(* cas d'un seul élément dans la liste *)
| [u] -> ([u],[])
(* s'il y a au moins deux éléments,
on distribue les deux premiers sur la première
et la seconde liste respectivement *)
| (premier::deuxieme::reste) -> let (liste1,liste2) = divise reste in
(premier::liste1,deuxieme::liste2);;
divise : 'a list -> 'a list * 'a list = <fun>
divise [1;2;3;4;5];;
- : int list * int list = [1; 3; 5], [2; 4]
Caml 4.2.4 – Le tri fusion, implémentation par les listes, la fonction divise
© MPSI–Joly–Lycée Cézanne–2016-2017
74
Chapitre 4 : Méthodes de programmation
4.3. Programmation dynamique
let rec fusion liste1 liste2 = match (liste1,liste2) with
| liste,[] -> liste
| [],liste -> liste
| (a::b),(c::d) -> if a < c then a::(fusion b liste2)
else c::(fusion liste1 d);;
fusion : 'a list -> 'a list -> 'a list = <fun>
fusion [1;3;5] [2;4;6;8];;
- : int list = [1; 2; 3; 4; 5; 6; 8]
Caml 4.2.5 – Le tri fusion, implémentation par les listes, la fonction fusion
let rec tri_fusion liste = match liste with
| [] -> []
| [e] -> [e]
(* si la liste comporte au moins deux éléments,
le résultat est la fusion des deux listes triées obtenues
par la division de la liste passée en argument *)
| l -> let (liste1,liste2) = divise l in
fusion (tri_fusion liste1) (tri_fusion liste2);;
tri_fusion : 'a list -> 'a list = <fun>
Caml 4.2.6 – Le tri fusion, implémentation par les listes, la fonction tri_fusion
let tri_fusion_vect vecteur =
(* initialisation dé'éun vecteur auxiliaire de même taille *)
let n = (vect_length vecteur) in
let aux = make_vect n 0 in
(* tri fait trie le vecteur entre les indices passés en argument *)
tri vecteur 0 (n-1) aux
where rec tri v i j ax =
if i < j then
begin
let m = (i + j)/2 in
tri v i m ax;
tri v (m+1) j ax;
fusion_vect v i j ax;
end;;
Caml 4.2.7 – Le tri fusion, implémentation par les tableaux, la fonction tri_fusion_vect
© MPSI–Joly–Lycée Cézanne–2016-2017
75
4.3. Programmation dynamique
Chapitre 4 : Méthodes de programmation
(* fusion fusionne deux moitiés de vecteurs déjà triés
entre les indices debut et fin *)
let fusion vecteur debut fin aux =
let m = (debut + fin)/2 and i = ref debut in let j = ref (m+1) in
(* boucle de copie dans aux des éléments dans lé'éordre *)
for k = 0 to (fin - debut) do
if (!i <= m)
then
begin
if (!j <= fin)
then
begin
if vecteur.(!i) <= vecteur.(!j)
then begin aux.(k) <- vecteur.(!i); incr i end
else begin aux.(k) <- vecteur.(!j); incr j end
end
else
begin aux.(k) <- vecteur.(!i); incr i end
end
else begin aux.(k) <- vecteur.(!j); incr j end
done;
(* boucle de copie dans vecteur du travail réalisé *)
for k = 0 to (fin - debut) do
vecteur.(debut+k) <- aux.(k)
done;;
fusion : 'a vect -> int -> int -> 'a vect -> unit = <fun>
let vect = [|1;2;5;6;3;4;7;8|];;
vect : int vect = [|1; 2; 5; 6; 3; 4; 7; 8|]
let auxi = make_vect 8 0;;
auxi : int vect = [|0; 0; 0; 0; 0; 0; 0; 0|]
fusion vect 0 7 auxi;;
- : unit = ()
vect;;
- : int vect = [|1; 2; 3; 4; 5; 6; 7; 8|]
auxi;;
- : int vect = [|1; 2; 3; 4; 5; 6; 7; 8|]
Caml 4.2.8 – Le tri fusion, implémentation par les tableaux, la fonction fusion
© MPSI–Joly–Lycée Cézanne–2016-2017
76
Chapitre 4 : Méthodes de programmation
4.3. Programmation dynamique
(* Karatsuba cas des entiers binaires *)
let rec karatsuba x y =
let longx = (vect_length x) and longy = (vect_length y) in
let long = max longx longy in
let m = long/2 in
(* on transforme les vecteurs pour les étirer sur la même longueur *)
let xx = concat_vect x (make_vect (long-longx) 0)
and yy = concat_vect y (make_vect (long-longy) 0)
in
if long = 1
then mult xx yy
else
if long = 0 then [||]
else
begin
(* la fonction coupe est détaillée plus loin, elle découpe
les deux tableaux en deux tableaux de
longueurs sensiblement égales *)
let x0,x1 = coupe xx
and y0,y1 = coupe yy
in
(* on définit z0 et z1 *)
let z0 = karatsuba x0 y0
and z2 = karatsuba x1 y1
in
(* la fonction decale permet de calculer z2 * B^{2m} *)
let Z2 = decale (2*m) z2
(* op calcule les opposés en complément à 2 *)
and z2m = op long z2
and z0m = op long z0
in
let z1 = (plus z0m (plus z2m (karatsuba (plus x1 x0) (plus y1 y0))))
in let Z1 = decale m z1
in sub_vect (plus Z2 (plus Z1 z0)) 0 (2*long)
end;;
karatsuba : int vect -> int vect -> int vect = <fun>
Caml 4.2.9 – Algorithme de Karatsuba, cas des nombres entiers
© MPSI–Joly–Lycée Cézanne–2016-2017
77
4.3. Programmation dynamique
Chapitre 4 : Méthodes de programmation
let mult x y =
[|x.(0)*y.(0)|];;
mult : int vect -> int vect -> int vect = <fun>
let coupe x =
let long = vect_length x in let m = long/2 in
(sub_vect x 0 m,sub_vect x m (long-m));;
coupe : 'a vect -> 'a vect * 'a vect = <fun>
coupe [|1;0;0;1;1|];;
- : int vect * int vect = [|1; 0|], [|0; 1; 1|]
let plus a b =
let na = vect_length a
and nb = vect_length b
and retenue = ref 0 in
let n = max na nb in
let res = make_vect n 0
and aa = concat_vect a (make_vect (n-na) 0)
and bb = concat_vect b (make_vect (n-nb) 0)
in
begin for i = 0 to (n-1) do
let re = aa.(i)+bb.(i)+ !retenue in
match re with
| 2 -> begin res.(i) <- 0; retenue := 1 end
| 3 -> begin res.(i) <- 1; retenue := 1 end
| _ -> begin res.(i) <- re; retenue := 0 end
done;
if !retenue = 1 then (concat_vect res [|1|]) else res
end;;
plus : int vect -> int vect -> int vect = <fun>
plus [|1;1|] [|1|];;
- : int vect = [|0; 0; 1|]
let decale long vecteur =
concat_vect (make_vect long 0) vecteur;;
decale : int -> int vect -> int vect = <fun>
decale 2 [|1;0;1;0|];;
- : int vect = [|0; 0; 1; 0; 1; 0|]
let op longueur vecteur =
let Complement = make_vect (2*longueur) 0
and long_vecteur = vect_length vecteur in
begin
for i = 0 to (long_vecteur - 1) do
Complement.(i) <- (1-vecteur.(i))
done;
for i = long_vecteur to (2*longueur-1) do
Complement.(i) <- 1
done;
plus [|1|] Complement
end;;
op : int -> int vect -> int vect = <fun>
Caml 4.2.10 – Fonctions en amont de l’algorithme de Karatsuba
© MPSI–Joly–Lycée Cézanne–2016-2017
78
Chapitre 4 : Méthodes de programmation
4.3. Programmation dynamique
type tache = {valeur : int; debut : int; fin : int};;
Type tache defined.
let p tache_vect =
let long = vect_length tache_vect
in
let P = make_vect (long+1) 0
in
for j = 1 to (long+1) do
let res = ref 0 in
for i = 1 to j-1 do
if tache_vect.(j-1).debut >= tache_vect.(i).fin
then res := i
done;
P.(j) <- !res;
done;
P;;
p : tache vect -> int vect = <fun>
Caml 4.3.1 – Le type tache, la fonction p
let ordonnancement tache_vect =
let long = vect_length tache_vect
and P = p tache_vect
in let Opt = make_vect (long+1) 0
in memoisation long
where
rec memoisation j =
if j = 0 then 0
else
begin
if Opt.(j) = 0
then
begin
Opt.(j) <- max (tache_vect.(j-1).valeur
+ (memoisation P.(j))) (memoisation (j-1));
Opt.(j);
end
else Opt.(j)
end;;
ordonnancement : tache vect -> int = <fun>
Caml 4.3.2 – La fonction ordonnancement
© MPSI–Joly–Lycée Cézanne–2016-2017
79
4.3. Programmation dynamique
Chapitre 4 : Méthodes de programmation
let tache1 = {valeur = 2; debut = 1 ; fin =4};;
tache1 : tache = valeur = 2; debut = 1; fin = 4
let tache2 = {valeur = 3; debut = 3; fin = 5};;
tache2 : tache = valeur = 3; debut = 3; fin = 5
let tache3 = {valeur = 2; debut = 0; fin = 6};;
tache3 : tache = valeur = 2; debut = 0; fin = 6
let tache4 = {valeur = 4; debut = 4; fin = 7};;
tache4 : tache = valeur = 4; debut = 4; fin = 7
let tache5 = {valeur = 2; debut = 3; fin = 8};;
tache5 : tache = valeur = 2; debut = 3; fin = 8
let tache6 = {valeur = 3; debut = 5; fin = 9};;
tache6 : tache = valeur = 3; debut = 5; fin = 9
let tache7 = {valeur = 3; debut = 6; fin = 10};;
tache7 : tache = valeur = 3; debut = 6; fin = 10
let tache8 = {valeur = 3; debut = 8; fin = 11};;
tache8 : tache = valeur = 3; debut = 8; fin = 11
let tache_vect_ex = [|tache1;tache2;tache3;tache4;tache5;tache6;tache7;tache8|];;
tache_vect_ex : tache vect =
[|valeur = 2; debut = 1; fin = 4; valeur = 3; debut = 3; fin = 5;
valeur = 2; debut = 0; fin = 6; valeur = 4; debut = 4; fin = 7;
valeur = 2; debut = 3; fin = 8; valeur = 3; debut = 5; fin = 9;
valeur = 3; debut = 6; fin = 10; valeur = 3; debut = 8; fin = 11|]
p tache_vect_ex;;
- : int vect = [|0; 0; 0; 0; 1; 0; 2; 3; 5|]
ordonnancement tache_vect_ex;;
- : int = 9
Caml 4.3.3 – La fonction ordonnancement, exemple d’utilisation
let ordo_bottom_up tache_vect =
let long = vect_length tache_vect
and P = p tache_vect
in
let Opt = make_vect (long+1) 0
in
for j = 1 to long do
Opt.(j) <- max (tache_vect.(j-1).valeur + Opt.(P.(j))) Opt.(j-1);
done;
Opt.(long);;
ordo_bottom_up : tache vect -> int = <fun>
Caml 4.3.4 – La fonction ordonnancement, programmation bottom-up
© MPSI–Joly–Lycée Cézanne–2016-2017
80
Chapitre 4 : Méthodes de programmation
4.3. Programmation dynamique
let ordo_bottom_up_vect tache_vect =
let long = vect_length tache_vect
and P = p tache_vect
in
let Opt = make_vect (long+1) 0
in
for j = 1 to long do
Opt.(j) <- max (tache_vect.(j-1).valeur + Opt.(P.(j))) Opt.(j-1);
done;
(Opt,P,long);;
ordo_bottom_up_vect : tache vect -> int vect * int vect * int = <fun>
let backtrack tache_vect =
let (Opt,P,long) = ordo_bottom_up_vect tache_vect
in chercheSol long
where rec chercheSol j =
if j=0 then []
else
begin
if ((tache_vect.(j-1).valeur+Opt.(P.(j)))>= Opt.(j-1))
then j::(chercheSol P.(j))
else chercheSol (j-1)
end;;
backtrack : tache vect -> int list = <fun>
backtrack tache_vect_ex;;
- : int list = [8; 4; 1]
Caml 4.3.5 – Recherche d’une solution optimale
© MPSI–Joly–Lycée Cézanne–2016-2017
81
4.3. Programmation dynamique
Chapitre 4 : Méthodes de programmation
let distanceEdition a b =
let longa = string_length a
and longb = string_length b
in let D = make_matrix (longa+1) (longb+1) 0
in memoisation longa longb
where
rec memoisation i j =
if i = 0
then begin D.(0).(j) <- j; j end
else
if j = 0
then begin D.(i).(0) <- i; i end
else
begin
let d1 = 1 + (memoisation (i-1) j)
and d2 = 1 + (memoisation i (j-1))
and d3 = (memoisation (i-1) (j-1)) +
(if a.[i-1] = b.[j-1] then 0 else 1)
in let d = (min d1 (min d2 d3));
in
D.(i).(j) <- d;
d;
end;;
distanceEdition : string -> string -> int = <fun>
Caml 4.3.6 – Distance d’édition
let distEditBottomUp a b =
let longa = string_length a
and longb = string_length b
in let D = make_matrix (longa+1) (longb+1) 0
in
(* cas limites *)
for i = 0 to longa do
D.(i).(0) <- i;
done;
for j = 0 to longb do
D.(0).(j) <- j;
done;
(* cas recursifs *)
for i = 1 to longa do
for j = 1 to longb do
let d1 = 1 + D.(i-1).(j)
and d2 = 1 + D.(i).(j-1)
and d3 = D.(i-1).(j-1)+(if a.[i-1]=b.[j-1] then 0 else 1)
in
D.(i).(j) <- (min d1 (min d2 d3));
done;
done;
D.(longa).(longb);;
distEditBottomUp : string -> string -> int = <fun>
Caml 4.3.7 – Distance d’édition
© MPSI–Joly–Lycée Cézanne–2016-2017
82
Chapitre 5
Algorithmes, analyse
“
It’s always seemed like a big mystery how nature, seemingly so effortlessly,
manages to produce so much that seems to us so complex. Well, I think we found
its secret. It’s just sampling what’s out there in the computational universe.
”
Stephen Wolfram,
5.1
5.1.1
Complexité, introduction
Premières idées
Une fois que la correction et la terminaison d’un algorithme A sont démontrées, il est légitime de
se poser deux questions au sujet de A :
• A se termine-t-il en un temps raisonnable ?
• La quantité de mémoire nécessaire à l’exécution de A est-il compatible avec les ressources
dont on dispose ?
La première question est relative au problème de complexité temporelle alors que la seconde est
relative au problème de complexité spatiale.
Nous nous intéresserons surtout à la complexité temporelle déjà abordée dans le cours d’informatique pour tous. Il faut retenir évidemment que les deux formes de complexités sont liées : un
algorithme qui réalise beaucoup d’affectations utilise des ressources en temps et en espace.
Nous ne reviendrons pas ici sur les la façon de définir la complexité en temps, il faut retenir
qu’il ne s’agit pas de chronométrer un programme mais d’évaluer un ordre de grandeur du temps
de réalisation de ce programme en le modélisant par un algorithme.
Le temps nécessaire au calcul d’une fonction dépend généralement de la taille des données
d’entrée, mais pas seulement. Ainsi est-il facile de tester si 243112609 est premier ou non (la réponse
est immédiate), il est beaucoup plus long de répondre à la même question au sujet de 243112609 − 1.
Il est possible de définir pour la plupart des algorithmes un entier qui représente la taille des
données d’entrée. La taille n des données peut représenter la taille d’un tableau ou d’une liste, le
nombre de bits nécessaires à la représentation d’un nombre entier qui est l’argument de la fonction
(dans le cas d’un test de primalité ou d’une factorisation).
On conçoit bien que la taille des données d’entrée ne préjuge pas toujours de la complexité d’un
algorithme. Certains algorithmes de tri sont par exemple très efficaces si le tableau à trier l’est
déjà et ce, quelle que soit sa longueur !
En notant Dn l’ensemble des données de taille n, on distingue donc trois types de complexités.
En notant C(d) le coût de l’algorithme pour des données d ∈ Dn , on a la définition suivante :
83
5.1. Complexité, introduction
Chapitre 5 : Algorithmes, analyse
Définition 5.1.1 (Complexités)
On distingue trois types de complexité :
• Complexité dans le pire des cas :
def
Cmax (n)(n) = max C(d)
d∈Dn
• Complexité dans le meilleur des cas :
def
Cmin (n)(n) = min C(d)
d∈Dn
• Complexité en moyenne :
def
Cmoy (n) =
X
p(d)C(d)
d∈Dn
où p est la loi de probabilité associée à l’apparition des données de taille n.
Remarque.
Les différents types de complexité ont chacun de l’intérêt. La complexité en moyenne est intéressante pour des tâches répétitives et non critiques (par exemple trier tous les jours par ordre
alphabétique les élèves qui déjeune au lycée). La complexité dans le pire des cas est essentielle
pour des algorithmes de maintenance (celui qui gère par exemple la réaction nucléaire dans une
centrale). La complexité dans le meilleur des cas donne une borne inférieure qui peut étre essentielle
(si dans le meilleur des cas un algorithme "casse" un code en un temps très long, le code est "bon")
Dans la cours d’informatique pour tous on utilise la notation de Landau O pour exprimer
qu’une suite est dominée par une autre. Nous utiliserons aussi la notation Θ qui exprime le fait
que deux suites ont le même ordre de grandeur :
Définition 5.1.2 (même ordre de grandeur)
On considère (un )n∈N et (vn )n∈N deux suites réelles. On dit que (un )n∈N a le même ordre
de grandeur que (vn )n∈N lorsque :
un = O(vn )
∧
vn = O(un )
si c’est le cas, on note un = Θ(vn ).
Remarque.
Il est clair que si un = Θ(vn ) alors vn = Θ(un ). Il faut se souvenir que les suites qui nous intéressent
ici sont à valeurs positives.
On distingue plusieurs classes de complexité :
Définition 5.1.3 (Classes de complexité)
On dit qu’un algorithme est :
• logarithmique si Cn = Θ (log2 (n)).
• linéaire si Cn = Θ(n).
• quasi-linéaire si Cn = Θ (n log n).
• quadratique si Cn = Θ n2 .
• polynomial si Cn = Θ nk , où k est un nombre entier naturel non-nul.
• exponentiel si Cn = an où a est un nombre réel strictement supérieur à 1.
© MPSI–Joly–Lycée Cézanne–2016-2017
84
Chapitre 5 : Algorithmes, analyse
5.1.2
5.1. Complexité, introduction
Deux exemples
Recherche d’un élément dans un tableau
On considère l’algorithme Caml 5.1.1 de recherche d’un élément dans un tableau.
let cherche x t = let i = ref 0 and trouve = ref false in
while (!i < vect_length t && not !trouve) do
trouve := t.(!i) = x ; i := !i +1
done ;
!trouve;;
cherche : 'a -> 'a vect -> bool = <fun>
cherche 3 [| 5; 6; 9|];; ? : bool = false
Caml 5.1.1 – Recherche d’un élément dans un tableau
n est ici la taille du tableau t. Pour calculer la complexité de cherche, il s’agit de compter le
nombre de comparaisons x = t.(!i). Dans le meilleur des cas, x est le premier élément du tableau
et il n’y a qu’une comparaison, dans le pire des cas, tous les éléments du tableau sont différents de
x (ou seul le dernier est égal à x) et il y a n comparaisons.
Les complexités dans le meilleur et le pire des cas sont donc respectivement 1 et n.
Pour calculer la complexité en moyenne, il faut faire des hypothèses raisonnables :
On suppose que les éléments du tableau sont tous distincts et que ces éléments ont tous la même
probabilité p d’apparaître. Les positions des éléments sont supposées équiprobables. Pour trouver
x en position i, il faut effectuer i comparaisons. Si x n’est pas présent, on effectue n comparaisons
(comme dans le cas où x est en position n). La complexité en moyenne de l’algorithme est donc :
Cmoy (n) = (1 − p) · |{z}
n + p ·
|{z}
| {z }
nombre de comparaisons
x apparaît à la place i
x apparaît
comparaisons
x n’apparaît pas
n
X
1
· i
n |{z}
i=1 |{z}
Soit encore :
Cmoy (n) = (1 − p) · n + p ·
1 n(n + 1)
(n + 1)
2−p
p
= (1 − p) · n + p ·
=
n + = Θ (n)
n
2
2
2
2
La complexité de l’algorithme est donc en moyenne comme dans le pire des cas linéaire.
Algorithme de Horner
Le problème consiste ici à évaluer la valeur d’un polynôme P =
n−1
X
ak · X k en un nombre réel x0
k=0
donné. Le principe de l’algorithme de Horner s’appuie sur la remarque suivante :
P (x0 ) = a0 + x0 (a1 + x0 (a2 + · · · + x0 (an−2 + x0 an−1 ) · · · ))
Dans l’algorithme Caml 5.1.2, les polynômes sont représentés par le tableau de leurs coefficients.
Si on décide de ne compter que les produits dans la complexité de l’algorithme, pour un polynôme de degré n − 1, on effectue exactement n multiplications.
5.1.3
Résultats théoriques
Les résultats mathématiques suivants sont très utiles en théorie de la complexité.
© MPSI–Joly–Lycée Cézanne–2016-2017
85
5.2. Complexité, cas des algorithmes diviser pour régner
Chapitre 5 : Algorithmes, analyse
let horner P x =
let n = vect_length P in
let res = ref 0. in
for k = n-1 downto 0 do
res := !res *. x +. P.(k);
done;
!res;;
Caml 5.1.2 – Algorithme de Horner
Proposition 5.1.1 (sommation des relations de comparaison)
On considère q, α et β trois nombres réels tels que q > 1, α > 0 et β > 0. Alors :
n
X
k
q =Θ q
n+1
,
k=0
n
X
α
k =Θ n
α+1
n
X
,
k=0
β
β
k α (ln(k)) = Θ nα+1 (ln(n))
k=0
Exercice 5.1.1
Démontrer les résultats de la proposition 5.1.1.
5.2
5.2.1
Complexité, cas des algorithmes diviser pour régner
Un petit point technique utile
On rappelle que pour tout nombre réel x, les nombres entiers bxc et dxe sont les seuls éléments de
Z qui vérifient :
bxc 6 x < bxc + 1,
dxe − 1 < x 6 dxe
Pour tout nombre entier n, on a :
• Si n est pair :
jnk
2
=
lnm
2
=
n
2
• Si n est impair :
jnk
2
Dans tous les cas :
=
jnk
2
5.2.2
n−1
,
2
+
lnm
2
lnm
2
=
n+1
2
=n
Retour sur la stratégie
La stratégie diviser pour régner consiste schématiquement, pour un problème de taille n, à :
• Partager les données en deux parties de tailles approximativement égales.
• Traiter séparément une ou les deux parties.
• Fusionner les résultats
Supposons que dans un algorithme diviser pour régner, le coût du travail de partage / fusion
soit linéaire de la forme α · n (α nombre réel strictement positif) et qu’il faille traiter récursivement
© MPSI–Joly–Lycée Cézanne–2016-2017
86
Chapitre 5 : Algorithmes, analyse
5.2. Complexité, cas des algorithmes diviser pour régner
les deux sous-problèmes de taille n2 et n2 . On suppose par ailleurs que le coût d’une donnée de
taille 1 vaut 0. En notant T (n) le coût d’une donnée de taille n, on a :
j n k
l n m
T (n) = T
+T
+α·n
(5.1)
2
2
Si n est une puissance de 2, on peut considérer p tel que n = 2p , en notant up le terme général
def
de la suite définie par : up = T (2p ), on a donc :
up = 2 · up−1 + α · 2p
(5.2)
Le calcul des premières valeurs donne :
u0 = 0,
u1 = 2α,
u2 = 8α,
u3 = 24α,
u4 = 64α,
u5 = 160α
On démontre par récurrence sur p ∈ N que :
∀p ∈ N,
up = α · p · 2p
(5.3)
L’équation 5.1 permet de démontrer par récurrence forte que la suite de terme général T (n) est
def
croissante. En posant p = dlog2 (n)e, on a :
2p−1 < n 6 2p
(5.4)
On a donc :
α · (p − 1) · 2p−1 < T (n) 6
{z
}
|
=α·2p−1
log2
(2p−1 )
α · p · 2p
| {z }
=α·2p
log2
(5.5)
(2p )
2p
α · p · 2p
=
et 2 <
α · (p − 1) · 2p−1
p−1
On peut en déduire, à partir des inégalités 5.5 que :
Par ailleurs, pour tout nombre entier p > 1 on a
2p
p−1
T (n) = Θ (n log2 (n))
< 4.
(5.6)
Remarque.
Le résultat précédent illustre de façon relativement simple les raisonnements mis en place pour les
calculs de complexités concernant les algorithmes récursifs du type diviser pour régner. Il montre
par ailleurs que des fonctions de partage et de fusion en temps linéaire sont très intéressants puisqu’elles conduisent à un algorithme quasi-linéaire.
5.2.3
Cas plus général
Si l’on reprend le schéma général d’une stratégie diviser pour régner, on est conduit à :
• Partager, diviser, le problème en deux sous-problèmes de tailles n2 et n2 pour un coût de
partage f1 (n).
• Traiter récursivement les deux sous-problèmes (ou un des deux sous-problèmes dans certains
cas).
• Fusionner les deux résultats pour un coût f2 (n).
On est donc conduit à la relation de récurrence :
j n k
l n m
T (n) = T
+T
+ f1 (n) + f2 (n)
2
2
En notant f (n) à la place de f1 (n) + f2 (n), on est conduit au résultat suivant :
Proposition 5.2.1 (Complexité d’un algorithme diviser pour régner)
Avec les notations précédentes, on a :
• Si f (n) = O nβ , où β ∈]0, 1[, alors T (n) = Θ(n).
• Si f (n) = Θ (n), alors T (n) = Θ (n log(n)).
• Si f (n) = Θ nβ , où β ∈]1, +∞[, alors T (n) = Θ nβ .
© MPSI–Joly–Lycée Cézanne–2016-2017
87
(5.7)
5.3. Exemples de complexité d’algorithmes diviser pour régnerChapitre 5 : Algorithmes, analyse
Preuve. Le deuxième point a été démontré dans la section 5.2.2. Les deux autres serons démontrés
en exercice ou dans un devoir.
Remarque.
On peut retenir que :
• si le coût total de la division et de la fusion d’un problème de taille n est négligeable devant
n, alors l’algorithme diviser pour régner est de complexité linéaire.
• si le coût total de la division et de la fusion d’un problème de taille n est linéaire, alors
l’algorithme diviser pour régner est de complexité quasi-linéaire.
• si le coût total de la division et de la fusion d’un problème de taille n est plus que linéaire, alors
l’algorithme diviser pour régner a la même complexité que les opérations de séparation/fusion.
Les opérations de séparation/fusion guident la complexité de l’algorithme diviser pour régner.
Tous les algorithmes diviser pour régner ne traitent pas les deux sous-problèmes de façon
symétrique (il suffit de penser par exemple à l’algorithme de recherche dichotomique). Le résultat
suivant est plus général :
Proposition 5.2.2 (Complexité d’un algorithme diviser pour régner, cas général)
On considère a et b deux nombres entiers naturels tels que a + b > 1, α défini par
def
α = log2 (a + b), la suite de terme général T (n) vérifiant la relation de récurrence :
j n k
l n m
T (n) = a · T
+b·T
+ f (n)
2
2
Alors :
• Si f (n) = O nβ , où β < α, alors T (n) = Θ (nα ).
• Si f (n) = Θ (nα ), alors T (n) = Θ (nα log(n)).
• Si f (n) = Θ nβ , où β > α[, alors T (n) = Θ nβ .
Preuve. La preuve sera faite en devoir ou en exercice.
5.3
5.3.1
Exemples de complexité d’algorithmes diviser pour régner
L’algorithme d’exponentiation rapide
L’algorithme d’exponentiation rapide propose le calcul de xn pour de grandes valeurs de n. La
première idée pour répondre au problème consiste évidemment à réaliser les n multiplications. On
peut aussi proposer un algorithme qui s’appuie sur la remarque suivante :
(
xn/2 · xn/2 si n est pair
xn =
x · xn/2 · xn/2 si n est impair
Ce qui conduit à l’algorithme Caml 5.3.1.
Si on considère T (n) le nombre de multiplications à réaliser pour obtenir xn par l’algorithme
Caml ??, on obtient :
j n k
+ f (n)
T (n) = T
2
où f (n) ∈ {1, 2} suivant la parité de n. Le résultat de la proposition 5.2.2 assure alors que la
complexité de l’exponentiation rapide est Θ (log(n)).
© MPSI–Joly–Lycée Cézanne–2016-2017
88
Chapitre 5 : Algorithmes, analyse5.3. Exemples de complexité d’algorithmes diviser pour régner
let rec expo_rapide x n =
match n with
| 0 -> 1.;
| 1 -> x;
| _ -> let y = expo_rapide x (n/2) in
if (n mod 2 = 0) then y *. y # une multiplication
else x *. y *. y ;; # deux multiplications
Caml 5.3.1 – Algorithme d’exponentiation rapide
Remarque.
Dans ce cas précis, on peut aussi compter le nombre de multiplications assez directement. Si n = 2p ,
on a exactement p multiplications à réaliser, c’est-é-dire log2 (n) multiplications, dans le cas général, on a Θ (log(n)) multiplications à réaliser.
La figure 5.1 illustre précisément la complexité de l’algorithme d’exponentiation rapide pour
les n ∈ [[1, 100]].
Figure 5.1 – Nombre de multiplications dans l’algorithme d’exponentiation rapide
5.3.2
L’algorithme de Knuth
Le problème consiste ici à multiplier deux polynômes P =
n
X
ak ·X k et Q =
k=0
m
X
bk ·X k , c’est-é-dire
k=0
à déterminer le polynôme P Q défini par :
PQ =
n+m
X
n
X
k=0
i=0
!
ai bk−i
Xk
(5.8)
en convenant que bj = 0 si j ∈
/ [[0, m]].
L’algorithme naïf Caml 5.3.2 s’appuie sur l’équation 5.8.
let multiplie P Q =
let degP = vect_length P - 1 and degQ = vect_length Q -1 in
let r = make_vect (degP + degQ +1) 0. in
for i = 0 to degP do
for j = 0 to degQ do
r.(i+j) <- r.(i+j) +. P.(i) *. Q.(j)
done;
done;
r;;
Caml 5.3.2 – Produit naïf de deux polynômes
Dans cet algorithme on effectue (deg P + 1) · (deg Q + 1) multiplications. Si les deux polynômes
sont de même degré n, on a donc une complexité en Θ n2 .
Le principe de l’algorithme de Knuth s’appuie sur une stratégie diviser pour régner proche de
celle de l’algorithme de Karatsuba. Il s’agit de "découper" chacun des deux polynômes en deux. Pour
l’implémentation, on suppose que chacun des deux polynômes est de degré 2p − 1, les polynômes
def
sont donc représentés par des tableaux de longueur n = 2p . On écrit alors :
P = P0 + X 2
© MPSI–Joly–Lycée Cézanne–2016-2017
p−1
Q = Q0 + X 2
P1 ,
89
p−1
Q1
5.3. Exemples de complexité d’algorithmes diviser pour régnerChapitre 5 : Algorithmes, analyse
oé, P0 , P1 , Q0 , Q1 sont des polynômes de degrés au plus 2p−1 − 1. En définissant les polynômes
R0 , R1 et R2 par :
def
R0 = P0 · Q0 ,
def
def
R1 = P1 · Q1 ,
R2 = (P0 + P1 ) · (Q0 + Q1 )
On a :
P · Q = R0 + (R2 − R0 − R1 ) · X 2n−1 + R1 · X 2n
On a donc 3 multiplications de deux polynômes de degrés au plus 2p−1 − 1 à réaliser. Si T (n)
compte le nombre de multiplications à réaliser dans le cas où les polynômes sont exactement de
degré 2k−1 − 1 à chaque étape, on a donc :
j n k
T (n) = 3 · T
2
Le résultat de la proposition 5.2.2 assure alors que la complexité de l’exponentiation rapide est
Θ nlog2 (3) . Puisque log2 (3) ' 1.58, l’algorithme de Knuth est plus satisfaisant que l’algorithme
naïf.
Remarque.
Ici on n’a pas tenu compte du coût f (n) des opérations de division/fusion. Il correspond à des
sélections dans des tableaux à des concaténations qui se font à coût constant.
Une implémentation de l’algorithme est proposée dans Caml 5.3.3
let decoupage P =
let n = vect_length P /2 in
let P0 = sub_vect P 0 n and P1 = sub_vect P n n in
(P0,P1);;
let rec knuth P Q =
let m = vect_length P in
let r = make_vect (2*m -1) 0 in
if m = 1 then r.(0) <- P.(0) *. Q.(0)
else begin
let n = m/2 in
let (P0,P1) = decoupage P and (Q0,Q1) = decoupage Q in
let P01 = make_vect n 0 and Q01 = make_vect n 0 in
for i = 0 to n-1 do
P01.(i) <- P0.(i) + P1.(i);
Q01.(i) <- Q0.(i) + Q1.(i)
done;
let R0 = knuth P0 Q0 and R1 = knuth P1 Q1 and R2 = knuth P01 Q01 in
for i = 0 to 2*n-2 do
r.(i) <- r.(i)+R0.(i);
r.(i+n) <- r.(i+n) + R1.(i) - R0.(i) - R2.(i);
r.(i+2*n) <- r.(i+2*n) + R2.(i)
done;
end;
r;;
Caml 5.3.3 – Algorithme de Knuth
5.3.3
L’Algorithme de Strassen
Le problème consiste ici à réaliser le produit de deux matrices carrées A = (ai,j )16i,j6n et B = (bi,j )16i,j6n .
Le coefficient général ci,j de la matrice produit AB est défini par :
∀(i, j) ∈ [[1, n]]2 ,
ci,j =
n
X
k=1
© MPSI–Joly–Lycée Cézanne–2016-2017
90
ai,k bk,j
Chapitre 5 : Algorithmes, analyse5.3. Exemples de complexité d’algorithmes diviser pour régner
Si on utilise naévement cette formule, pour chacun des n2 coefficients, il y a donc n multiplications à réaliser et n − 1 additions. La complexité en nombre de multiplications ou nombre
d’additions est donc Θ n3 .
L’algorithme de Strassen part de la remarque suivante faite à propos du produit de deux
matrices 2 lignes, 2 colonnes :
a b
e f
ae + bg af + bh
=
c d
g h
ce + dg cf + dh
p1 + p2 − p4 + p6
p4 + p5
=
p6 + p7
p2 − p3 + p5 − p7
où les pi , i ∈ [[1, 7]] sont définis par :
def
p1 = (b − d)(g + h),
p2 = (a + d)(e + h),
p5 = a(f − h),
p3 = (a − c)(e + f ),
p6 = d(g − e),
p4 = (a + b)h
p7 = (c + d)e
On remplace donc 8 multiplications par 7 multiplications et 4 additions par 18...
L’idée de l’algorithme de Strassen consiste à "découper" les matrices en 4 blocs de même taille.
Le calcul sur les matrices 2×2 est encore valable en remplaçant les coefficients par les blocs obtenus.
En notant M uN et AddN le nombre de multiplications et d’additions respectivement pour réaliser
le produit de deux matrices N × N , on a donc :
∀n ∈ N,
Add2n+1 = 7Add2n + 18(2n )2
M u2n+1 = 7M u2n ,
Le résultat de la proposition 5.2.2 permet à nouveau (en généralisant un peu rapidement pour tous
les entiers N ) que :
M uN = Θ (N log2 (7)) , AddN = Θ (N log2 (7))
Comme log2 (7) ' 2.81, l’algorithme de Strassen constitue une amélioration de l’algorithme naïf.
© MPSI–Joly–Lycée Cézanne–2016-2017
91
5.3. Exemples de complexité d’algorithmes diviser pour régnerChapitre 5 : Algorithmes, analyse
© MPSI–Joly–Lycée Cézanne–2016-2017
92
Liste des Figures
3.1
3.2
3.3
3.4
3.7
3.8
3.9
Une liste représentée par un tableau . . . . . . . . . . . . . . . . . . . .
Une file de trois éléments représenté par un “vecteur circulaire” . . . . .
Représentation de la même file de trois éléments par différents tableaux
Exemples d’arbres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
(a) arbre généalogique . . . . . . . . . . . . . . . . . . . . . . . . . . .
(b) arbre de probabilité . . . . . . . . . . . . . . . . . . . . . . . . . .
(c) arbre syntaxique, (a · b) + (c − d) . . . . . . . . . . . . . . . . . . .
(d) arborescence linux (partielle) de dossiers et fichiers . . . . . . . . .
Parcours en profondeur d’un arbre binaire . . . . . . . . . . . . . . . . .
Traitements, prefixe, infixe, postfixe . . . . . . . . . . . . . . . . . . . .
(a) traitement prefixe . . . . . . . . . . . . . . . . . . . . . . . . . . .
(b) traitement infixe . . . . . . . . . . . . . . . . . . . . . . . . . . . .
(c) traitement postfixe . . . . . . . . . . . . . . . . . . . . . . . . . . .
Parcours en largeur d’un arbre . . . . . . . . . . . . . . . . . . . . . . .
Parcours en largeur d’un arbre généalogique . . . . . . . . . . . . . . . .
Exemple de parcours en largeur, cas pratique : sapin . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
38
39
39
45
45
45
45
45
52
53
53
53
53
55
55
57
4.1
4.2
4.3
Principe des tâches pondérées . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Principe des tâches pondérées . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Principe de calcul d’une solution optimale . . . . . . . . . . . . . . . . . . . . . . .
69
70
71
5.1
Nombre de multiplications dans l’algorithme d’exponentiation rapide . . . . . . . .
89
3.5
3.6
93
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Liste des Algorithmes Caml
2.1.1 Un calcul simple . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.1.2 Un calcul simple qui ne fonctionne pas . . . . . . . . . . . . . . .
2.1.3 Liaison . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.1.4 Liaison statique . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.1.5 Liaisons locales . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.1.6 Liaisons locales : Attention ! . . . . . . . . . . . . . . . . . . . . .
2.1.7 Liaisons locales simultanées . . . . . . . . . . . . . . . . . . . . .
2.1.8 Liaisons locales simultanées : Attention ! . . . . . . . . . . . . . .
2.1.9 Liaisons locales simultanées dépendantes . . . . . . . . . . . . . .
2.2.1 Type unit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2.2 Fonctions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2.3 Les fonctions / et mod . . . . . . . . . . . . . . . . . . . . . . . .
2.2.4 Limitation de int . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2.5 Le type bool . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2.6 Le type char . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2.7 Le type string, concaténation . . . . . . . . . . . . . . . . . . . .
2.2.8 Le type string, aliasing . . . . . . . . . . . . . . . . . . . . . . .
2.2.9 Produits cartésiens . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2.10 Le type enregistrement . . . . . . . . . . . . . . . . . . . . . . .
2.2.11 Le type enregistrement, champ mutable . . . . . . . . . . . . .
2.2.12 Le type enregistrement, entiers de Gauss . . . . . . . . . . . . .
2.2.13 Le type enregistrement, polymorphisme . . . . . . . . . . . . .
2.2.14 Le type somme . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2.15 Constructeur . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2.16 Type et homonymie . . . . . . . . . . . . . . . . . . . . . . . . . .
2.3.1 Trois façons de définir une fonction en Caml . . . . . . . . . . . .
2.3.2 La définition de la fonction fournit un typage non ambigu . . . .
2.3.3 Des cas plus élaborés, des fonctions qui donnent des fonctions... .
2.3.4 Des fonctions sur des types définis par l’utilisateur . . . . . . . .
2.3.5 Une fonction qui ne présente pas beaucoup d’intérêt a priori . . .
2.3.6 Comment lever l’ambiguïté de typage ? . . . . . . . . . . . . . . .
2.3.7 Importance du parenthésage . . . . . . . . . . . . . . . . . . . . .
2.3.8 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.4.1 Premier exemple de filtrage . . . . . . . . . . . . . . . . . . . . .
2.4.2 Deuxième exemple de définition d’un filtrage . . . . . . . . . . . .
2.4.3 Filtrage sur un couple . . . . . . . . . . . . . . . . . . . . . . . .
2.4.4 Les messages d’alerte qui suivent les erreurs de filtrage classiques
2.5.1 Quelques exemples de tests conditionnels . . . . . . . . . . . . . .
2.5.2 Une boucle for élémentaire . . . . . . . . . . . . . . . . . . . . .
2.5.3 On peut décrémenter dans une boucle for... . . . . . . . . . . . .
2.5.4 Utilisation de référence . . . . . . . . . . . . . . . . . . . . . . .
2.5.5 Définition d’un compteur . . . . . . . . . . . . . . . . . . . . . . .
2.5.6 Définition d’une fonction puissance à l’aide d’une référence . . . .
2.5.7 La suite de Fibonacci dans le style Python . . . . . . . . . . . . .
2.5.8 Un exemple élémentaire de boucle while . . . . . . . . . . . . . .
2.6.1 La fonction factorielle définie de façon récursive . . . . . . . . . .
2.6.2 La suite de Fibonacci définie de façon récursive . . . . . . . . . .
2.6.3 Définition de deux suites par récurrence croisée . . . . . . . . . .
2.6.4 Remplacement d’une boucle . . . . . . . . . . . . . . . . . . . . .
94
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
4
5
5
5
5
6
6
6
6
7
7
7
8
8
8
9
9
9
10
10
10
11
11
11
12
12
12
12
13
13
13
14
14
15
15
15
16
17
17
18
18
19
19
19
20
20
21
21
21
LISTE DES ALGORITHMES CAML
2.6.5
2.7.1
2.7.2
2.7.3
2.7.4
2.8.1
2.8.2
2.8.3
2.8.4
2.9.1
2.9.2
2.9.3
2.9.4
3.1.1
3.2.1
3.2.2
3.2.3
3.2.4
3.3.1
3.3.2
3.3.3
3.3.4
3.3.5
3.4.1
3.4.2
3.5.1
3.5.2
3.5.3
3.5.4
3.5.5
3.5.6
3.5.7
3.5.8
LISTE DES ALGORITHMES CAML
La fonction petitexpo sans boucle while ni référence . . . . . . . . . . . . . . . . .
Un exemple de récursivité non-terminale qui conduit à un dépassement de mémoire
Une version récursive terminale . . . . P
. . . . . . . . . . . . . . . . . . . . . . . . . .
n
Version récursive non-terminale de n 7→ k=1 k . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Créations de tableaux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Longueur d’un tableau, copie physique . . . . . . . . . . . . . . . . . . . . . . . . . .
Accès et modification après copie physique . . . . . . . . . . . . . . . . . . . . . . . .
Les chaînes de caractères, des tableaux un peu particuliers . . . . . . . . . . . . . . .
Quelques exemples élémentaires sur les listes . . . . . . . . . . . . . . . . . . . . . . .
La fonction rev, miroir d’une liste . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Une fonction qui compte le nombre d’occurrences d’un élément dans une liste . . . .
Une fonction qui compte le nombre d’occurrences d’un élément dans une liste, utilisation du mot clef when . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
22
23
23
24
24
25
25
25
26
27
27
28
Constructeur de tableau, exemple . . . . . . . . . . . . . . . . .
Implémentation de la structure de pile native . . . . . . . . . .
Premier exemple d’implémentation de la structure de pile . . .
Implémentation sans effet de bord . . . . . . . . . . . . . . . . .
Effets de bord pour depile et empile . . . . . . . . . . . . . .
Implémentation de la structure de pile native . . . . . . . . . .
Typefile,implémentation . . . . . . . . . . . . . . . . . . . . .
Type file, comprendre l’implémentation . . . . . . . . . . . . .
Type file, représentation par un tableau . . . . . . . . . . . .
Type file, représentation par un tableau, exemple d’utilisation
Type dictionnaire, implémentation . . . . . . . . . . . . . . .
Type dictionnaire, implémentation, fonctionnement . . . . .
Type arbre, implémentation . . . . . . . . . . . . . . . . . . . .
Type arbre, implémentation, exemple . . . . . . . . . . . . . .
Comptage de tous les sommets . . . . . . . . . . . . . . . . . .
Nombre de Nœuds . . . . . . . . . . . . . . . . . . . . . . . . .
Nombre de feuilles . . . . . . . . . . . . . . . . . . . . . . . . .
Parcours en profondeur . . . . . . . . . . . . . . . . . . . . . . .
Traitement préfixe, infixe, postfixe . . . . . . . . . . . . . . . .
Parcours en largeur, implémentation Caml . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
29
31
32
33
34
36
37
38
40
41
43
44
50
51
51
51
52
52
54
56
4.1.1 La fonction trace . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.1.2 Algorithme de Collatz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.1.3 fonction absurd . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.2.1 Le tri rapide, principe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.2.2 Le tri rapide, l’algorithme lui même . . . . . . . . . . . . . . . . . . . . . . .
4.2.3 le tri rapide, fonctionnement . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.2.4 Le tri fusion, implémentation par les listes, la fonction divise . . . . . . . .
4.2.5 Le tri fusion, implémentation par les listes, la fonction fusion . . . . . . . .
4.2.6 Le tri fusion, implémentation par les listes, la fonction tri_fusion . . . . .
4.2.7 Le tri fusion, implémentation par les tableaux, la fonction tri_fusion_vect
4.2.8 Le tri fusion, implémentation par les tableaux, la fonction fusion . . . . . .
4.2.9 Algorithme de Karatsuba, cas des nombres entiers . . . . . . . . . . . . . . .
4.2.10 Fonctions en amont de l’algorithme de Karatsuba . . . . . . . . . . . . . . .
4.3.1 Le type tache, la fonction p . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.3.2 La fonction ordonnancement . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.3.3 La fonction ordonnancement, exemple d’utilisation . . . . . . . . . . . . . .
4.3.4 La fonction ordonnancement, programmation bottom-up . . . . . . . . . . .
4.3.5 Recherche d’une solution optimale . . . . . . . . . . . . . . . . . . . . . . . .
4.3.6 Distance d’édition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.3.7 Distance d’édition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
59
60
60
73
74
74
74
75
75
75
76
77
78
79
79
80
80
81
82
82
5.1.1
5.1.2
5.3.1
5.3.2
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
85
86
89
89
Recherche d’un élément dans un tableau
Algorithme de Horner . . . . . . . . . .
Algorithme d’exponentiation rapide . . .
Produit naïf de deux polynômes . . . . .
© MPSI–Joly–Lycée Cézanne–2016-2017
.
.
.
.
95
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
28
.
.
.
.
.
.
.
.
LISTE DES ALGORITHMES CAML
LISTE DES ALGORITHMES CAML
5.3.3 Algorithme de Knuth . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
© MPSI–Joly–Lycée Cézanne–2016-2017
96
90
Téléchargement