Détour : les sacs
Les piles et les files sont des cas particuliers des sacs.
Les opérations suivantes sont définies sur les sacs
- 
Le sac est-il vide ?
- Ranger dans le sac,
- Sortir un élément du sac (un des éléments rangés au préalable).
Le sac typique (en style objet) :
class Bag {
  …
  Bag () { … }
  boolean isEmpty() { … }
  void put(Object o) { … }
  Object get() { … }
}
Remarquer : Le sac est une structure mutable.
Les piles et les files
Piles et files sont des sacs munis de règles supplémentaires reliant
les sorties et les entrées :
- 
Les piles sont des sacs « évangéliques » : dernier entré, premier sorti
(ou LIFO : Last In, First Out).
- 
En ce cas, put se dit souvent push (empiler) et
get se dit souvent pop (dépiler).
 
- Les files sont des sacs égalitaires : premier entré, premier
sorti (ou FIFO : First In, First Out).
- 
En ce cas, put peut se dire enfiler et
get peut se dire défiler.
 
La pile
La pile
La file
La file
À quoi ça sert ?
La file est la structure la plus intuitive.
La file sert par exemple, dans un système d’exploitation,
à servir des demandes d’impression dans l’ordre.
À quoi ça sert ?
La pile est la structure la plus « informatique ».
Son besoin se fait clairement sentir dans la situation suivante…
- 
Un cuisinier fait une sauce…- 
Chef, votre épouse au téléphone ! Mettre la sauce en attente
(empiler(sauce)) et répondre…- 
Chef, il y a le feu ! Mettre le téléphone en attente
(empiler(téléphone)) et aller éteindre le feu.
- Le feu est éteint…
 
- Dépiler la tâche en attente (téléphone) et la terminer…
 
- Dépiler la tâche en attente (sauce) et la terminer.
Autre utilisation des piles
Évaluation des expressions arithmétiques.
Soit les piles d’entiers :
class Stack {
  …
  Stack () { … }
  boolean isEmpty() { … }
  void push(int i) { … }
  int pop() { … }
}
Les calculettes en notation postfixe (ou « polonaise ») fonctionnent
à l’aide d’une pile, selon ce principe :
- 
Un entier → l’empiler.
- Une opération ⊕, depiler x, dépiler y, empiler ⊕(y, x).
Exemples de calcul en notation postfixe
Calculer 1 + (2 × 3) : 1 2 3 * +
(ou 2 3 * 1 + d’ailleurs).
|  | → 1
→ |  | → 2
→ |  | → 3
→ |  | → ×
→ |  | → +
→ |  | 
Calculer (1 + 2) × 3 : 1 2 + 3 *
|  | → 1
→ |  | → 2
→ |  | → +
→ |  | → 3
→ |  | → ×
→ |  | 
class Calc {
  
public static void main (
String [] 
arg) {
    
Stack stack = 
new Stack () ;
    
for (
int i = 0 ; 
i < 
arg.
length ; 
i++) {
      
String cmd = 
arg[
i] ;
      
if (
cmd.
equals(
"+")) {
        
int x = 
stack.
pop(), 
y = 
stack.
pop() ;
        
stack.
push(
y+
x) ;
      } 
else if …
      
// Idem pour "*", "/", "-"
      } 
else {
        
stack.
push(
Integer.
parseInt(
cmd)) ; 
// doc
      }
    }
    
System.
out.
println(
stack.
pop()) ;
  }
}
 
Exécution de Calc
% java Calc 1 2 + 3 '*'
1 -> [1]
2 -> [1, 2]
+ -> [3]
3 -> [3, 3]
* -> [9]
9
Mais…
% java Calc 1 +        
1 -> [1]
Exception in thread "main" java.util.EmptyStackException
        at java.util.Stack.peek(Stack.java:79)
        at java.util.Stack.pop(Stack.java:61)
        at Calc.main(Calc.java:9)
Précisions sur la notation postfixe
Par définition, une notation postfixe est une suite de symboles P.
Par exemple :
Par définition, la valeur d’une notation postfixe est :
| | v(int) | = | int |  | v(P1   P2   op) | = | op(v(P1), v(P2)) | 
 | 
