🚛 Structures linéaires - La pile

Sommaire

Rappels sur le tableau

Exemple de tableau : les nombres premiers inférieurs à 100.

Indice 00 11 22 33 44 55 66 77 88 99 1010 1111 1212 1313 1414 1515 1616 1717 1818 1919 2020 2121 2222 2323 2424
Élément 22 33 55 77 1111 1313 1717 1919 2323 2929 3131 3737 4141 4343 4747 5353 5959 6161 6767 7171 7373 7979 8383 8989 9797

Un tableau, masses par exemple ici, est une structure de données, abstraite et élémentaire :

En interne, on accède, en pratique, à un élément d'indice i du tableau par son adresse mémoire qui est égale à adresse_tableau + i * taille_élément.

On peut lire et modifier un élément d'indice i. Avec un langage de programmation, on note très souvent masses[i] cet élément de masses d'indice i.

Avec cette structure de données, on a déjà résolu de nombreux problèmes, mais on peut aussi construire de nouvelles structures de données.

Concrètement, on retrouve des implémentations de cette structure abstraite, le tableau, dans la plupart des langages de programmation. Une limitation évidente est la taille d'un tableau, limitée par la capacité de mémoire disponible ; sinon, c'est assez simple.

La pile

On utilisera les notations de la POO.

Image : wikipedia, la pile

Implémentation avec tableau

On propose ici, en Python, une implémentation qui utilise en arrière-plan une structure de type list de Python, mais en limitant volontairement l'usage, comme un tableau, sans méthode dynamique. Ainsi l'implémentation pourrait être réalisée dans de nombreux langages de programmation avec de rares ajustements.

La limitation, ici, sera que les éléments de la pile devront être de même type, et de même taille.

class Pile():
    """
    Une classe "pile d'entiers", de taille maximale 'taille_max',
    implémentée avec les données dans un tableau.
    """

    def __init__(self, taille_max: int):
        self.taille_max = taille_max
        self.données = [None for _ in range(taille_max)] # un tableau
        self.hauteur = 0

    def est_vide(self) -> bool:
        return self.hauteur == 0

    def empile(self, élément):
        """Ajoute `élément` au sommet de la pile.
        """
        if self.hauteur == self.taille_max:
            raise ValueError('Pile pleine')
        self.données[self.hauteur] = élément
        self.hauteur += 1
    
    def dépile(self):
        """Enlève et renvoie l' `élément` du sommet de la pile.
        """
        if self.est_vide():
            raise ValueError('Pile vide')
        self.hauteur -= 1
        élément = self.données[self.hauteur]
        #self.données[self.hauteur] = None # optionnel
        return élément

On pourrait ajouter une méthode .__str__(self).

    def __str__(self) -> str:
        """Pour usage interne, tests.
        """
        ans = "[Bas de la pile]"
        for i in range(self.hauteur):
            ans += str(self.données[i])
            ans += ", "
        ans += " [Sommet de la pile.]"
        return ans

Idéalement, il faudrait écrire les attributs taille_max, hauteur et données en les préfixant de __ pour les rendre privés. Exercice 1 : faire cela et justifier ce choix.

Exercice 2 : Ajouter une méthode accesseur .hauteur(self). Cela donne une raison de plus d'avoir préfixé l'attribut !

Exercice 3 : Vérifier l'utilisation avec le code suivant.

ma_pile = Pile(100)
for x in range(20):
    ma_pile.empile(x*x + 2)
print(ma_pile)

for i in range(11):
    print("valeur dépilée :", ma_pile.dépile())

print("Et ensuite")

print(ma_pile)

ma_pile.empile(1337)

print(ma_pile)

print(dir(ma_pile))

Implémentation avec les listes dynamiques de Python

On peut facilement implémenter une pile de taille arbitraire avec le type list de Python et ses méthodes .append(self, élément) et .pop(self). Tous les langages de programmation ne le permettent pas aussi facilement...

class Pile():
    def __init__(self):
        self.données = []

    def __str__(self) -> str:
        return str(self.données)

    def est_vide(self) -> bool:
        return self.données == []

    def empile(self, valeur):
        self.données.append(valeur)
    
    def dépile(self):
        if self.est_vide():
            raise ValueError('Pile vide')
        return self.données.pop()

