Une Pile (stack ou LIFO en anglais) est un conteneur d'éléments qui réalise le principe dernier arrivé, premier sorti (Last In, First Out en anglais).

Dans la vie courante, une analogie usuelle serait une pile d'assiettes : "l'assiette qu'on récupère est toujours la dernière qui a été empilée".

Cette fiche précise le Type Abstrait de Données Pile, puis introduit l'implémentation en C des piles qui sera la référence pour ce module, basée sur une simple liste chaînée dynamique.

1. Le type abstrait Pile (Stack ou LIFO)

Parmi les types abstraits "conteneurs d'éléments", une Pile s'appuie sur le principe dernier arrivé, premier sorti.

En anglais, on parle de stack ou de LIFO pour Last In, First Out.

Dans nos sources, on appellera le type C lifo_t (car le mot clé stack_t existe déjà dans librairie C).

Les principales opérations qui définissent le TAD Pile sont :

  • Créer (new) : créer une pile vide
  • Détruire (delete) : détruire une pile
  • Empiler (push) : ajouter un élément sur la pile
  • Consulter la tête (peek) : récupérer le dernier élément ajouté (on dit la "tête de pile") sans l'enlever. N'a pas de sens que si la pile est vide.
  • Dépiler (pop) : récupérer la tête et l'enlever de la pile. N'a pas de sens si la pile est vide.
  • Tester si vide : retourner VRAI ou FAUX suivant que la pile est vide ou non

On peut remarquer que, dans cette définition basique du TAD pile, on ne peut accéder qu'à la tête de la pile, c'est à dire au dernier élément ajouté. A l'inverse, il n'est pas possible d'accéder à un élément à l'intérieur de la pile.

Dans cette approche "rigoriste", le TAD ne définit par exemple même pas d'opération pour afficher tous les éléments contenus dans la pile ! Dans la vraie vie, on ajoute quand même souvent cette opération d'affichage - et nous vous conseillons de le faire, ne serait-ce que pour débugger vos programmes...

Les opérations du TAD Pile sont ainsi parfois complétées :

  • Afficher : affiche tous les éléments présents dans la pile
  • Compter : retourne le nombre d'éléments présents dans la pile
  • etc.

Comme d'habitude, lors de l'implantation du TAD dans une structure de données, il importera de s'assurer que les coûts des fonctions réalisant les opérations du TAD sont minimaux.

Pour les piles, les principales opérations (empiler, dépiler, accéder à la tête) sont censées être à coût constant O(1).

2. Pile dynamique : implémentation du TAD Pile s'appuyant sur une liste chaînée

Le TAD Pile peut être réalisé par plusieurs structures de données. Cette secton est consacrée à l'implémentation des piles qui s'appuie sur une simple liste chaînée dynamique.

Cette implémentation s'appuie sur la remarque simple suivante :

Une liste chaînée dynamique, comme vue dans les fiches précédentes, peut tout à fait être utilisée comme une pile.

Il faut et suffit de n'accéder qu'à la tête de la liste.

Dès lors :

  • "Empiler" un élément, c'est tout simplement ajouter l'élément en tête de la liste
  • Et "dépiler" un élément, c'est tout simplement enlever l'élément en tête de la liste

Exemple de mise en oeuvre du TAD pile au moyen d'une liste chaînée dynamique.

Pour implanter le TAD Pile au moyen d'une structure de données de type liste chaînée, deux solutions :

  • On peut définir le type pile (lifo_t) comme on avait définit le type liste, puis on écrit les fonctions lifo_push() et lifo_pop() comme on avait écrit les fonctions d'ajout et suppression en tête sur les listes.
  • Si on dispose déjà du code de la liste chaînée dynamique, on peut aussi dire que la pile est en fait une liste, puis écrire les fonctions du TAD pile tout simplement en appelant les fonctions du TAD liste. Autrement dit, dans ce cas, une pile est en fait une liste dont on a juste changé la sémantique.

Voyons comment faire dans le 2nd cas.

2.1 Ficher header lifo.h : type C et API du module Pile

