Une pile est une structure qui stocke de manière ordonnée des éléments, mais rend accessible uniquement un seul d'entre eux, appelé le sommet de la pile. Quant on ajoute un élément, celui-ci devient le sommet de la pile, c'est-à-dire le seul élément accessible. Quant on retire un élément de la pile, on retire toujours le sommet, et le dernier élément ajouté avant lui devient alors le sommet de la pile. Pour résumer, le dernier élément ajouté dans la pile est le premier élément à en être retiré. Cette structure est également appelée une liste LIFO (Last In, First Out). Généralement, il y a deux façons de représenter une pile. La première s'appuie sur la structure de liste chaînée vue précédemment. La seconde manière utilise un tableau.
La première façon de modéliser une pile consiste à utiliser une liste chaînée en n'utilisant que les opérations ajouterTete et retirerTete. Dans ce cas, on s'aperçoit que le dernier élément entré est toujours le premier élément sorti. La figure suivante représente une pile par cette modélisation. La pile contient les chaînes de caractères suivantes qui ont été ajoutées dans cet ordre: "Paris", "Marseille", "Bordeaux", "Lyon" et "Toulouse". Pour cette modélisation, la structure d'une pile est celle d'une liste chaînée.
La deuxième manière de modéliser une pile consiste à utiliser un tableau. L'ajout d'un élément se fera à la suite du dernier élément du tableau. Le retrait d'un élément de la pile se fera en enlevant le dernier élément du tableau. La figure suivante représente une pile par cette modélisation. Elle reprend la même pile que pour l'exemple de modélisation par liste chaînée. Voici la structure de données correspondant à une pile représentée par un tableau.
NMAX est une constante représentant la taille du tableau alloué. n est le nombre d'éléments dans la pile.
A partir de maintenant, nous allons employer le type PILE qui représente une pile au sens général, c'est-à-dire sans se soucier de sa modélisation. PILE représente aussi bien une pile par liste chaînée (PILE_LST) qu'une pile par tableau (PILE_TAB). Voici les opérations que nous allons détailler pour ces deux modélisations.
Les prototypes de ces opérations (paramètres et type de retour) sont les mêmes quelque soit la modélisation choisie.
Cette fonction initialise les valeurs de la structure représentant la pile pointée par p, afin que celle-ci soit vide. Dans le cas d'une représentation par liste chaînée, il suffit d'initialiser la liste chaînée qui représente la pile.
Cette fonction indique si la pile p est vide. Dans le cas d'une représentation par liste chaînée, la pile est vide si la liste qui la représente est vide.
Cette fonction retourne l'élément au sommet de la pile p. Dans le cas d'une représentation par liste chaînée, cela revient à retourner la valeur de l'élément en tête de la liste. A n'utiliser que si la pile p n'est pas vide.
Cette fonction empile l'élément e au sommet de la pile pointée par p. Pour la représentation par liste chaînée, cela revient à ajouter l'élément e en tête de la liste. VRAI est retournée si l'élément a bien été ajouté.
Cette fonction dépile l'élément au sommet de la pile pointée par p et stocke sa valeur à l'adresse e. Pour la représentation par liste chaînée, cela revient à récupérer la valeur de l'élément en tête de liste avant de le supprimer de cette dernière. VRAI est retournée si la pile n'est pas déjà vide.
Cette fonction initialise les valeurs de la structure représentant la pile pointée par p, afin que celle-ci soit vide. Dans le cas d'une représentation par tableau, il suffit de rendre n nul.
Cette fonction indique si la pile p est vide. Dans le cas d'une représentation par tableau, la pile est vide si le champ n est nul.
Cette fonction retourne l'élément au sommet de la pile p. Dans le cas d'une représentation par tableau, cela revient à retourner la valeur du nième élément du tableau (i.e. l'élément d'indice n - 1). A n'utiliser que si la pile p n'est pas vide.
Cette fonction empile l'élément e au sommet de la pile pointée par p. Pour la représentation par tableau, cela revient à ajouter l'élément e à la fin du tableau. S'il reste de la place dans l'espace réservé au tableau, la fonction retourne VRAI.
Cette fonction dépile l'élément au sommet de la pile pointée par p et stocke sa valeur à l'adresse e. Pour la représentation par tableau, cela revient à diminuer d'une unité le champ n, et à renvoyer l'élément d'indice n du tableau. Si la pile n'est pas déjà vide, VRAI est renvoyée.
Une image en informatique peut être représentée par une matrice de points m ayant XMAX colonnes et YMAX lignes. Un élément m[x][y] de la matrice représente la couleur du point p de coordonnées (x;y). On propose d'écrire ici une fonction qui, à partir d'un point p, "étale" une couleur c autour de ce point. La progression de la couleur étalée s'arrête quand elle rencontre une couleur autre que celle du point p. La figure suivante illustre cet exemple, en considérant p = (3;4). On propose ici deux façon d'écrire cette fonction. La première solution est récursive. La seconde reprend la première en éliminant la récursivité à l'aide d'une pile. Par la suite, COULEUR sera le type qui représente une couleur, et POINT sera la structure qui représente un point.
La manière récursive consiste à écrire une fonction qui change la couleur d'un point p en une couleur c2 si sa couleur originale vaut c1. Dans le même temps, cette fonction s'appelle elle-même pour les quatre points adjacents à p. Bien entendu, il faut prendre garde de ne pas sortir des limites de la matrice.
Pour réaliser l'exemple présenté en introduction, la fonction récursive s'utilisera de la manière suivante.
Quant on appelle une fonction récursivement, il y a "empilement" des appels à cette fonction. En effet, chaque fois qu'une fonction est lancée, on empile le contexte de la fonction appelante de manière à pouvoir y revenir ensuite. Lorsque la fonction se termine, on dépile le dernier contexte empilé et on reprend l'exécution de la fonction appelante. L'idée ici est d'identifier les données qui définissent le contexte de la fonction et de les empiler de manière explicite. Ensuite, itérativement, tant que la pile n'est pas vide, on applique le corps de la fonction récursive sur les données dépilées du sommet de la pile. Un appel initialement récursif à la fonction se traduira alors par l'empilement de nouvelles données sur la pile. Ici, la donnée cruciale, c'est le point de la matrice que l'on est en train de traiter. On va donc empiler les coordonnées des différents points traités pour reproduire un comportement similaire à l'implémentation récursive qu'est la fonction remplirR.
Pour réaliser l'exemple présenté en introduction, la fonction itérative s'utilisera de la manière suivante.
Il s'agit d'un jeu de réflexion dont voici la règle. Des anneaux de diamètres différents sont empilés sur un poteau. Un anneau peut être empilé sur un autre seulement si il a un diamètre inférieur à celui de l'anneau sur lequel il repose. Le but du jeu est de déplacer les anneaux initialement empilés sur un seul poteau vers un autre en respectant les règles et en n'utilisant qu'un seul poteau intermédiaire.
Les poteaux sont représentés par des piles d'entiers numérotées 0, 1 et 2. Le jeu entier sera alors représenté par un tableau t de trois piles. On suppose qu'il y a n anneaux dans le jeu. On attribue à chaque anneau un numéro de manière à ce que si l'anneau a est plus petit que l'anneau b, alors son numéro est plus petit. Au départ, on aura donc: Voici maintenant la fonction qui réalise le déplacement des anneaux du poteau numéro a au poteau numéro b (on déduit le numéro du poteau intermédiaire c par la formule c = 3 - a - b). Cette fonction est récursive. S'il y a un seul anneau, on le déplace directement, sinon on déplace les n - 1 premiers anneaux sur le poteau intermédiaire c (appel récursif à la fonction) et le dernier anneau sur le poteau final b (appel récursif à la fonction). Ensuite, on déplace les anneaux du poteau intermédiaire c sur le poteau final a (appel récursif à la fonction). Voici une illustration. Et voici maintenant le détail de la fonction qui déplace les anneaux.
Une avec son code source est disponible pour vous permettre de mieux comprendre la résolution du jeu.
Comme la pile ne permet l'accès qu'à un seul de ses éléments, son usage est limité. Cependant, elle peut être très utile pour supprimer la récursivité d'une fonction comme nous l'avons vu précédemment. La différence entre la modélisation par liste chaînée et la modélisation par tableau est très faible. L'inconvénient du tableau est que sa taille est fixée à l'avance, contrairement à la liste chaînée qui n'est limitée que par la taille de la mémoire centrale de l'ordinateur. En contrepartie, la liste chaînée effectue une allocation dynamique de mémoire à chaque ajout d'élément et une libération de mémoire à chaque retrait du sommet de la pile. En résumé, la modélisation par liste chaînée sera un peu plus lente, mais plus souple quant à sa taille.
|