Cours BUT Info

TD 2 — Piles et files

Trois exercices — la pile sur tableau, la file circulaire, et la vérification des délimiteurs — corrigés, compilés et testés, avec les traces d'exécution.

Ce TD met les mains dans le cambouis : on implémente nous-mêmes la pile et la file du chapitre 2, avec pour seul outil un tableau et quelques indices. Trois exercices :

  1. la pile — Pile<E> sur un tableau, avec la convention sommet = -1 ;
  2. la file — File<E> sur un tableau circulaire, pour réutiliser les cases libérées ;
  3. les délimiteurs — vérifier qu'une expression comme {a(b(d[d]))e} est bien parenthésée, d'abord sans pile, puis avec la pile de l'exercice 1.

Comment utiliser cette page

Cherchez d'abord sur papier ou dans votre éditeur : les corrigés sont repliés dans des accordéons. Tout le code de cette page a été compilé et exécuté — les sorties affichées sont de vraies sorties.

1. Implémenter une pile avec un tableau

Le type de données abstrait de la pile fixe le contrat :

MéthodeRôleEn cas de problème
void push(E val)ajoute au sommetpile pleine → IllegalStateException
E pop()retire et renvoie le sommetpile vide → NoSuchElementException
E peek()renvoie le sommet sans le retirerpile vide → NoSuchElementException
boolean isEmpty()la pile est-elle vide ?—
boolean isFull()la pile est-elle pleine ?—

Le sujet propose de représenter la pile par un tableau contenu plus un indice sommet, avec la convention : pile vide ⇔ sommet vaut -1, et l'élément en haut de la pile est contenu[sommet].

La démarche

  • isEmpty : il suffit de tester sommet == -1 ;
  • isFull : le tableau a tailleMax cases, d'indices 0 à tailleMax - 1 → plein quand sommet == tailleMax - 1 ;
  • push(val) : on avance d'abord le sommet (++sommet) puis on écrit — écrire puis avancer ferait déborder de une case ;
  • pop() : on lit contenu[sommet] puis on recule (sommet--) — reculer puis lire renverrait l'ancien élément !

Déroulons une pile de taille 3 (_ = case jamais écrite) :

Actionsommetcontenu
new Pile(3)-1[_, _, _]
push(5)0[5, _, _]
push(8)1[5, 8, _]
peek() → 81[5, 8, _] (rien ne bouge)
push(14)2[5, 8, 14] — isFull() vaut true
push(32)2IllegalStateException
pop() → 141[5, 8, _]
pop() → 8, pop() → 5-1[_, _, _] — isEmpty() vaut true
pop()-1NoSuchElementException

Les quatre pièges qui coûtent des points

sommet == tailleMax - 1 et pas sommet == tailleMax ; push avance avant d'écrire (++sommet) ; pop lit avant de reculer (sommet--) ; et n'oubliez jamais de lever l'exception en premier dans push, pop et peek — avant tout accès au tableau.

2. Implémenter une file avec un tableau

Pour la file (FIFO), on ajoute en queue et on retire en tête. Problème : si la tête avance vers la droite à chaque retrait, les cases de gauche deviennent inutilisables et la file « pleine » alors que le tableau est presque vide. Deux solutions : décaler les éléments (coûteux), ou rendre le tableau circulaire — c'est cette deuxième voie que propose le sujet, avec deux indices :

  • tete : où se trouve la tête de la file dans le tableau ;
  • nbElement : combien d'éléments contient la file — il dit où insérer le prochain : à l'indice (tete + nbElement) % contenu.length.
File sur tableau circulaire : après deux retraits, la tête a avancé. La prochaine insertion se fait à (tete + nbElement) % 5 = (3 + 2) % 5 = 0, en réutilisant la place libérée.

Grâce à ces deux indices, tout est direct :

MéthodeFormule
isEmpty()nbElement == 0
isFull()nbElement == contenu.length
offer(t)écrire à (tete + nbElement) % contenu.length, puis nbElement++
poll()lire contenu[tete], puis tete = (tete + 1) % contenu.length, puis nbElement--
peek()lire contenu[tete] sans rien changer

Déroulons une file de taille 3 :

ActiontetenbElementcontenu
new File(3)00[_, _, _]
offer("a"), offer("b"), offer("c")03[a, b, c] — isFull() vaut true
offer("d")03IllegalStateException
poll() → "a"12[a, b, c] (a est « oublié »)
offer("d")13[d, b, c] — l'indice (1+2)%3 = 0 réutilise la case !
toString()13[tête b, c, d queue]

Pourquoi nbElement, et pas un simple test tete == queue ?

Avec un tableau circulaire, la position d'insertion peut coïncider avec tete : la file est alors soit pleine, soit vide, et le seul indice tete ne permet pas de trancher. nbElement lève l'ambiguïté. (Variante classique : sacrifier une case et garder un écart d'au moins 1 entre les deux indices.)

3. Vérifier les délimiteurs avec une pile

Passons à l'application : une chaîne est bien délimitée si chaque délimiteur ouvrant (, [, { est fermé par le délimiteur fermant correspondant, dans le bon ordre. C'est ce que fait un compilateur pour vérifier que vos accolades s'emboîtent.

Q1 — Les cinq exemples du sujet

ChaîneVerdictPourquoi
a(b)c✔ bien délimitéela ( de l'indice 1 est fermée par )
a[b(c)}d✘le [ de l'indice 1 est fermé par } (indice 6)
{a(b[c[d))]e]}✘le [ de l'indice 6 est fermé par ) (indice 8)
{a(b(d[d]))e}✔ bien délimitéechaque ouvrant trouve son fermant dans l'ordre
(a[b]c✘la ( de l'indice 0 n'est jamais fermée

Q2 — Peut-on se contenter de compter ?

Pour un seul type de délimiteur, oui — mais avec une précaution : il ne suffit pas que le compteur final soit à zéro, il faut aussi qu'il ne devienne jamais négatif. Sinon on accepterait )( : autant d'ouvrantes que de fermantes, compteur final nul… et pourtant l'ordre est absurde.

Pour les trois types, non — et c'est un beau contre-exemple : a[b(c)}d contient deux ouvrants et deux fermants, mais associe un [ à un }. Aucun compteur ne peut voir la catégorie des délimiteurs ni leur imbrication : il faut se souvenir de l'ordre d'ouverture — exactement ce qu'une pile fait.

Deux pièges classiques

La parenthèse orpheline : à la fin du parcours, il faut tester pile.isEmpty() — sinon (a[b]c passerait (le [ est bien fermé, mais la ( traîne). La fermante seule : avant de dépiler, tester isEmpty() — sinon )( lève une NoSuchElementException au lieu de renvoyer false. Un pop() sans test préalable est presque toujours un bug.

Tester le tout dans un main

Ce qu'il faut savoir refaire sans le sujet

  • écrire Pile<E> sur tableau : convention sommet = -1, ++sommet avant d'écrire, sommet-- après avoir lu, exceptions IllegalStateException / NoSuchElementException ;
  • écrire File<E> sur tableau circulaire : (tete + nbElement) % taille pour insérer, tete = (tete + 1) % taille pour retirer, nbElement pour lever l'ambiguïté vide/plein ;
  • l'algorithme de vérification de délimiteurs : empiler les ouvrants, dépiler et comparer sur les fermants, tester la pile vide avant chaque pop et à la fin.

td2-piles-files.pdf

TD 2 — Piles et Files · Structures de données · 2 pages

Consultation seule — téléchargement désactivé

Chargement du document…

Dernière mise à jour le

Sur cette page