On remarque que nous n'avons pas ici (besoin) de méthode ni d'attribut hauteur... Pour le type abstrait pile, on travaille parfois sans l'avoir disponible au départ.

Exercice 1 : rendre l'attribut données privé et ajouter une méthode .hauteur(self).
Refaire les tests vus précédemment.

Exercice 2 : Résoudre le problème Dates de péremption sur France-IOI.

Exercice 3 : Pour cet exercice, on supposera qu'on ne dispose que du constructeur Pile() d'une pile vide, ainsi que des méthodes .est_vide(self), .empile(self, élément) et .dépile(self).

    # suite de class Pile():
    def mystère(self):# -> Pile:
        autre = Pile()
        while not self.est_vide():
            autre.empile(self.dépile())
        return autre

Implémentation de façon récursive

Gardons à l'esprit qu'un programme n'a pas une vue d'ensemble d'une pile. Il ne voit que le sommet ; en effet l'accès lui est aisé, moins pour le reste. On peut alors considérer une pile comme étant :

On devine alors une définition récursive d'une pile :

Dans l'implémentation ci-dessous, on choisit :

class Pile():
    def __init__(self, données=None):
        self.__données = données
    
    def est_vide(self):
        return self.__données is None

    def empile(self, élément):
        queue = self.__données
        tête = élément
        self.__données = (tête, queue)
    
    def dépile(self):
        if self.est_vide():
            raise ValueError('Pile vide')
        tête, queue = self.__données
        self.__données = queue
        return tête

On aurait pu écrire la méthode empile en une seule ligne avec self.__données = (élément, self.__données), mais c'est moins lisible, et moins pédagogique.

Attention : ici nous avons en structure interne une pile qui est soit None, soit un tuple qui n'a que deux éléments, et de types différents. Nous nous l'étions interdit pour les listes ! Acceptons l'idée qu'il s'agit en réalité de deux adresses. L'adresse de l'élément tête puis celle de queue.

Voilà un exemple de la représentation interne de cette pile :

(31, (12, (55, (20, None))))

Nous reviendrons sur cette construction, c'est une bonne méthode pour construire la structure de type liste chaînée ; oui, ça vient ensuite !

L'intérêt de ce genre de définition est qu'il est très commode de construire d'autres méthodes qui se prêtent bien à la récursivité. Par exemple :

    def hauteur(self):
        """Renvoie la hauteur de la pile"""
        if self.est_vide():
            return 0
        tête, queue = self.__données
        return 1 + Pile(queue).hauteur()

Dit autrement : la hauteur d'une pile c'est zéro si la pile est vide, sinon, c'est un, plus, la hauteur de la pile qui est sous l'élément au sommet.

Exercice 1 : Proposer une méthode récursive qui renvoie la somme des valeurs d'une telle pile, en supposant qu'il ne s'agisse que de nombres entiers.

Exercice 2 : Proposer une méthode récursive .contient_valeur(self, valeur) qui renvoie un booléen, True si un élément possède une certaine valeur, et False sinon.

On rappelle au passage un point technique, la différence entre élément et valeur. Regardons l'exemple ci-dessous.

>>> 2 == 2.0 # même valeur ?
True
>>> 2 is 2.0 # même élément ?
False

⚠️ On fera attention.

(Pour aller plus loin) : Un autre intérêt de cette présentation est son approche d'un style de programmation, le filtrage par motif (pattern matching).

Utilisations concrètes

Glossaire anglais - français

push
c'est le terme qu'on retrouve le plus pour empiler, enfiler. On trouve aussi append avec Python.
pop
c'est le terme qu'on retrouve le plus pour dépiler, défiler.

Liens wikipedia en anglais et en français pour ceux qui veulent aller plus loin :

English Français
data structure structure de données
array tableau
stack pile
queue file
pattern matching filtrage par motif
push empiler, ou enfiler
pop dépiler, ou défiler

List of data structures ; en anglais.