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