On suppose donc qu'on dispose d'un module liste, tel que vu dans la fiche implémentation de référence des listes :


                // Fichier list.h
                #ifndef _LIST_H
                #define _LIST_H

                #include "element.h"

                // 1. Définition des types maillon (link_t) et liste (list_t)
                typedef struct _link {
                  element_t val; /* un élément de la liste*/
                  struct _link *next; /* l'adresse du maillon suivant */
                } link_t, *list;

                // 2. Protoype des fonctions realisant les opérations essentielles du type abstrait Liste

                // Crée une liste vide
                list_t list_new() ;

                // Retourne VRAI si l est une liste vide
                int list_is_empty(list_t l);

                // Retourne l'élément en tête de liste
                // PRECONDITION : liste non vide
                element_t list_first(list_t l);

                // Retourne le reste de la liste
                // PRECONDITION : liste non vide
                list_t list_next(list_t l);

                // Ajoute l'élément e en tête de la liste et retourne la nouvelle liste
                list_t list_add_first(element_t e, list_t l);

                // Supprime le maillon en tête de liste et retourne la nouvelle liste
                // PRECONDITION : liste non vide
                list_t list_del_first(list_t l);

                // Retourne le nombre d'éléments (ou de maillons) de la liste
                size_t list_length(list_t l);

                // Retourne un pointeur sur le premier maillon contenant e,
                // ou NULL si e n'est pas dans la liste
                list_t list_contains(list_t l, element_t e);

                // Affiche la liste
                void list_print(list_t l);

                // Libère toute la liste et retourne une liste vide
                list_t list_delete(list_t l);

                #endif
            

Pour déclarer le type lifo_t dans le fichier header lifo.h du module pile, il suffit d'importer le module list puis d'indiquer que "une pile est en fait une liste" :


          // fichier lifo.h pour l'implantation du TAD pile au moyen d'une liste chaînée dynamique
          #ifndef LIFO_H_
          #define LIFO_H_

          #include "list.h"  // On importe le TAD liste chaînée dynamique

          // 1. declaration du type lifo_t.
          //    Il suffit de déclarer que "une pile est une liste" !
          typedef list_t lifo_t ;

          // 2. prototype des fonctions de l'API du TAD Pile
          ...

          #endif
          

Pour l'API du module lifo, c'est à dire la liste des prototype des fonctions réalisant le TAD Pile, on traduit les opérations du TAD.

Le fichier lifo.h est alors donné in extenso ci-dessous :


          // fichier lifo.h pour l'implantation du TAD pile au moyen d'une liste chaînée dynamique
          #ifndef LIFO_H_
          #define LIFO_H_

          #include "list.h"  // On importe le TAD liste chaînée dynamique

          // 1. declaration du type lifo_t.
          //    Il suffit de déclarer que "une pile est une liste" !
          typedef list_t lifo_t ;

          ...

          // 2. prototype des fonctions de l'API du TAD Pile

          // Crée et retourne un pile vide ou NULL en cas d'erreur
          lifo_t lifo_new();

          // Détruit la pile et retourne une pile vide
          lifo_t lifo_delete(lifo_t stack);

          // Retourne 1 si la pile  stack   est vide, 0 sinon
          int lifo_is_empty(lifo_t stack);

          // Ajoute l'élément e à la pile  stack  et retourne la nouvelle pile
          lifo_t lifo_push(element_t e, lifo_t stack);

          // Retourne l'élément en tête de pile (sans l'enlever de la pile)
          // PRECONDITION : la pile  stack  ne doit pas être vide
          element_t lifo_peek(lifo_t stack);

          // Enlève l'élément en tête de la pile, et retourne cet élément
          // PRECONDITION : la pile pointée par  p_stack  ne doit pas être vide
          element_t lifo_pop(lifo_t * p_stack);

          #endif
          

Dans ce fichier lifo.h, le prototype de la fonction lifo_pop() appelle une remarque importante.

Cette fonction lifo_pop() doit en effet "retourner" 2 choses :

  • l'élément qui était en tête
  • et la pile modifiée, dont on enlevée l'ancienne tête

Seulement voila : en langage C, il n'est possible de retourner (avec return) qu'une unique valeur.

Pour cette fonction, il faut donc nécessairement avoir recours à un passage par pointeur (ou "par adresse") pour l'une des deux valeurs à "retourner".

Dans notre API, le choix réalisé est de passer la pile par pointeur et de retourner l'élément qui était en tête avec return. C'est pourquoi le prototype de la fonction est : element_t lifo_pop(lifo_t * p_stack).

2.2 Ficher source lifo.c

