Lycée Victor Hugo MPSI 2016-2017 Option informatique Chapitre 2. Les listes I Introduction En informatique, une structure de données est la description d'une structure logique destinée à organiser et à agir sur des données indépendamment de la mise en ÷uvre eective de ces structures. Nous allons en étudier plusieurs, en commençant par les listes simplement chainées (ou plus simplement dans la suite de ce cours, les listes). Une liste est une collection séquentielle et de taille arbitraire de données de même type : chaque élément possède, en plus de la donnée, d'un pointeur vers l'élément suivant de la liste. On peut donc représenter une liste par la gure suivante : a0 a1 a2 ··· an ··· Néanmoins, un tel schéma est incomplet car taille arbitraire ne signie pas taille innie : il est nécessaire que notre liste se termine. Nous allons donc adjoindre à cette description un élément particulier caractérisant la terminaison de la liste, et qu'on appelle le nil (abréviation du latin nihil, autrement dit, rien). a0 a1 a2 ··· an nil Ainsi, le type de données abstrait dénissant une liste est le suivant : Liste = nil + Élément × Liste, ce qui signe qu'une liste est soit la liste vide, soit un couple constitué d'un élément et d'une liste. On notera que cette dénitation de la liste est récursive ! I.1 Mise en ÷uvre en Caml Bien que cette structure de donnée soit déjà présente en Caml , nous allons commencer par le faire nous-mêmes, pour bien comprendre ses avantages, mais aussi ses contraintes. Commençons par dénir le type polymorphe et récursif 'a liste : Code Caml 1 1 # type 'a liste = Nil | Cellule of 'a * ( ' a liste ) ;; 2 Le type liste est défini. On a traduit le type abstrait sous la forme d'un type somme. Une des valeurs possibles est la constante Nil (Noter la majuscule obligatoire). L'autre valeur possible est un couple (2-uplet) Cellule constitué d'une valeur de type 'a et d'une liste de valeurs de type 'a . Nous pouvons maintenant dénir notre première liste d'entiers, par exemple celle représentée par le schéma suivant : 3 2 1 nil Code Caml 2 1 # let lst = Cellule (3 , Cellule (2 , Cellule (1 , Nil ) ) ) ;; 2 lst : int liste = Cellule (3 , Cellule (2 , Cellule (1 , Nil ) ) ) 1 Lycée Victor Hugo MPSI 2016-2017 Option informatique Nous allons adjoindre à notre type une fonction permettant d'insérer un élément en tête de liste : Code Caml 3 1 # let construire t q = Cellule (t , q ) ;; 2 construire : 'a -> 'a liste -> 'a liste = <fun > Pour dénir la liste représentée ci-dessous, il sura donc d'écrire : 4 3 2 1 nil Code Caml 4 1 # let lst2 = construire 4 lst ;; 2 lst2 : int liste = Cellule (4 , Cellule (3 , Cellule (2 , Cellule (1 , Nil)))) Inversement, nous allons avoir besoin de récupérer les éléments d'une liste. Ceci nous amène à dénir deux fonctions supplémentaires : une fonction tete qui retourne le premier élément de la liste, et queue qui renvoie la liste pointée par la tête de liste : Code Caml 5 1 2 3 4 5 6 7 8 9 10 11 12 # let tete l = match l with | Nil -> failwith " liste vide " | Cellule (t , q ) -> t ;; tete : 'a liste -> 'a = <fun> # let queue l = match l with | Nil -> failwith " liste vide " | Cellule (t , q ) -> q ;; queue : 'a liste -> 'a liste = <fun> # tete lst2 ;; - : int = 4 # queue lst2 ;; - : int liste = Cellule (3 , Cellule (2 , Cellule (1 , Nil))) Nous n'allons pas pousser plus loin la mise en ÷uvre de notre propre type de liste puisque le type 'a list existe d'ores et déjà en Caml , mais on retiendra principalement de cette construction les observations suivantes, qui en font un type très diérents des listes python : les listes peuvent grossir dynamiquement, mais les éléments sont toujours rajoutés en tête de liste ; on ne peut accéder directement qu'à la tête de la liste. tous les éléments d'une liste sont du même type (listes homogènes) II Description des listes en Caml II.1 Construction d'une liste Les listes sont délimitées par des crochets [ et ] , les éléments (qui doivent être de même type) étant séparés par un point-virgule. Par exemple : Code Caml 6 1 2 3 4 5 6 7 8 9 10 # [4; 3; 2; 1] ;; - : int list = [4; 3; 2; 1] # [ 'a '; 'b '; 'c '] ;; - : char list = [ 'a'; 'b'; 'c'] # [4; 'a '; 3; 2; 1] ;; Entrée interactive : >[4; 'a '; 3; 2; 1] ;; > Cette expression est de type int list, mais est utilisée avec le type char list. 2 Lycée Victor Hugo MPSI 2016-2017 Option informatique Bien entendu, l'élément nil sera représenté par la liste vide [] et aura pour type 'a list . L'ajout d'un élément en tête de liste est représenté par l'opérateur inxe : : qu'on appelle conse (pour constructeur de liste). Ce constructeur est associatif à droite, ce qui permet d'éviter l'écriture de nombeuses parenthèses inutiles. Code Caml 7 1 # 5 :: [4; 3; 2; 1] ;; 2 -: int list = [5; 4; 3; 2; 1] 3 # 'a ' :: ( 'b ' :: ( 'c ' :: []) ) ;; (* version avec parenth è ses ( inutiles ) *) 4 -: char list = ['a'; 'b'; 'c'] 5 # 'a ' :: 'b ' :: 'c ' :: [] ;; (* version sans parenth è ses , é criture conseill é e *) 6 -: char list = ['a'; 'b'; 'c'] À l'inverse, les fonctions hd (head) et tl (tail) permettent d'obtenir la tête (le premier élément de la liste) et la queue (la liste privée de son premier élément) d'une liste : Code Caml 8 1 2 3 4 # # - hd [4; 3; 2; 1] ;; : int = 4 tl [4; 3; 2; 1] ;; : int list = [3; 2; 1] Notons que ces deux fonctions déclenchent une exception lorsqu'on essaye de les appliquer à la liste vide : Code Caml 9 1 # hd [] ;; 2 Exception non rattrapée : Failure "hd " II.2 Filtrage et récursivité Le motif tete :: queue est reconnu par toute liste non vide, et dans la suite de l'évaluation tete prendra la valeur de la tête et queue celle de la queue. Il nous est donc facile de redénir à titre d'exemple les fonctions hd et tl : Code Caml 10 1 2 3 4 5 6 7 8 9 10 # let head liste = match liste with | [] -> failwith " liste vide " | tete :: queue -> tete ;; head : 'a list -> 'a = <fun> # let tail liste = match liste with | [] -> failwith " liste vide " | tete :: queue -> queue ;; tail : 'a list -> 'a list = <fun> Associé à la récursivité, nous avons là le mode principal de dénition d'une fonction agissant sur les listes. À titre d'exemple, on peut proposer les instructions suivantes. 3 Lycée Victor Hugo MPSI 2016-2017 Le calcul de la longueur d'une liste Option informatique : Code Caml 11 1 # let rec longueur liste = 2 match liste with 3 | [] -> 0 4 | tete :: queue -> 1 + longueur queue ;; Caml dispose déjà de l'instruction correspondante list_length. L'obtention du dernier élément d'une liste Code Caml 12 1 # let rec dernier liste = 2 match liste with 3 | [] -> failwith " liste vide " 4 | [ a ] -> a 5 | _ :: queue -> dernier queue ;; Notons que le motif [a] est équivalent au motif a :: [] . Notons également que nous avons ici deux cas de base ! Si la liste est vide il faut lever une exception. Si la liste n'a qu'un seul élément, c'est forcément le dernier. Le test d'appartenance à une liste Code Caml 13 1 # let rec membre x liste = 2 match liste with 3 | [] -> false 4 | tete :: _ when tete = x -> true 5 | _ :: queue -> membre x queue ;; Caml dispose déjà de l'instruction correspondante mem . Notons que le principe de l'évaluation paresseuse permet d'écrire de manière équivalente : Code Caml 14 1 # let rec membre x liste = 2 match liste with 3 | [] -> false 4 | tete :: queue -> ( tete = x ) || membre x queue ;; Je rappelle que si vous décidez de programmer en utilisant vous avez tout intérêt à le signaler au correcteur... L'obtention du ne élément d'une liste en conscience l'évaluation paresseuse, La programmation est un peu plus subtile... et fait appel a un ltrage sur les deux paramètres d'entrée. Rappelons qu'on n'a pas accès à la taille de la liste, et qu'on n'a pas non plus de boucle for en programmation fonctionnelle ! L'idée de la récurrence est que le n-ième élément d'une liste est le (n - 1)-ième élément de la queue de cette liste. Les cas de base sont les suivants : si on demande un élément d'une liste vide il y a bien sûr erreur ; si on demande le premier élément il est facile de le rendre avec la fonction hd (Par homogénéité avec les us informatiques habituels on numérote à partir de 0, le premier élément de la liste est le 0-ième) 4 Lycée Victor Hugo MPSI 2016-2017 Option informatique Code Caml 15 1 # let rec nieme n liste = 2 match n , liste with 3 | _ , [] -> failwith " Liste trop courte " 4 | 0 , x :: _ -> x 5 | _ , _ :: queue -> nieme ( n - 1) queue ;; La concaténation de deux listes Code Caml 16 1 # let rec concat lst1 lst2 = 2 match lst1 with 3 | [] -> lst2 4 | tete :: queue -> tete :: ( concat queue lst2 ) ;; On notera l'expressivité de cette programmation récursive... Caml dispose de l'instruction @ qui est la forme inxe (i.e. que l'opérateur se met opérandes) de cette fonction : entre les deux Code Caml 17 1 # [1; 2; 3] @ [4; 5; 6] ;; 2 - : int list = [1 ;2; 3; 4; 5; 6] Attention, mis à part les fonctions hd et tl , aucune de ces fonctions ci-dessus n'a un coût temporel constant : si n désigne la longueur de la liste, la fonction list_length et la fonction mem ont une complexité en O(n). Quand à la concaténation lst1 @ lst2 , son coût est proportionnel à la longueur de la liste lst1 . III Fonctionnelles agissant sur les listes III.1 Les fonctions map et do_list Étant données une fonction f de type 'a -> 'b et une liste [a0 ; · · · ; an−1 ] de type 'a list , la fonctionnelle map a pour objet de créer la liste [f (a0 ); · · · ; f (an−1 )] . Sa dénition est la suivante : Code Caml 18 1 # let rec map f liste = 2 match liste with 3 | [] -> [] 4 | tete :: queue -> ( f tete ) :: ( map f queue ) ;; Son type est ('a -> 'b) -> 'a list -> 'b list ; notons que cette fonction est prédénie en Caml . Code Caml 19 1 # map string_length [ " alpha " ; " beta " ; " gamma " ; " delta " ] ;; 2 -: int list = [5; 4; 5; 5] Étant données une fonction f de type 'a -> unit et une liste [a0 ; · · · ; an−1 ] de type 'a list , la fonctionnelle do_list a pour objet d'eectuer la séquence (notion que nous reverrons plus tard, plutôt dans le contexte de la programme impérative) f (a0 ); f (a1 ); · · · ; f (an−1 ) (ce qui n'a d'intérêt que si f a un eet sur l'environnement). Sa dénition est la suivante : 5 Lycée Victor Hugo MPSI 2016-2017 Option informatique Code Caml 20 1 # let rec do_list f liste = function 2 | [] -> () 3 | tete :: queue -> f tete ; do_list f queue ;; Son type est ('a -> 'b) -> 'a list -> unit ; cette fonction est elle aussi prédénie en Caml . Code Caml 21 1 # do_list print_string [ " alpha " ; " beta " ; " gamma " ; " delta " ] ;; 2 alphabetagammadelta-: unit = () 6