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.
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).
Stack.create (): unit -> 'a Stack.t— une pile vide ;Stack.is_empty p: 'a Stack.t -> bool;Stack.push x p: 'a -> 'a Stack.t -> unit— empilexau sommet ;Stack.pop p: 'a Stack.t -> 'a— retire et renvoie le sommet (lèveStack.Emptysi 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
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).
Queue.create (): unit -> 'a Queue.t— une file vide ;Queue.is_empty f: 'a Queue.t -> bool;Queue.push x f: 'a -> 'a Queue.t -> unit— ajoutexà la fin ;Queue.pop f: 'a Queue.t -> 'a— retire et renvoie l'élément de tête (lèveQueue.Emptysi 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
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.
Une table a le type ('cle, 'valeur) Hashtbl.t.
Hashtbl.create n: int -> ('a, 'b) Hashtbl.t— une table vide (nest une taille initiale indicative) ;Hashtbl.add t cle v: ... -> 'a -> 'b -> unit— associevàcle;Hashtbl.mem t cle: ... -> 'a -> bool;Hashtbl.find t cle: ... -> 'a -> 'b— la valeur (lèveNot_foundsi absente) ;Hashtbl.find_opt t cle: ... -> 'a -> 'b option—Some vouNone;Hashtbl.remove t cle: ... -> 'a -> unit;Hashtbl.iter f t: ('a -> 'b -> unit) -> ... -> unit— appliquef 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.
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)
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'.)
É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.
Créer une table, y associer "un" -> 1 et "deux" -> 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)
É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.)
É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 ().
É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)
É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.
À 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.
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).
À 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 "" ; on pourrait préférer un string option.)
- Pile (
Stack, LIFO) et file (Queue, FIFO) : même interfacecreate/is_empty/push/pop;poprend le dernier poussé (pile) ou le premier (file). Structures mutables ;popsur vide lèveStack.Empty/Queue.Empty. - Table de hachage (
Hashtbl) : dictionnaire mutable clé valeur, accès en moyenne.create,add,mem,find(lèveNot_found),find_opt(préférable),remove,iter. - Clés uniques :
addne remplace pas (il masque). Pour mettre à jour :removepuisadd. - 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.
- [11.] Écrire
hauteur p(nombre d'éléments d'une pile) en la vidant ; pourquoi est-ce destructif ? - [12.] Écrire
vers_liste pqui renvoie la liste des éléments d'une pile, du sommet vers le fond. - [13.] Étendre l'évaluateur postfixe avec
Sub(soustraction) ; attention à l'ordre des opérandes.
Thème B — Files.
- [14.] Écrire
longueur_file f(nombre d'éléments) de façon destructive. - [15.] Écrire
de_liste lqui crée une file contenant les éléments del, dans l'ordre. - [16.] Simuler un guichet : à partir d'une
int listde durées de service, calculer le temps d'attente total cumulé en traitant les clients dans l'ordre (file).
Thème C — Tables de hachage.
- [17.] Écrire
compte_lettres s(table caractère nombre d'occurrences dans la chaînes). - [18.] Écrire
distincts lqui renvoie le nombre d'éléments distincts d'une liste, via une table-ensemble. - [19.] Écrire
anagrammes s1 s2(mêmes lettres avec mêmes effectifs) à l'aide d'une table de comptage.
Thème D — Combinaisons.
- [20.] Détecter si une
stringde'(',')','[',']'est bien parenthésée (pile, en vérifiant la correspondance des types de parenthèses). - [21.] Mémoïser une autre fonction coûteuse de votre choix (p. ex. le nombre de chemins dans une grille) avec une table.
- [22.] Discuter : pour mémoïser, pourquoi une table de hachage est-elle préférable à une liste de couples (clé, valeur) ?