Le fichier source lifo.c du module pile est alors particulièrement simple à écrire : puisque une pile est une file dans cette implémentation, il faut et suffit d'appeler les fonctions des listes !


          // Extrait du fichier lifo.c pour l'implantation du TAD pile au moyen d'un tableau
          #include "lifo.h"

          // Crée et retourne un pile vide ou NULL en cas d'erreur
          lifo_t lifo_new() {
            return list_new(); // "créer une pile, c'est en fait créer une liste !"
          }

          ...

          // Ajoute l'élément e à la pile s et retourne la nouvelle pile
          lifo_t lifo_push(element_t e, lifo_t stack) {
            return list_add_first(e, stack); // "empiler, c'est en fait ajouter en tête de la liste !"
          }


          // Retourne l'élément en tête de pile (sans l'enlever de la pile)
          // PRECONDITION : la pile  stack  ne doit pas être vide
          element_t lifo_peek(lifo_t stack) {
            return list_first(stack);
          }

          ... etc ...

          

Ecrire de la fonction lifo_pop() est un peu plus délicat du fait que la pile est passée par adresse à cette fonction.

Le QCM suivant propose alors 3 implémentations. Retrouverez-vous laquelle est correcte ?

Comment écrire la fonction lifo_pop() ? Quelle est la bonne version de la fonction lifo_pop() dans pour l'implémentation des piles par liste chaînée dynamique ?

                  // Enlève l'élément en tête de la pile, et retourne cet élément
                  // PRECONDITION : la pile pointée par  p_stack  ne doit pas être vide
                  element_t lifo_pop(lifo_t * p_stack) {
                    element_t e = list_first(*p_stack);
                    list_del_first(*p_stack);
                    return e;
                  }
              
Attention, rappel !
La fonction list_del_first() supprime le premier maillon de la liste et retourne la nouvelle liste.
A la ligne 5, il faut penser à mettre à jour la variable liste (ici : la pile pointée par p_stack) à partir de la valeur retournée par la fonction !

                    // Enlève l'élément en tête de la pile, et retourne cet élément
                    // PRECONDITION : la pile pointée par  p_stack  ne doit pas être vide
                    element_t lifo_pop(lifo_t * p_stack) {
                      * p_stack   = list_del_first(*p_stack);
                      return list_first(*p_stack);
                    }
                

                   // Enlève l'élément en tête de la pile, et retourne cet élément
                   // PRECONDITION : la pile pointée par  p_stack  ne doit pas être vide
                   element_t lifo_pop(lifo_t * p_stack) {
                     element_t e = list_first(*p_stack);
                     * p_stack   = list_del_first(*p_stack);
                     return e;
                   }
               

2.3 Embryon de code de test du module lifo

Enfin, voici un embryon de code de test de notre module pile, qui correspond aux évolution de l'image précédente (on suppose donc que le type element_t a été défini avec typedef double element_t ;).


          // Code de test des piles, dans le cas où les éléments sont des double
          #include "lifo.h"

          int main() {
            lifo_t une_pile = lifo_new();

            une_pile = lifo_push(9.5, une_pile);
            printf("L'élément en tete de pile vaut : %lf\n", lifo_peek(une_pile));
            // affiche 9.5 . Remarque : l'élément n'a pas été enlevé

            une_pile = lifo_push(6.0, une_pile);
            une_pile = lifo_push(5.4, une_pile);
            une_pile = lifo_push(3.6, une_pile);


            element_t ancienne_tete = lifo_pop( &une_pile );
            printf("L'élément qui vient d'être supprimé en tete de pile vaut : %lf\n", ancienne_tete);
            // affiche 3.6

            une_pile = lifo_delete(une_pile);

            return 0;
          }
          

2.4 Un QCM pour finir

Un dernier petit QCM pour la route... On complète le main() précédent, juste avant lifo_delete(), par les appels suivants :


         int main() {
           ... // Même début

           // ICI LA PILE CONTIENT 4 ELEMENTS

           element_t ancienne_tete = lifo_pop( &une_pile );
           printf("L'élément qui vient d'être supprimé en tete de pile vaut : %lf\n", ancienne_tete);
           // affiche 3.6

           lifo_pop( &une_pile );
           lifo_pop( &une_pile );
           lifo_pop( &une_pile );

           lifo_pop( &une_pile );  // <=====

           une_pile = lifo_delete(une_pile);
         }
            
Que dire de la ligne 14 ?
A la ligne 14, comme la pile est déjà vide, il ne se passe rien. A la ligne 14, il y a une erreur de segmentation. C'est normal. Eh, oui, c'est normal ! En effet, il est absurde de vouloir "dépiler" une pile vide. D'ailleurs, le contrat de la fonction fifo_pop() le précise bien en annonçant la précondition "la pile ne doit pas être vide" ! A la ligne 14, il y a une erreur de segmentation. Aie, c'est donc que notre code pour la fonction list_pop() a un gros problème, puisqu'il ne traite pas le cas d'erreur "pile vide" ! Le cas "liste vide" n'est pas une erreur : il est géré comme une précondition de la fonction.

