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 :
- la pile —
Pile<E>sur un tableau, avec la conventionsommet = -1; - la file —
File<E>sur un tableau circulaire, pour réutiliser les cases libérées ; - 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éthode | Rôle | En cas de problème |
|---|---|---|
void push(E val) | ajoute au sommet | pile pleine → IllegalStateException |
E pop() | retire et renvoie le sommet | pile vide → NoSuchElementException |
E peek() | renvoie le sommet sans le retirer | pile 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 testersommet == -1;isFull: le tableau atailleMaxcases, d'indices0àtailleMax - 1→ plein quandsommet == 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 litcontenu[sommet]puis on recule (sommet--) — reculer puis lire renverrait l'ancien élément !
Déroulons une pile de taille 3 (_ = case jamais écrite) :
| Action | sommet | contenu |
|---|---|---|
new Pile(3) | -1 | [_, _, _] |
push(5) | 0 | [5, _, _] |
push(8) | 1 | [5, 8, _] |
peek() → 8 | 1 | [5, 8, _] (rien ne bouge) |
push(14) | 2 | [5, 8, 14] — isFull() vaut true |
push(32) | 2 | IllegalStateException |
pop() → 14 | 1 | [5, 8, _] |
pop() → 8, pop() → 5 | -1 | [_, _, _] — isEmpty() vaut true |
pop() | -1 | NoSuchElementException |
import java.util.NoSuchElementException;
class Pile<E> {
private int tailleMax; // taille maximale de la pile
private E[] contenu; // éléments contenus dans la pile
private int sommet; // position du sommet dans le tableau
@SuppressWarnings("unchecked")
public Pile(int taille) {
this.tailleMax = taille;
this.contenu = (E[]) new Object[taille];
this.sommet = -1; // convention : la pile est vide
}
public boolean isEmpty() {
return sommet == -1;
}
public boolean isFull() {
return sommet == tailleMax - 1;
}
public void push(E val) {
if (isFull()) {
throw new IllegalStateException("Pile pleine");
}
contenu[++sommet] = val; // on avance, puis on écrit
}
public E pop() {
if (isEmpty()) {
throw new NoSuchElementException("Pile vide");
}
return contenu[sommet--]; // on lit, puis on recule
}
public E peek() {
if (isEmpty()) {
throw new NoSuchElementException("Pile vide");
}
return contenu[sommet];
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("[bas ");
for (int i = 0; i <= sommet; i++) {
sb.append(contenu[i]);
if (i < sommet) {
sb.append(", ");
}
}
return sb.append(" haut]").toString();
}
}Le tableau générique. On ne peut pas écrire new E[taille] en Java : on alloue un
Object[] et on force le type par un transtypage (E[]) new Object[taille]. Le
compilateur râle (« unchecked cast ») — d'où l'annotation @SuppressWarnings("unchecked").
C'est le passage obligé pour un conteneur générique sur tableau.
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.
Grâce à ces deux indices, tout est direct :
| Méthode | Formule |
|---|---|
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 :
| Action | tete | nbElement | contenu |
|---|---|---|---|
new File(3) | 0 | 0 | [_, _, _] |
offer("a"), offer("b"), offer("c") | 0 | 3 | [a, b, c] — isFull() vaut true |
offer("d") | 0 | 3 | IllegalStateException |
poll() → "a" | 1 | 2 | [a, b, c] (a est « oublié ») |
offer("d") | 1 | 3 | [d, b, c] — l'indice (1+2)%3 = 0 réutilise la case ! |
toString() | 1 | 3 | [tête b, c, d queue] |
import java.util.NoSuchElementException;
class File<E> {
private E[] contenu; // éléments contenus dans la file
private int tete; // position de la tête dans le tableau
private int nbElement; // nombre d'éléments dans la file
@SuppressWarnings("unchecked")
public File(int taille) {
this.contenu = (E[]) new Object[taille];
this.tete = 0;
this.nbElement = 0;
}
public boolean isEmpty() {
return nbElement == 0;
}
public boolean isFull() {
return nbElement == contenu.length;
}
public boolean offer(E t) {
if (isFull()) {
throw new IllegalStateException("File pleine");
}
contenu[(tete + nbElement) % contenu.length] = t;
nbElement++;
return true;
}
public E poll() {
if (isEmpty()) {
throw new NoSuchElementException("File vide");
}
E t = contenu[tete];
tete = (tete + 1) % contenu.length; // la tête « tourne »
nbElement--;
return t;
}
public E peek() {
if (isEmpty()) {
throw new NoSuchElementException("File vide");
}
return contenu[tete];
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("[tête ");
for (int i = 0; i < nbElement; i++) {
sb.append(contenu[(tete + i) % contenu.length]);
if (i < nbElement - 1) {
sb.append(", ");
}
}
return sb.append(" queue]").toString();
}
}Le % dans le toString aussi. Pour afficher la file « dans l'ordre », il faut
parcourir les éléments depuis tete en tournant : c'est exactement le même calcul que
pour les insertions. Si vous bouclez simplement de 0 à contenu.length - 1, vous
afficherez le tableau dans l'ordre du stockage — pas l'ordre de la file.
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îne | Verdict | Pourquoi |
|---|---|---|
a(b)c | ✔ bien délimitée | la ( 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ée | chaque 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.
public static boolean verifDelim(String s) {
int compteur = 0;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '(') {
compteur++;
} else if (c == ')') {
compteur--;
if (compteur < 0) {
return false; // une fermante de trop, avant toute ouvrante
}
}
}
return compteur == 0; // toutes les ouvertes ont été fermées
}Sur les cinq exemples (seules les parenthèses comptent ici) : a(b)c ✔,
a[b(c)}d ✔ (aucune parenthèse à part la paire correcte), {a(b[c[d))]e]} ✘,
{a(b(d[d]))e} ✔, (a[b]c ✘.
L'algorithme, à retenir car il resservira : on parcourt la chaîne ; un ouvrant est empilé ; un fermant dépile et doit correspondre au dernier ouvrant (sinon c'est raté) ; à la fin, la pile doit être vide (sinon des ouvrants n'ont jamais été fermés).
private static boolean correspond(char ouvrant, char fermant) {
return (ouvrant == '(' && fermant == ')')
|| (ouvrant == '[' && fermant == ']')
|| (ouvrant == '{' && fermant == '}');
}
public static boolean verifDelimStack(String s) {
Pile<Character> pile = new Pile<>(s.length() + 1);
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '(' || c == '[' || c == '{') {
pile.push(c); // on retient l'ouvrant
} else if (c == ')' || c == ']' || c == '}') {
if (pile.isEmpty()) {
return false; // fermante sans ouvrante
}
if (!correspond(pile.pop(), c)) {
return false; // mauvais type de fermant
}
}
}
return pile.isEmpty(); // des ouvrants restent ? chaîne invalide
}Au lieu d'empiler les caractères, on empile les indices des ouvrants : au moment d'une erreur, on peut alors citer la position exacte du couple fautif. C'est aussi l'occasion de traiter les trois cas d'erreur séparément.
public static boolean verifDelimDetail(String s) {
Pile<Integer> pile = new Pile<>(s.length() + 1);
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '(' || c == '[' || c == '{') {
pile.push(i); // on retient OÙ l'ouvrant a été lu
} else if (c == ')' || c == ']' || c == '}') {
if (pile.isEmpty()) {
// une fermante, mais aucune ouvrante en attente
System.out.println(" indice " + i + " : « " + c + " » fermante sans ouvrante");
return false;
}
int j = pile.pop();
char ouvrant = s.charAt(j);
if (!correspond(ouvrant, c)) {
// on s'arrête au premier problème : la suite n'aurait plus de sens
System.out.println(" indice " + j + " : « " + ouvrant
+ " » fermé par « " + c + " » (indice " + i + ")");
return false;
}
}
}
if (!pile.isEmpty()) {
int j = pile.pop();
System.out.println(" indice " + j + " : « " + s.charAt(j) + " » jamais fermé");
return false;
}
return true;
}Sorties réelles du programme sur les exemples du sujet :
« a[b(c)}d » :
indice 1 : « [ » fermé par « } » (indice 6)
« {a(b[c[d))]e]} » :
indice 6 : « [ » fermé par « ) » (indice 8)
« (a[b]c » :
indice 0 : « ( » jamais fermé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
public static void main(String[] args) {
Pile<Integer> p = new Pile<>(3);
p.push(1);
p.push(2);
p.push(3);
System.out.println(p.isFull()); // true
System.out.println(p.peek()); // 3
System.out.println(p.pop()); // 3
p.push(4); // réutilise la case libérée
System.out.println(p); // [bas 1, 2, 4 haut]
File<String> f = new File<>(3);
f.offer("a");
f.offer("b");
f.offer("c");
System.out.println(f.poll()); // a
f.offer("d"); // tourne dans le tableau
System.out.println(f); // [tête b, c, d queue]
String[] exemples = { "a(b)c", "a[b(c)}d", "{a(b[c[d))]e]}", "{a(b(d[d]))e}", "(a[b]c" };
for (String s : exemples) {
System.out.println(s + " -> " + verifDelimStack(s));
}
}Ce qu'il faut savoir refaire sans le sujet
- écrire
Pile<E>sur tableau : conventionsommet = -1,++sommetavant d'écrire,sommet--après avoir lu, exceptionsIllegalStateException/NoSuchElementException; - écrire
File<E>sur tableau circulaire :(tete + nbElement) % taillepour insérer,tete = (tete + 1) % taillepour retirer,nbElementpour 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
popet à la fin.
td2-piles-files.pdf
TD 2 — Piles et Files · Structures de données · 2 pages
Chargement du document…
Dernière mise à jour le
TD 1 — Set et Map
Ensembles aléatoires, intersection, union et réductions sur un Set, puis un annuaire de téléphones en Map — corrigé complet, compilé et exécuté, avec les traces d'exécution.
TP 1 — Utilisation des collections
Le TP d'ouverture de R3.02 : des iris dans un Set (et le piège equals/hashCode observé en direct), les Streams en une ligne, le TreeSet, puis un compteur de mots sur Notre-Dame de Paris.