Correction du calcul
Modélisons le programme Calc. Son état : ⟨  I  •  S  ⟩.
- 
L’entrée I, c’est-à-dire une suite de symboles.
- La pile S (d’entiers).
Une action consiste à lire le symbole.
| |  | → |  |  |  | → | | ⟨ ⟨
 | I  •  S,  op(v1, v2) | ⟩ ⟩
 | 
 | 
 | 
Théorème  Si I est une notation postfixe P,
alors la pile S contient v(P) à la fin.
| ⟨ ⟨
 | P  • | ⟩ ⟩
 | →* | ⟨ ⟨
 | •  v(P) | ⟩ ⟩
 | 
Lemme  Pour toute suite de symboles I, toute pile S,
et toute notation postfixe :
| ⟨ ⟨
 | P  I  •  S | ⟩ ⟩
 | →* | ⟨ ⟨
 | I  •  S, v(P) | ⟩ ⟩
 | 
Démonstration  Par induction (sur P).
Dans le cas P = P1   P2   op.
| |  | = |  |  |  | →* | | ⟨ ⟨
 | P2  op    I  •  S ,  v(P1) | ⟩ ⟩
 | 
 |  |  | →* | | ⟨ ⟨
 | op    I  •  S ,  v(P1) ,  v(P2) | ⟩ ⟩
 | 
 |  |  | → | | ⟨ ⟨
 | I  •  S ,  op(v(P1), v(P2)) | ⟩ ⟩
 | 
 |  |  | = |  | 
 | 
Pile et appel de méthode
Revenons sur la fusion récursive
qui échoue par « débordement de la pile » pour des listes
assez longues.
static List merge(List xs, List ys) {
  if (xs == null) {
    return ys ;
  } else if (ys == null) {
    return xs ;
  } // NB: désormais xs != null && ys != null 
    else if (xs.val <= ys.val) {
    return new List (xs.val, merge(xs.next, ys)) ;
  } else { // NB: ys.val < xs.val
    return new List (ys.val, merge(xs, ys.next)) ;
  }
}
Appel de méthode
En simplifiant un peu.
- 
Le système d’exécution Java dispose d’une pile.
- Pour appeler une méthode :
- 
Empiler une addresse de retour.
- Empiler les arguments de la méthode.
 
- Une méthode
- 
Prélude : récupérer les arguments.
- Code de la méthode proprement dit… qui rend
la pile dans l’état où il l’a trouvée.
- Postlude : (pour revenir)
- 
Dépiler l’adresse de retour.
- Empiler le résultat de la méthode.
- Transférer le contrôle du programme à l’adresse de retour.
 
 
Dans le cas qui nous occupe
Nous allons modéliser un processeur en Java,
et examiner ce que fait le code compilé de merge.
- 
Toutes les variables sont globales (registres).
- Le contrôle est explicite (code = séquence d’instructions).
- 
Il y a des étiquettes (points dans l’exécution).
- Il y a des instructions « goto» (transfert direct)
et «goto*» (transfert indirect).
 
merge: // « Étiquette » de début de la méthode.
/* La pile S est P ,  ret ,  xs ,  ys
   Il faut rendre P ,  merge(xs, ys) */
  ys = S.pop() ;
  xs = S.pop() ;
  if (xs == null) {
    lab = S.pop() ; S.push(ys) ; goto* lab ;
  } …
Appel récursif
  ⋮
/* La pile S est P ,  ret
   Il faut rendre P ,  merge(xs, ys) */
  if (xs.val <= ys.val) {
     S.push(xs.val) ; // Sauver xs.val dans la pile
     S.push(lab1) ; S.push(xs.next) ; S.push(ys) ;
     // S est P ,  xs.val ,  lab1 ,  xs.next ,  ys
     goto merge ; // Appel récursif
   lab1:
     // S est P ,  merge(xs.next, ys)
     r = S.pop() ;          // Résultat de l'appel
     xsPointVal = S.pop() ; // xs.val sauvé avant l'appel
     lab = S.pop() ;        // Étiquette de retour
     S.push(new List (xsPointVal, r)) ;
     goto* lab ; // La pile est P ,  merge(xs, ys) ok.
  } …