3 En conclusion

Nous avons introduit l'implémentation du TAD pile qui s'appuie sur une structure de données "liste chaînée dynamique".

Avec l'implémentation du TAD à base d'une liste chaînée dynamique, il n'y a pas de limite au nombre d'éléments contenus dans la pile (à part celle due à la mémoire disponible sur la machine), la complexité mémoire croit linéairement avec la taille de la pile, et la complexité des fonctions est constante, O(1).

Le code s'avère particulièrement simple à écrire si on dispose déjà d'un module liste.

Notre implémentation des piles qui s'appuie sur les listes dynamiques a un problème : puisque le type C pile est défini comme le type liste avec typedef list_t lifo_t ;, alors, si on créer une variable de type pile, cette variable est en fait de type liste... et rien n'empêche d'utiliser cette variable comme une liste.

Autrement dit, il devient possible d'appeler toutes les fonctions du module liste avec une variable de type Pile. Par exemple, il est possible d'appeler la fonction d'ajout en queue de liste, d'appeler la fonction qui supprime les maillons contenant un certain élément, etc.

Ceci n'est pas cohérent avec l'idée que, dans une pile, on ne doit voir et manipuler que le dernier élément ajouté dans la pile (la "tête de pile"), mais pas le reste.

Il y a deux solutions à ce problème en langage C.

  • Définir explicitement le type pile comme on avait défini le type liste, et tricher dans le fichier .c en indiquant au compilateur que les deux types sont en fait identiques.
  • Avoir recours aux types opaques en langage C.

La seconde solution est la plus propre, mais elle est au delà de nos compétences ce semestre. Voyons donc la première

Définir explicitement le type pile comme on avait défini le type liste, et tricher dans le fichier .c en indiquant au compilateur que les deux types sont en fait identiques.

Il s'agit d'abord de ne pas indiquer dans le fichier header du module pile qu'une pile est une liste, mais juste de définir le type pile comme on avait défini le type liste.

Le fichier header devient donc :


        #ifndef LIFO_H_
        #define LIFO_H_

        #include "element.h"

        // 1. declaration explicite du type lifo_t.
        // En fait, on sait que c'est la même chose que le type list_t
        // Mais ca, le compilateur ne le sait pas !
        typedef struct _lifo_link {
          element_t     val;
          _lifo_link *  next;
        } * lifo_t ;

        // 2. prototype des fonctions de l'API du TAD Pile

        ... ETC ... // les prototypes ne changent pas

        #endif
        

Comme le type pile n'est pas défini comme étant identique au type liste, le compilateur râlera si on veut appeler, dans le main() par exemple, une fonction du module liste en lui passant une variable de type pile. Ca tombe bien, c'est tout à fait ce que l'on cherche !

Dans le fichier source du module pile, et uniquement dans ce fichier, le secret est alors de "tricher" en indiquant au compilateur que, en fait, on sait nous qu'une variable pile est la même chose qu'une variable liste.

Pour cela, on utilise des casts de pointeurs.

Voici alors ce que devient le fichier lifo.c.


      // Extrait du fichier lifo.c pour l'implantation du TAD pile au moyen d'une liste
      // avec les "types opaques"
      #include "lifo.h"
      #include "list.h" // ici on inclue list.h pour connaître les prototypes des fonctions sur les listes

      // Crée et retourne un pile vide ou NULL en cas d'erreur
      lifo_t lifo_new() {
        list_t l = list_new();
        return (lifo_t) l;      // <=== Remarquez le cast du type du pointeur
        // ou directement :
        // return (lifo_t) list_new();
      }

      // Ajoute l'élément e à la pile s et retourne la nouvelle pile
      lifo_t lifo_push(element_t e, lifo_t stack) {
        list_t l = (list_t) stack;  // <=== Remarquez le cast du type du pointeur
        l = list_add_first(e, l);
        return (lifo_t) l;      // <=== Remarquez le cast du type du pointeur
        // ou directement :
        // return (lifo_t) list_add_first(e, (list_t) stack);
      }

      ... etc ...
      

Et voila, le tour est joué : à part dans notre fichier lifo.c, personne ne sait que derrière nos piles se cachent en fait une bête liste chaînée dynamique ! Et hors de ce module, par exemple dans la fonction main(), le compilateur ralera si on passe une variable pile de type lifo_t à une fonction du module list.