Adloun

Piles, files et tables de hachage

Cours complet · OCaml (option informatique), chapitre 8 · prépas MPSI et MP, option informatique

Travailler ce chapitre sur Adloun Exercices corrigés de ce chapitre

<i class="fa-solid fa-compass mr-2" style="color:#9A563B"></i>8.1 Introduction et motivation

On a souvent besoin de structures mutables prêtes à l'emploi pour stocker des éléments et les retrouver selon une discipline précise : « le dernier arrivé d'abord » (une pile), « le premier arrivé d'abord » (une file), ou « par clé » (une table de hachage). OCaml les fournit dans sa bibliothèque standard, via les modules Stack, Queue et Hashtbl. Ce chapitre en présente l'interface — la dernière brique du sous-ensemble du programme.

iRemarqueNiveau A.2

Ces modules sont utilisables après rappel ; on en donne les signatures. On pourrait réimplémenter une pile à la main (un enregistrement à champ mutable contenu : 'a list, cf. chapitre 7) ; l'intérêt des modules est de fournir ces structures efficaces et testées, sans réinventer la roue.

8.2 Les piles : dernier entré, premier sorti (LIFO)

Une pile (stack) fonctionne comme une pile d'assiettes : on ajoute et on retire par le même bout (le sommet). Le dernier élément empilé est le premier dépilé — discipline LIFO (Last In, First Out).

iRemarqueRappel du module `Stack` (A.2)
  • Stack.create () : unit -&gt; 'a Stack.t — une pile vide ;
  • Stack.is_empty p : 'a Stack.t -&gt; bool ;
  • Stack.push x p : 'a -&gt; 'a Stack.t -&gt; unit — empile x au sommet ;
  • Stack.pop p : 'a Stack.t -&gt; 'a — retire et renvoie le sommet (lève Stack.Empty si vide).

# let p = Stack.create ();;
# Stack.push 1 p; Stack.push 2 p; Stack.push 3 p;;
- : unit = ()
# Stack.pop p;;
- : int = 3
# Stack.pop p;;
- : int = 2
Attention

La pile est une structure mutable : Stack.push et Stack.pop modifient la pile sur place (et renvoient () pour push, l'élément pour pop). Dépiler une pile vide lève l'exception Stack.Empty : on teste Stack.is_empty avant, ou l'on sait par construction que la pile n'est pas vide.

8.3 Les files : premier entré, premier sorti (FIFO)

Une file (queue) fonctionne comme une file d'attente : on ajoute par un bout (la fin) et on retire par l'autre (le début). Le premier arrivé est le premier servi — discipline FIFO (First In, First Out).

iRemarqueRappel du module `Queue` (A.2)
  • Queue.create () : unit -&gt; 'a Queue.t — une file vide ;
  • Queue.is_empty f : 'a Queue.t -&gt; bool ;
  • Queue.push x f : 'a -&gt; 'a Queue.t -&gt; unit — ajoute x à la fin ;
  • Queue.pop f : 'a Queue.t -&gt; 'a — retire et renvoie l'élément de tête (lève Queue.Empty si vide).

# let f = Queue.create ();;
# Queue.push 1 f; Queue.push 2 f; Queue.push 3 f;;
- : unit = ()
# Queue.pop f;;
- : int = 1
# Queue.pop f;;
- : int = 2
ImportantMême `push`, `pop` différent

Piles et files ont la même interface (create, is_empty, push, pop) ; seule diffère la discipline de sortie. pop d'une pile rend le dernier poussé (le sommet) ; pop d'une file rend le premier poussé (la tête). On choisit la pile pour revenir sur ses pas (parenthésage, retour arrière), la file pour traiter dans l'ordre d'arrivée (parcours en largeur).

8.4 Les tables de hachage : dictionnaires mutables

Une table de hachage (Hashtbl) associe des clés à des valeurs, avec un accès très rapide en moyenne. C'est le dictionnaire mutable d'OCaml — l'équivalent du dict de Python.

iRemarqueRappel du module `Hashtbl` (A.2)

Une table a le type ('cle, 'valeur) Hashtbl.t.

  • Hashtbl.create n : int -&gt; ('a, 'b) Hashtbl.t — une table vide (n est une taille initiale indicative) ;
  • Hashtbl.add t cle v : ... -&gt; 'a -&gt; 'b -&gt; unit — associe v à cle ;
  • Hashtbl.mem t cle : ... -&gt; 'a -&gt; bool ;
  • Hashtbl.find t cle : ... -&gt; 'a -&gt; 'b — la valeur (lève Not_found si absente) ;
  • Hashtbl.find_opt t cle : ... -&gt; 'a -&gt; 'b option — Some v ou None ;
  • Hashtbl.remove t cle : ... -&gt; 'a -&gt; unit ;
  • Hashtbl.iter f t : ('a -&gt; 'b -&gt; unit) -&gt; ... -&gt; unit — applique f cle valeur à chaque liaison.

# let t = Hashtbl.create 16;;
# Hashtbl.add t "Alice" 17; Hashtbl.add t "Bob" 12;;
- : unit = ()
# Hashtbl.find_opt t "Alice";;
- : int option = Some 17
# Hashtbl.mem t "Eve";;
- : bool = false

Complexité : Coût et usage

L'accès (add, mem, find) est en en moyenne, dans le pire cas. C'est ce qui rend la table idéale pour compter, mémoriser ou retrouver par clé, là où une liste imposerait un parcours linéaire.

ImportantClés uniques : `add` ne remplace pas

On utilise la table sans liaison multiple : une clé porte une valeur. Attention, Hashtbl.add t cle v sur une clé déjà présente n'écrase pas l'ancienne valeur — elle la masque (créant une seconde liaison). Comme la fonction de remplacement n'est pas au programme, pour mettre à jour la valeur d'une clé on retire puis rajoute :


Hashtbl.remove t cle;        (* enlève l'ancienne liaison *)
Hashtbl.add t cle nouvelle   (* en met une neuve *)

On garde ainsi, à tout instant, une seule valeur par clé.

<i class="fa-solid fa-dumbbell mr-2" style="color:#2E7559"></i>8.5 Exercices résolus

Niveau (application directe du cours)

Exercice 1 : Ordre de sortie d'une pile

On exécute Stack.push 'a' p, Stack.push 'b' p, Stack.push 'c' p, puis trois Stack.pop p. Dans quel ordre sortent les caractères ?

Démonstration

Dans l'ordre 'c', 'b', 'a' : c'est une pile (LIFO), le dernier empilé sort en premier. (Avec une file, l'ordre serait 'a', 'b', 'c'.)

Exercice 2 : Vider une file

Écrire somme_file f qui dépile entièrement une file d'entiers et renvoie la somme de ses éléments.

Démonstration

let somme_file f =
  let s = ref 0 in
  while not (Queue.is_empty f) do
    s := !s + Queue.pop f
  done;
  !s

On dépile tant que la file n'est pas vide, en accumulant. À la fin, la file est vide (effet de bord) et l'on renvoie la somme. La discipline FIFO n'importe pas ici : on visite tous les éléments.

Exercice 3 : Premières manipulations de table

Créer une table, y associer &quot;un&quot; -&gt; 1 et &quot;deux&quot; -&gt; 2, puis écrire valeur t cle renvoyant 0 si la clé est absente.

Démonstration

let valeur t cle =
  match Hashtbl.find_opt t cle with
  | None -> 0
  | Some v -> v

find_opt évite l'exception Not_found de find : on filtre None/Some v et l'on choisit une valeur par défaut pour l'absence. C'est l'usage le plus sûr.

Niveau (raisonnement intermédiaire)

Exercice 4 : Parenthésage équilibré

Écrire equilibre s qui teste si une chaîne de '(' et ')' est correctement parenthésée, à l'aide d'une pile.

Démonstration

let equilibre s =
  let p = Stack.create () in
  let ok = ref true in
  for i = 0 to String.length s - 1 do
    if s.[i] = '(' then Stack.push '(' p
    else if s.[i] = ')' then
      if Stack.is_empty p then ok := false
      else ignore (Stack.pop p)
  done;
  !ok && Stack.is_empty p

Chaque '(' est empilé ; chaque ')' doit pouvoir dépiler une parenthèse ouvrante en attente — sinon il y a une fermante de trop (ok := false). À la fin, la pile doit être vide (sinon il reste des ouvrantes). (ignore jette la valeur dépilée, dont on n'a pas l'usage.)

Exercice 5 : Renverser avec une pile

Écrire renverse_file f qui renverse l'ordre des éléments d'une file (le dernier devient le premier), en s'aidant d'une pile.

Démonstration

let renverse_file f =
  let p = Stack.create () in
  while not (Queue.is_empty f) do
    Stack.push (Queue.pop f) p
  done;
  while not (Stack.is_empty p) do
    Queue.push (Stack.pop p) f
  done

On vide la file dans une pile : l'ordre s'inverse (FIFO puis LIFO). On revide la pile dans la file : les éléments y reviennent renversés. La file f est modifiée sur place ; la fonction renvoie ().

Exercice 6 : Compter les occurrences

Écrire occurrences l qui, d'une string list, renvoie une table associant à chaque mot son nombre d'apparitions.

Démonstration

let occurrences l =
  let t = Hashtbl.create 16 in
  let rec parcours l =
    match l with
    | [] -> ()
    | mot :: reste ->
        (match Hashtbl.find_opt t mot with
         | None -> Hashtbl.add t mot 1
         | Some n -> Hashtbl.remove t mot; Hashtbl.add t mot (n + 1));
        parcours reste
  in
  parcours l;
  t

Pour chaque mot, on consulte sa valeur : absent, on l'ajoute avec 1 ; présent avec n, on le retire puis rajoute avec n + 1 (pas de remplacement au programme). On maintient ainsi une liaison unique par clé.

Niveau (approfondissement)

Exercice 7 : Premier doublon

Écrire premier_doublon l qui renvoie Some x où x est le premier élément de la liste l déjà rencontré auparavant, ou None si tous sont distincts.

Démonstration

let premier_doublon l =
  let vus = Hashtbl.create 16 in
  let rec parcours l =
    match l with
    | [] -> None
    | x :: reste ->
        if Hashtbl.mem vus x then Some x
        else begin
          Hashtbl.add vus x true;
          parcours reste
        end
  in
  parcours l

La table vus joue le rôle d'un ensemble : on y note chaque élément déjà vu (la valeur true importe peu, seule compte la présence de la clé). Dès qu'un élément est déjà présent, c'est le premier doublon. Coût en moyenne, contre avec une recherche par liste.

Exercice 8 : Mémoïsation de Fibonacci

À l'aide d'une table, écrire fibo en qui mémorise les valeurs déjà calculées.

Démonstration

let memo = Hashtbl.create 100

let rec fibo n =
  if n < 2 then n
  else
    match Hashtbl.find_opt memo n with
    | Some v -> v
    | None ->
        let v = fibo (n - 1) + fibo (n - 2) in
        Hashtbl.add memo n v;
        v

Avant de calculer fibo n, on regarde s'il est déjà en cache. Sinon on le calcule une fois et on l'enregistre. Chaque valeur n'est ainsi calculée qu'une fois : on passe du coût exponentiel de la version naïve (chapitre 3) à un coût linéaire. C'est la mémoïsation — la programmation dynamique appliquée par cache.

Exercice 9 : Évaluer une expression postfixe

On représente une expression en notation postfixe (polonaise inverse) par une liste de jetons :


type jeton = Nombre of int | Add | Mul

Écrire evalue qui évalue une telle liste à l'aide d'une pile. Exemple : [Nombre 2; Nombre 3; Nombre 4; Mul; Add] vaut .

Démonstration

let evalue jetons =
  let p = Stack.create () in
  let traite j =
    match j with
    | Nombre n -> Stack.push n p
    | Add -> let b = Stack.pop p in let a = Stack.pop p in Stack.push (a + b) p
    | Mul -> let b = Stack.pop p in let a = Stack.pop p in Stack.push (a * b) p
  in
  List.iter traite jetons;
  Stack.pop p

On empile chaque nombre ; à chaque opérateur, on dépile les deux derniers opérandes, on calcule, et on rempile le résultat. À la fin, la pile contient l'unique résultat. C'est le principe d'une calculatrice à pile — et un classique d'usage conjoint des piles (chapitre 8), des types sommes (chapitre 3) et des listes (chapitre 2).

Exercice 10 : Le mot le plus fréquent

À partir de la table d'occurrences (exercice 6), écrire plus_frequent t qui renvoie le mot de plus grand effectif, à l'aide de Hashtbl.iter.

Démonstration

let plus_frequent t =
  let meilleur = ref "" and maxi = ref 0 in
  Hashtbl.iter
    (fun mot n -> if n > !maxi then begin maxi := n; meilleur := mot end)
    t;
  !meilleur

Hashtbl.iter applique la fonction à chaque liaison (mot, n) ; on retient au passage le maximum dans deux références. L'ordre de parcours d'une table n'est pas spécifié, mais peu importe pour chercher un maximum. (Sur une table vide, la fonction renvoie &quot;&quot; ; on pourrait préférer un string option.)

Synthèse du chapitre (à retenir)
  • Pile (Stack, LIFO) et file (Queue, FIFO) : même interface create / is_empty / push / pop ; pop rend le dernier poussé (pile) ou le premier (file). Structures mutables ; pop sur vide lève Stack.Empty/Queue.Empty.
  • Table de hachage (Hashtbl) : dictionnaire mutable clé valeur, accès en moyenne. create, add, mem, find (lève Not_found), find_opt (préférable), remove, iter.
  • Clés uniques : add ne remplace pas (il masque). Pour mettre à jour : remove puis add.
  • Usages typiques : pile pour parenthésage / retour arrière / postfixe ; file pour le traitement dans l'ordre (parcours en largeur) ; table pour compter, mémoriser (mémoïsation), détecter des doublons, indexer par clé.

8.6 Exercices d'entraînement

Légende : application directe, raisonnement intermédiaire, approfondissement ; signale un classique incontournable. La numérotation prolonge celle des dix exercices résolus.

Thème A — Piles.
Thème B — Files.
Thème C — Tables de hachage.
Thème D — Combinaisons.

Continuer sur Adloun : animation, QCM, fiches, exercices