Le principe est le suivant :
- 
L’objet pile (classe Stack) maintient une liste (privée)
d’entiers.
- Opérations :
- 
Ajouter au début de la liste,
- Enlever du début de la liste.
 
class Stack {
  private List me ;
  Stack () { me = null ;  }
  boolean isEmpty() { return me == null ; }
  …
}
Diversion : modificateurs de visibilité
Le champ me des objets Stack est déclaré private.
class Stack {
  private List me ; …
En effet, il ne faut pas que qui que ce soit d’autre (que l’objet Stack)
utilise me.
Les modificateurs, du plus restrictif au plus permissif.
- 
private, visible de la classe uniquement.
- rien, visible du package (un paquet de classes).
- protectedvisible des héritiers (une nouvelle notion).
- publicvisible de partout.
Puisque nous n’utilisons ni package, ni héritage, on
se limite à private et à rien.
(sauf pour public static void main).
Empiler : ajouter au début
  me = new List (4, me) ;
Dépiler : enlever du début
   me = me.next ;
Code de push et pop
  void push(int i) {
    me = new List (i, me) ;
  }
  int pop() {
    if (me == null) throw new Error("Pile Vide") ;
    int r = me.val ;
    me = me.next ;
    return r ;
  }
Remarquer 
« throw new Error(…) » qui interrompt le programme
en lançant (instruction throw) une exception
(ici objet de la classe Error).
Diversion : programmation des erreurs
Si l’effet attendu d’une erreur est de faire échouer le programme,
on se demande bien l’intérêt de l’exception.
   System.err.println("Erreur : pile vide") ; // NB sortie d'erreur
   System.exit(2) ;                           // Tout arrêter
Mais on veut pouvoir réparer les erreurs dans le programme.
Or, une exception est un résultat innattendu, qui passe par
tous les appels en attente.
% java Calc 1 +        
1 -> [1]
Exception in thread "main" java.util.EmptyStackException
        at java.util.Stack.peek(Stack.java:79)
        at java.util.Stack.pop(Stack.java:61)
        at Calc.main(Calc.java:9)
Et on peut « attraper » l’erreur au passage.
Signaler une erreur
Déclarer une classe de nos exceptions (plus propre).
class StackEmpty { } extends Exception
Lancer notre exception.
 int pop() throws StackEmpty { // Obligatoire ici
    if (me == null) throw new StackEmpty () ;
  …
Note : Le code la classe Stack se contente
de signaler l’erreur.
Cette classe n’est pas modifiable (par ex. code de la bibliothèque).
Traiter l’erreur
Comme la vraie calculette HP:
une pile « vide » contient en fait 0, 0, …
Attraper l’exception (∼ résultat exceptionnel)
pour la rempacer par un résultat normal.
static int popStack(Stack s) {
  try {
    return s.pop() ;
  } catch (StackEmpty e) {
    return 0 ;
  }
}
Et appeler
popStack(s) en place de s.pop().
dans Calc.
Réalisation des files
Le principe est le suivant, l’objet file maintient une liste (privée)
d’entiers.
- 
Ajouter à la fin,
- Enlever du début.
Pour réaliser ces opérations en temps constant il nous faut deux
références :
- 
insur la dernière cellule de liste (pour ajouter).
- outsur la première cellule de liste (pour enlever).
class Fifo {
  private List in, out ;
  Fifo () { in = out = null ; }
  boolean isEmpty() { return in == null && out == null ; }
  …
}
  in.next = new List (4, null) ;
Enfiler : ajouter à la fin (suite)
  i = i.next ;
   out = out.next ;
Code complet d’enfiler
Le code final doit tenir compte des files vides.
void enfiler(int i) {
   if (isEmpty()) {
     in = out = new List (i, null) ;
   } else {
     in.next = new List (i, null) ;
     in = in.next ;
   }
}
Il en va de même pour défiler (attention à bien remettre in à
null en cas de défilage du dernier élément.)
Autre réalisation des piles/files
Pile et files peuvent également être codées à l’aide de tableaux.
Voici l’exemple des piles :
class Stack {
  private final static int SZ=32 ; // Taille de pile
  private int [] t ;
  private int sp ; // « pointeur de pile »
  Stack() { t = new int[SZ] ; sp = 0 ; }
  …
}
Principe : le tableau est rempli de 0 à sp-1.
- 
pop:sp = sp-1 ;
- push(x):- t[sp] = x ; sp = sp+1 ;
La pile avec un tableau
class Stack {
  …
  boolean isEmpty() { return sp <= 0 ; } // Expression booléenne
  int pop() {
    if (isEmpty()) throw new Error ("Pile vide") ;
    return t[--sp] ; // pour sp = sp-1 ; return t[sp] ;
  }
  void push(int x) {
    if (sp >= t.length) {
      throw new Error ("Pile pleine") ; // Comme Java!
    }
    t[sp++] = x ; // Pour t[sp] = x ; sp = sp+1 ;
  }
}
Exemple de pile tableau
Redimensionnement automatique
Il est dommage de limiter la taille des piles à priori.
Profitons des tableaux « dynamiques » de Java.
// Double la taille du tableau interne
  private void resize() {
    if (sp >= t.length) {
      int [] newT = new int [2*t.length] ; // Allouer
      for (int i = 0 ; i < sp ; i++) {
        newT[i] = t[i] ;
      } // Copier t[0..sp] -> newT[0..sp]
      t = newT ; // t référence vers nouveau tableau
    }
  }
  void push(int x) {
    resize() ; t[sp++] = x ;
  }
Coût de push en cas de redimensionnement
Jusqu’ici push et pop s’exécutaient en temps constant.
Ce n’est plus le cas : push peut maintenant prendre
un temps arbitrairement long.
Mais push est toujours en O(1) « amorti ».
- 
Un programme fait N fois push.
- Le tableau est initialement de taille 2p0.
- Au pire le tableau est redimensionné p+1−p0 fois
(2p < N ≤ 2p+1).
Pour un coût total de l’ordre de :
| 2p0 + 2p0+1 + ⋯ + 2p+1 < 2p+2 < 4 · N |  
 
- Le coût de N pushest en O(N).
- Le coût amorti d’un pushest en O(1)
(coût total/nbpush< cst).
Les piles toutes faites
La pile est une structure tellement utile, que la bibliothèque Java
la propose, classe Stack, package java.util
Mais des piles de quoi ?
- 
La classe Stackest une classe « générique »
qui prend une classe en argument.
- Les objets de la classe
Stack<C>sont des piles de C, où C est
une classe arbitraire.
Par exemple Stack<String>, Stack<List>, …
Voir la doc
Exemple, pile de String
Notation postfixe → Notation infixe (usuelle).
import java.util.* ; // Stack est java.util.Stack
class Infix {
  public static void main (String [] arg) {
    Stack<String> stack = new Stack<String> () ;
    for (int k = 0 ; k < arg.length ; k++) {
      String x = arg[k] ;
      if (x.equals("+") || …) {
        String i1 = stack.pop(), i2 = stack.pop() ;
        stack.push("(" + i2 + x + i1 + ")") ;
      } else {
        stack.push(x) ;
      }
      System.err.println(x + " -> " + stack) ;
    }
    System.out.println(stack.pop()) ;
  }}
Exemple de transformation
% java Infix 1 2 + 3 '*'
1 -> [1]
2 -> [1, 2]
+ -> [(1+2)]
3 -> [(1+2), 3]
* -> [((1+2)*3)]
((1+2)*3)
Piles de scalaires
Dans Stack<C> C est une classe.
On ne peut pas faire des piles Stack<int>.
Mais on peut faire des piles Stack<Integer>,
où Integer est une classe fournie par défaut
(package java.lang).
Deux méthodes utiles :
- 
public static Integer valueOf(int i), du scalaire vers l’objet.
- public int intValue(), et réciproquement.
Il y a des classes semblables
(Char, Short etc.)
pour tous les types scalaires
(char, short etc.)
La calculette avec pile de la bibliothèque
Cela semble assez lourd.
import java.util.* ;
class Calc {
  public static void main (String [] arg) {
    Stack<Integer> stack = new Stack<Integer> () ;
    …
        int i1 = stack.pop().intValue() ;
        int i2 = stack.pop().intValue() ;
        stack.push(Integer.valueOf(i2+i1)) ;
    …
  }
}
La calculette avec pile de la bibliothèque II
Mais en fait le compilateur sait faire
les conversions int ↔ Integer tout seul.
import java.util.* ;
class Calc {
  public static void main (String [] arg) {
    Stack<Integer> stack = new Stack<Integer> () ;
    …
        int i1 = stack.pop(), i2 = stack.pop() ;
        stack.push(i2+i1) ;
    …
  }
}
Attention,
le code qui s’exécute est bien par exemple
stack.push(Integer.valueOf(i2+i1)).
Une file dans un tableau, principe
Deux champs privés, int in et int out.
La file va de la case out à la case in-1.
File dans le tableau, difficulté I
Le tableau est « circulaire »
Solution, incrémenter modulo la taille du tableau.
File dans le tableau, difficulté II
in et out ne suffisent pas toujours.
Solution, conserver une information additionelle : le nombre d’éléments
de la file.
Une classe commode LinkedList<C>
(package java.util) convient,
il s’agit en fait d’une double-ended queue.
La documentation de la classe LinkedList<C>.
- 
Méthodes addFirstetaddLast(put) pour
ajouter un élément au debut ou à la fin.
- Méthodes removeFirst(get) etremoveLastpour
récupérer le premier ou dernier élément.
- Le tout en temps constant, par des listes doublement chaînées
(voir le poly, section 2.3.3)
D’autres classes de bibliothèque existent avec des noms
plus séduisants (Queue), mais
LinkedList<C> est la plus commode.
Queue à deux bouts
Un petit exemple,
- 
Par les liens → on a facilement
removeFirst/addLast— comme une file, en fait.
- Par les liens ← on a facilement
removeLast/addFirst— par symétrie, file du dernier au
premier élément.
Choix de l’implémentation
- 
Hors considérations de facilité ou d’efficacité,
il n’y a pas à choisir : le service rendu est le même
(type abstrait de données).
- Si on veut examiner la question…- 
Bibliothèque, aucun code à écrire, mais il faut
lire la doc, et la comprendre.
- Listes, grande souplesse.
- Tableaux (redimensionné), à priori plus efficace,
mais attention à l’occupation mémoire.
 
Une autre utilisation des files/piles
Soit un petit robot, perdu dans un labyrinthe, mais muni d’un sac (de
cases) d’un pinceau et de trois couleurs.
Le robot cherche la sortie ainsi :
  bag.put(entrée) ;
  while (!bag.isEmpty()) {
    Case case = bag.get() ;
    case.colorie(rouge) ;
    if (case.equals.(sortie)) return ;
    for ( … ) { // Tous les voisins non-coloriés
      Case voisin = … ;
      voisin.colorie(bleu) ;
      bag.put(voisin) ;
    }
    case.colorie(vert) ;
  }
Rapport voisins/couleur
- 
La case rouge vient de sortir du sac,
- elle est nécessairement voisine d’au moins une case verte,
- la case noire indique un mur.
|  | ⇒ |  | 
 - 
Les deux cases voisines accessibles et non encore coloriées
entrent dans le sac (en bleu).
Mettre les voisins dans le sac (file ici)
|  | ⇒ |  | 
Sortir une case de la file
|  | ⇒ |  | 
Noter : c’est la case la plus ancienne qui est sortie de la
file.
Différents sacs, différents parcours
Sur un labyrinthe sans murs, l’effet est notable.
|  |  |  | 
|  | 
| Pile |  | File | 
Ce document a été traduit de LATEX par HEVEA