Données Pile Implémentation

Identification

Infoforall

18 - Implémenter une Pile


Nous sommes parvenus à implémenter le type Liste sous la forme de plusieurs structures de données ayant des comportements internes différents mais ayant les mêmes fonctions d'interface primitives.

  1. Liste contiguë IMMUABLE avec des tableaux statiques : mauvaise idée.
  2. Insertion et suppression LINEAIRE partout.

  3. Liste contiguë MUABLE avec des tableaux dynamiques, comme le type list de Python :
  4. Insertion et suppression à coût LINEAIRE sur la Tête.

    Insertion et suppression à coût CONSTANT sur la Fin

  5. Liste chaînée IMMUABLE avec un tuple.
  6. Insertion et suppression à coût CONSTANT sur la Tête

    Insertion et suppression à coût LINEAIRE sur la Fin.

  7. Liste chaînée MUABLE avec des Classes Cellule et une Liste qui pointe uniquement la Tête.
  8. Insertion et suppression à coût CONSTANT sur la Tête

    Insertion et suppression à coût LINEAIRE sur la Fin.

  9. Liste chaînée MUABLE avec des Classes Cellule et une Liste qui pointe la Tête et la Fin.
  10. Insertion et suppression à coût CONSTANT sur la Tête

    Insertion à coût CONSTANT et suppression à coût LINEAIRE sur la Fin.

Aujourd'hui, nous allons implémenter le type abstrait Pile.

Prerequis : l'activité sur la Pile et celles sur les implémentations des Listes.

Documents de cours : open document ou pdf

1 - Implémentation d'une pile via un tableau dynamique

1.1 Schéma de principe avec la list Python

Réfléchissons :

  1. Nous voulons implémenter la Pile sous forme d'un tableau de type list.
  2. Nous savons que l'insertion et la suppression sur l'indice 0 est à coût LINEAIRE dans ces tableaux dynamiques.
  3. Nous savons que l'insertion et la suppression sur l'indice de fin (avec les méthodes append() et pop() est à coût amorti CONSTANT dans ces tableaux dynamiques.
  4. CONCLUSION : nous allons placer le SOMMET de notre Pile du côté de l'indice final.

Les données de la Pile sont dans un tableau.

On place le Sommet de la Pile à la fin du tableau pour profiter des coûts constants des méthodes append() et pop().

graph LR A("[0]") --- B("[1]") --- C("[2]") -.- D("[n-2]") --- E("[n-1] Sommet") -. empilement -.- I(("+")) E -. dépilement -.- F(("-")) style A fill:#99f,stroke:#333,stroke-width:2px style E fill:#ff9,stroke:#333,stroke-width:2px style B fill:#99f,stroke:#333,stroke-width:2px style C fill:#99f,stroke:#333,stroke-width:2px style D fill:#99f,stroke:#333,stroke-width:2px
(Rappel) 1.2 Pile sous forme de list Python

A - Les primitives désirées pour la Pile MUABLE
  1. nouvelle_pile() -> 'Pile VIDE'
  2. est_pile_vide(p:'Pile') -> bool
  3. empiler(elt:'Elt', p:'Pile') -> None
  4. depiler(p:'Pile NON VIDE') -> 'Elt'
  5. lire_sommet(p:'Pile NON VIDE') -> 'Elt'
  6. taille(p:'Pile') -> int
B - Implémentation avec le type list
  1. nouvelle_pile() -> 'Pile VIDE'
  2. Deux versions possibles.

    >>> p = list()
    >>> p = []

    Coût CONSTANT dans les deux cas.

  3. est_pile_vide(p:'Pile') -> bool
  4. >>> p == [] True

    Coût CONSTANT.

  5. empiler(elt:'Elt', p:'Pile') -> None
  6. >>> p.append(50) # on place 50 au sommet >>> p == [] False >>> p.append(20) # on place 20 au sommet >>> p.append(10) # on place 10 au sommet

    Le sommet contient donc 10, et on trouvera 20 et 50 en dessous.

    Nous avons vu que, dans le cas du tableau dynamique, le coût amorti est CONSTANT.

  7. depiler(p:'Pile NON VIDE') -> 'Elt'
  8. >>> p.pop() # on dépile le sommet 10

    On récupère la valeur de l'ancien sommet. Le sommet est maintenant le 20.

    Nous avons vu que, dans le cas du tableau dynamique, le coût est CONSTANT.

  9. lire_sommet(p:'Pile NON VIDE') -> 'Elt'
  10. On sait que le sommet est à la fin de notre tableau dynamique. Deux versions pour lire ce que contient la dernière case du tableau.

    >>> p[len(p)-1] 20
    >>> p[-1] 20

    Le coût est CONSTANT.

  11. taille(p:'Pile') -> int
  12. >>> len(p) 2

    Le pile contient bien le sommet 20 suivi d'un 50. Deux éléments.

    Le coût est CONSTANT car le nombre d'éléments est stocké directement dans l'objet.

C - Erreurs classiques sur les muables

La pile est modifiée en place.

N'écrivez donc JAMAIS ceci avec une pile muable :

>>> p = p.append(50) # p contient maintenant None, bravo... >>> p = p.pop() # p n'est plus une pile mais un élément, bravo...
1.3 - Quelques explications sur le contenu interne du tableau

On notera que les instructions correspondent bien à un type mutable) : on ne stocke pas la réponse d'empiler() puisque cette réponse est ... None.

>>> a = nouvelle_pile() >>> empiler(a, 5)
Tableau contient 5
>>> empiler(a, 10)
Tableau contient 5-10
>>> empiler(a, 20)
Tableau contient 5-10-20
>>> depiler(a) 20
Tableau contient 5-10

01° Compléter les différentes primitives pour parvenir à implémenter notre Pile MUABLE sous forme d'un tableau dynamique à l'interne.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
"""MODULE d'implémentation de type abstrait PILE MUABLE en utilisant des TABLEAUX DYNAMIQUES La Pile vide sera représentée par un tableau vide []. Le sommet d'une Pile non vide est situé en fin de tableau. Primitives -------------------- ... : nouvelle_pile() -> 'Pile VIDE' ... : est_pile_vide(p:'Pile') -> bool ... : empiler(p:'Pile', elt:'Element') -> None ... : depiler(p:'Pile NON VIDE') -> 'Element' ... : lire_sommet(p:'Pile NON VIDE') -> 'Element' """ def nouvelle_pile() -> 'Pile VIDE': """Renvoie une nouvelle Pile vide""" pass def est_pile_vide(p:'Pile') -> bool: """Prédicat qui renvoie True si p est une Pile vide""" pass def empiler(p:'Pile', elt:'Element' ) -> None: """Modifie la pile p en rajouter elt au sommet""" pass def depiler(p:'Pile NON VIDE') -> 'Element': """Supprime le sommet de la Pile p et renvoie l'élément supprimé""" pass def lire_sommet(p:'Pile NON VIDE') -> 'Element': """Renvoie la valeur actuellement au sommet de la Pile p NON VIDE""" pass if __name__ == '__main__': a = nouvelle_pile() empiler(a, 0) empiler(a, 5) empiler(a, 10) empiler(a, 15) s = lire_sommet(a) assert s == 15 depiler(a) s = lire_sommet(a) assert s == 10

02° Estimer le coût des cinq fonctions primitives.

...CORRECTION...

Difficile de répondre sans connaître les codes des méthodes correspondantes de Python. C'est bien cela le problème de cette façon de faire.

Si on suppose que

  • l'accès à la dernière case avec les crochets est à coût constant,
  • le rajout d'une case en fin de tableau avec append() et
  • la suppression de la dernière case pop() est à coût constant,

alors les opérations sur le sommet (lecture/modification et insertion/suppression) sont toutes à coût constant.

(Bilan Correction) 1.4 Module de Pile implémentée via list

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
"""MODULE d'implémentation de type abstrait PILE MUABLE en utilisant des TABLEAUX DYNAMIQUES La Pile vide sera représentée par un tableau vide []. Le sommet d'une Pile non vide est situé en fin de tableau. Fonctions primitives -------------------- CST : nouvelle_pile() -> 'Pile VIDE' CST : est_pile_vide(p:'Pile') -> bool CST : empiler(p:'Pile', elt:'Element') -> None CST : depiler(p:'Pile NON VIDE') -> 'Element' CST : lire_sommet(p:'Pile NON VIDE') -> 'Element' """ def nouvelle_pile() -> 'Pile VIDE': """Renvoie une nouvelle Pile vide""" return [] def est_pile_vide(p:'Pile') -> bool: """Prédicat qui renvoie True si p est une Pile vide""" return p == [] def empiler(p:'Pile', elt:'Element' ) -> None: """Modifie la pile p en rajouter elt au sommet""" p.append(elt) # rajoute un élément à la fin du tableau def depiler(p:'Pile NON VIDE') -> 'Element': """Supprime le sommet de la Pile p et renvoie l'élément supprimé""" return p.pop() # supprime et renvoie le dernier élément du tableau def lire_sommet(p:'Pile NON VIDE') -> 'Element': """Renvoie la valeur actuellement au sommet de la Pile p NON VIDE""" return p[len(p)-1] # ou même return p[-1] if __name__ == '__main__': a = nouvelle_pile() empiler(a, 0) empiler(a, 5) empiler(a, 10) empiler(a, 15) s = lire_sommet(a) assert s == 15 depiler(a) s = lire_sommet(a) assert s == 10

Attention : dans la version MUABLE (ou MUTABLE en franglais), la pile est modifiée directement en mémoire. Pour modifier une pile, il faut donc écrire quelque chose comme empiler(a, 15) puisque la fonction ne renvoie rien. N'écrivez SURTOUT PAS a = empiler(a, 15). Sinon, votre Pile contiendra None après exécution !

03° Utiliser cette implémentation pour constater qu'on a bien une Pile et, qu'à part la mutabilité, on ne peut pas savoir comment tout cela est implémenté à l'interne tant qu'on utilise uniquement les fonctions d'interface.

>>> a = nouvelle_pile() >>> empiler(a, 5) >>> a [5] >>> empiler(a, 10) >>> a [5, 10] >>> empiler(a, 20) >>> a [5, 10, 20] >>> depiler(a) 20 >>> a [5, 10] >>> depiler(a) 10 >>> a [5] >>> empiler(a, 40) >>> a [5, 40] >>> depiler(a) 40 >>> a [5] >>> depiler(a) 5

Notez bien le comportement en PILE : le 40, qu'on rajoute plus tard, sort immédiatement alors qu'il est arrivé bien après : comportement LIFO.

Ce dernier exemple vous permet aussi de vous rendre compte que la pile fonctionne vraiment comme une pile d'assiettes ou de legos : on récupère nécessairement le dernier élément qu'on a inséré dans la Pile.

2 - Implémentation d'une Pile via une liste chaînée de tuples

2.1 Schéma de principe avec une tuple Python

Nous allons implémenter une Pile IMMUABLE en reprenant l'idée d'une liste chaînée dont les cellules sont des tuples (valeur de Tête, Queue) en remplaçant les noms des éléments pour qu'ils soient ceux de la Pile : un tuple (valeur du sommet, reste de la Pile).

Réfléchissons :

  1. Nous voulons implémenter la Pile sous forme d'un tuple n'ayant que deux éléments : la valeur à l'indice 0 et la cellule suivante sur l'indice 1.
  2. Nous savons que l'insertion et la suppression sur la tête de la liste chainée est à coût CONSTANT.
  3. Nous savons que l'insertion et la suppression sur la cellule de fin est à côut LINEAIRE.
  4. CONCLUSION : nous allons placer le SOMMET de notre Pile du côté de la TETE de la Liste chaînée.

Les données de la Pile sont dans les cellules de la liste chaînée.

On place le Sommet de la Pile sur la Tête de la liste chaînée pour profiter des coûts constants des fonctions inserer_tete() et supprimer_tete().

graph LR F(("+")) -.- empilement -.- A("Tete et Sommet") --> B("Cellule 1") --> C("Cellle 2") -.-> D("Cellule n-2") ---> E("Cellule n-1 de Fin") I(("+")) -.- dépilement -.- A style E fill:#99f,stroke:#333,stroke-width:2px style A fill:#ff9,stroke:#333,stroke-width:2px style B fill:#99f,stroke:#333,stroke-width:2px style C fill:#99f,stroke:#333,stroke-width:2px style D fill:#99f,stroke:#333,stroke-width:2px

04° Compléter les différentes primitives pour parvenir à implémenter notre Pile IMMUABLE sous forme du tuple à l'interne.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
"""MODULE d'implémentation de type abstrait PILE IMMUABLE en utilisant des Cellules TUPLES pour former une liste chaînée La Pile vide sera représentée par un tuple vide (). Le sommet d'une Pile non vide est situé du même côté que la tête de la liste chaînée associée. Fonctions primitives -------------------- ... : nouvelle_pile() -> 'Pile VIDE' ... : est_pile_vide(p:'Pile') -> bool ... : empiler(p:'Pile', elt:'Element') -> 'Pile' ... : depiler(p:'Pile NON VIDE') -> 'Pile' ... : lire_sommet(p:'Pile NON VIDE') -> 'Element' """ def nouvelle_pile() -> 'Pile': """Renvoie une nouvelle Pile vide""" pass def est_pile_vide(p:'Pile') -> bool: """Prédicat qui renvoie True si p est une Pile vide""" pass def empiler(p:'Pile', elt:'Element' ) -> 'Pile': """Renvoie une pile basée sur p mais en rajoutant elt au sommet""" pass def depiler(p:'Pile NON VIDE') -> 'Pile': """Renvoie une pile basée sur p mais en supprimant le sommet""" pass def lire_sommet(p:'Pile NON VIDE') -> 'Element': """Renvoie la valeur actuellement au sommet de la Pile p NON VIDE""" pass if __name__ == '__main__': a = nouvelle_pile() a = empiler(a, 0) a = empiler(a, 5) a = empiler(a, 10) a = empiler(a, 15) s = lire_sommet(a) assert s == 15 a = depiler(a) s = lire_sommet(a) assert s == 10

05° Estimer le coût des cinq fonctions primitives.

...CORRECTION...

On retrouve les mêmes coûts qu'avec la liste chaînée dont les cellules sont des tuples : toutes les opérations sont à coût CONSTANT sur la tête.

Et c'est pour cela qu'on a placer le sommet du côté de la tête.

(Bilan Correction) 2.2 Module de Pile implémentée via tuple

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
"""MODULE d'implémentation de type abstrait PILE IMMUABLE en utilisant des Cellules TUPLES pour former une liste chaînée La Pile vide sera représentée par un tuple vide (). Le sommet d'une Pile non vide est situé du même côté que la tête de la liste chaînée associée. Fonctions primitives -------------------- CST : nouvelle_pile() -> 'Pile VIDE' CST : est_pile_vide(p:'Pile') -> bool CST : empiler(p:'Pile', elt:'Element') -> 'Pile NON VIDE' CST : depiler(p:'Pile NON VIDE') -> 'Pile' CST : lire_sommet(p:'Pile NON VIDE') -> 'Element' """ def nouvelle_pile() -> 'Pile VIDE': """Renvoie une nouvelle Pile vide""" return () def est_pile_vide(p:'Pile') -> bool: """Prédicat qui renvoie True si p est une Pile vide""" return p == () def empiler(p:'Pile', elt:'Element' ) -> 'Pile NON VIDE': """Renvoie une pile basée sur p mais en rajoutant elt au sommet""" return (elt, p) # renvoie un tuple contenant elt et menant ensuite à la pile p reçue def depiler(p:'Pile NON VIDE') -> 'Pile': """Renvoie une pile basée sur p mais en supprimant le sommet""" return p[1] # renvoie un tuple qui est le reste de la pile p def lire_sommet(p:'Pile NON VIDE') -> 'Element': """Renvoie la valeur actuellement au sommet de la Pile p NON VIDE""" return p[0] # renvoie la valeur stockée dans la cellule Sommet if __name__ == '__main__': a = nouvelle_pile() a = empiler(a, 0) a = empiler(a, 5) a = empiler(a, 10) a = empiler(a, 15) s = lire_sommet(a) assert s == 15 a = depiler(a) s = lire_sommet(a) assert s == 10

Attention : dans la version IMMUABLE, la pile n'est pas vraiment modifiée. Pour modifier une pile, il faut donc écrire quelque chose comme a = empiler(a, 15)

06° Utiliser cette implémentation pour constater qu'on a bien une Pile et, qu'à part la mutabilité, on ne peut pas savoir comment tout cela est implémenté à l'interne tant qu'on utilise uniquement les fonctions d'interface.

>>> a = nouvelle_pile() >>> a = empiler(a, 5) >>> a (5, ()) >>> a = empiler(a, 10) >>> a (10, (5, ())) >>> a = empiler(a, 20) >>> a (20, (10, (5, ()))) >>> a = depiler(a) (10, (5, ())) >>> a (10, (5, ())) >>> depiler(a) (5, ()) >>> a (10, (5, ())) # on voit que a n'a pas été modifié car on a juste écrit depiler(a) >>> a = empiler(a, 40) >>> a (40, (10, (5, ()))) >>> a = depiler(a) (10, (5, ())) >>> a (10, (5, ())) # on voit que a a été modifié car on a écrit a = depiler(a) >>> a = depiler(a) (5, ())

3 - Implémentation d'une pile via une liste chaînée basée sur des cellules-objets

3.1 Schéma de principe avec une liste chaînée Python

Nous allons ici implémenter une Pile MUABLE en reprenant l'idée d'une liste chaînée dont les cellules sont des instances d'une classe Cellule n'ayant que deux attributs : v pour la valeur et s pour le successeur.

Réfléchissons :

  1. Nous voulons implémenter la Pile sous forme d'une liste chaînée simple. Comme pour la liste chaînée précédente, on sait qu'on ne parvient à insérer et supprimer à coût constant qu'au niveau de la Tête.
  2. CONCLUSION : nous allons placer le SOMMET de notre Pile du côté de la TETE de la Liste chaînée.

Les données de la Pile sont dans les cellules de la liste chaînée.

On place le Sommet de la Pile sur la Tête de la liste chaînée pour profiter des coûts constants des fonctions inserer_tete() et supprimer_tete().

graph LR F(("+")) -.- empilement -.- A("Tete et Sommet") --> B("Cellule 1") --> C("Cellle 2") -.-> D("Cellule n-2") ---> E("Cellule n-1 de Fin") I(("+")) -.- dépilement -.- A style E fill:#99f,stroke:#333,stroke-width:2px style A fill:#ff9,stroke:#333,stroke-width:2px style B fill:#99f,stroke:#333,stroke-width:2px style C fill:#99f,stroke:#333,stroke-width:2px style D fill:#99f,stroke:#333,stroke-width:2px

07° Compléter les différentes primitives pour parvenir à implémenter notre Pile MUABLE sous forme d'objets à l'interne.

Commencez par réaliser les méthodes de l'objet puis compléter les fonctions pour qu'un utilisateur utilisant vos fonctions ne se rende pas compte qu'on utilise des objets pour implémenter la Pile.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
"""MODULE d'implémentation d'une Pile sous forme d'une liste Chaînée MUABLE en utilisant des Cellules-Objets pour former une liste chaînée où la Tete de la liste correspond au Sommet de la Pile. On peut manipuler directement l'objet Pile via les méthodes. On peut utiliser les fonctions sans se rendre compte qu'on manipule un objet. Méthodes primitives de la classe Pile ------------------------------------- CST : Pile() -> 'Pile VIDE' CST : Pile.est_pile_vide(p:'Pile') -> bool CST : Pile.empiler(p:'Pile', elt:'Element') -> None CST : Pile.depiler(p:'Pile NON VIDE') -> 'Element' CST : Pile.lire_sommet(p:'Pile NON VIDE') -> 'Element' Fonctions primitives de la Pile ------------------------------- CST : Pile() -> 'Pile VIDE' CST : Pile.est_pile_vide(p:'Pile') -> bool CST : Pile.empiler(p:'Pile', elt:'Element') -> None CST : Pile.depiler(p:'Pile NON VIDE') -> 'Element' CST : Pile.lire_sommet(p:'Pile NON VIDE') -> 'Element' """ # Déclaration des classes Cellule et Pile class Cellule: def __init__(self, valeur:'Element', successeur:'Cellule|None'): self.v = valeur # Valeur ne peut pas être None si liste homogène self.s = successeur # successeur vaut None si Cellule de Fin class Pile: def __init__(self): self.sommet = None def est_pile_vide(self:'Pile') -> bool: pass def empiler(self:'Pile', elt:'Element') -> None: pass def depiler(self:'Pile NON VIDE') -> 'Element': pass def lire_sommet(self:'Pile NON VIDE') -> 'Element': """Renvoie la valeur au sommet de la Pile""" pass # Déclaration des fonctions primitives si on veut "cacher" l'objet def nouvelle_pile() -> 'Pile VIDE': pass def est_pile_vide(p:'Pile') -> bool: """Prédicat qui renvoie True si p est une Pile vide""" pass def empiler(p:'Pile', elt:'Element' ) -> None: """Rajoutant elt au sommet de la pile p""" pass def depiler(p:'Pile NON VIDE') -> 'Element': """Dépile et renvoie le sommet de la pile p""" pass def lire_sommet(p:'Pile NON VIDE') -> 'Element': """Renvoie la valeur actuellement au sommet de la Pile p NON VIDE""" pass # Programme-test if __name__ == '__main__': p = nouvelle_pile() empiler(p, 0) empiler(p, 5) empiler(p, 10) assert lire_sommet(p) == 10 depiler(p) assert lire_sommet(p) == 5

08° Estimer le coût des cinq fonctions primitives.

...CORRECTION...

On retrouve les mêmes coûts qu'avec la liste chaînée dont les cellules sont des tuples : toutes les opérations sont à coût CONSTANT sur la tête.

Et c'est pour cela qu'on a placer le sommet du côté de la tête.

(Bilan Correction) 3.2 Module de Pile implémentée via liste chaînée

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
"""MODULE d'implémentation d'une Pile sous forme d'une liste Chaînée MUABLE en utilisant des Cellules-Objets pour former une liste chaînée où la Tete de la liste correspond au Sommet de la Pile. On peut manipuler directement l'objet Pile via les méthodes. On peut utiliser les fonctions sans se rendre compte qu'on manipule un objet. Méthodes primitives de la classe Pile ------------------------------------- CST : Pile() -> 'Pile VIDE' CST : Pile.est_pile_vide(p:'Pile') -> bool CST : Pile.empiler(p:'Pile', elt:'Element') -> None CST : Pile.depiler(p:'Pile NON VIDE') -> 'Element' CST : Pile.lire_sommet(p:'Pile NON VIDE') -> 'Element' Fonctions primitives de la Pile ------------------------------- CST : nouvelle_pile() -> 'Pile VIDE' CST : est_pile_vide(p:'Pile') -> bool CST : empiler(p:'Pile', elt:'Element') -> None CST : depiler(p:'Pile NON VIDE') -> 'Element' CST : lire_sommet(p:'Pile NON VIDE') -> 'Element' """ # Déclaration des classes Cellule et Pile class Cellule: def __init__(self, valeur:'Element', successeur:'Cellule|None'): self.v = valeur # Valeur ne peut pas être None si liste homogène self.s = successeur # successeur vaut None si Cellule de Fin class Pile: def __init__(self): self.sommet = None def est_pile_vide(self:'Pile') -> bool: return self.sommet is None def empiler(self:'Pile', elt:'Element') -> None: """Rajoute une nouvelle cellule contenant elt au sommet de la Pile""" nouvelle = Cellule(elt, None) # Etape 1 : création de la cellule nouvelle.s = self.sommet # Etape 2 : on relie la nouvelle cellule à l'"ancien" sommet self.sommet = nouvelle # Etape 3 : modification de la liste def depiler(self:'Pile NON VIDE') -> 'Element': """Supprime la cellule de tête et renvoie l'élément qui y était stocké""" ancienne_valeur = self.sommet.v # Etape 1 : on mémorise la valeur au sommet self.sommet = self.sommet.s # Etape 2 : le sommet devient le successeur du sommet supprimé return ancienne_valeur # Etape 3 : on renvoie la valeur stockée def lire_sommet(self:'Pile NON VIDE') -> 'Element': """Renvoie la valeur au sommet de la Pile""" return self.sommet.v # Déclaration des fonctions primitives si on veut "cacher" l'objet def nouvelle_pile() -> 'Pile VIDE': """Renvoie une nouvelle Pile vide""" return Pile() def est_pile_vide(p:'Pile') -> bool: """Prédicat qui renvoie True si p est une Pile vide""" return p.est_pile_vide() def empiler(p:'Pile', elt:'Element' ) -> None: """Rajoutant elt au sommet de la pile p""" p.empiler(elt) def depiler(p:'Pile NON VIDE') -> 'Element': """Dépile et renvoie le sommet de la pile p""" return p.depiler() def lire_sommet(p:'Pile NON VIDE') -> 'Element': """Renvoie la valeur actuellement au sommet de la Pile p NON VIDE""" return p.lire_sommet() # Programme-test if __name__ == '__main__': p = nouvelle_pile() empiler(p, 0) empiler(p, 5) empiler(p, 10) assert lire_sommet(p) == 10 depiler(p) assert lire_sommet(p) == 5

4 - Mini-Projet pour les plus rapides : Pile sous forme dictionnaire + tableau statique

Bon, à vous de travailler : on veut créer une implémentation de Pile muable ne pouvant contenir que 10 élements.

Le principe est le suivant :

  • On crée un dictionnaire qui encodera la pile et qui intégre
    • le nombre d'éléments dans la Pile via une clé "nbr",
    • le nombre d'éléments maximum dans la Pile via une clé "max",
    • les données de la Pile via une clé "data"
  • On crée un tableau statique contenant 10 cases vides. Ce tableau est destiné à contenir les données de notre Pile et sa référence est la valeur associée à la clé "data".

Un exemple d'utilisation qui devra fonctionner de la même manière avec votre implémentation une fois qu'elle sera fonctionnelle.

>>> a = nouvelle_pile() >>> empiler(5, a) >>> a {'nb': 1, 'max': 10, 'data': [5, None, None, None, None, None, None, None, None, None]} >>> empiler(10, a) >>> a {'nb': 2, 'max': 10, 'data': [5, 10, None, None, None, None, None, None, None, None]} >>> empiler(20, a) >>> a {'nb': 3, 'max': 10, 'data': [5, 10, 20, None, None, None, None, None, None, None]} >>> est_pile_vide(a) False >>> est_pile_pleine(a) False >>> taille(a) 3 >>> depiler(a) 20 >>> a {'nb': 2, 'max': 10, 'data': [5, 10, None, None, None, None, None, None, None, None]} >>> empiler(40, a) >>> a {'nb': 3, 'max': 10, 'data': [5, 10, 40, None, None, None, None, None, None, None]} >>> depiler(a) 40 >>> depiler(a) 10 >>> depiler(a) 5 >>> depiler(a) >>> a {'nb': -1, 'max': 10, 'data': [None, None, None, None, None, None, None, None, None, None]}

Remarquez bien que comme l'utilisateur a dépilé une pile vide, le compteur final donne quelque chose d'aberrant : la taille de la pile vaudrait -1 éléments !

Et voici le début de l'implémentation. A vous de la finir !

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
"""Implémentation de type abstrait PILE MUABLE en utilisant un dictionnaire et un tableau statique""" # Phase d'importation # Phase de déclaration des CONSTANTES et variables globales TAILLE = 10 # Phase de déclaration des classes et fonctions def nouvelle_pile() -> 'Pile': """Renvoie la référence d'une nouvelle Pile de TAILLE limitée""" p = {} p['nb'] = 0 # contient le nombre de données stockées p['max'] = TAILLE p['data'] = [None for x in range(TAILLE)] # Aucune donnée au début return p def est_pile_vide(p:'Pile') -> bool: """Prédicat qui renvoie True si la Pile est bien vide""" return p['nb'] == 0 def est_pile_pleine(p:'Pile') -> bool: """Prédicat qui renvoie True si la Pile est pleine""" pass def empiler(elt:'Elt', p:'Pile NON PLEINE') -> None: """Empile l'élément dans une pile NON PLEINE""" pass def depiler(p:'Pile NON VIDE') -> 'Elt': """Dépile et renvoie le sommet d'une pile NON VIDE""" pass def lire_sommet(p:'Pile NON VIDE') -> 'Elt': """Renvoie la donnée stockée au sommet de la pile NON VIDE p""" pass def taille(p:'Pile') -> int: pass def tester(): """Fonction ne servant qu'à valider en partie vos fonctions d'interface""" print("DEBUT DU JEU DE TEST...") a = nouvelle_pile() assert a == {'nb': 0, 'max': 10, 'data': [None, None, None, None, None, None, None, None, None, None]} print("... Création : ok") empiler(5, a) assert a == {'nb': 1, 'max': 10, 'data': [5, None, None, None, None, None, None, None, None, None]} empiler(10, a) assert a == {'nb': 2, 'max': 10, 'data': [5, 10, None, None, None, None, None, None, None, None]} empiler(20, a) assert a == {'nb': 3, 'max': 10, 'data': [5, 10, 20, None, None, None, None, None, None, None]} print("... Empilement : ok") assert est_pile_vide(a) == False assert est_pile_pleine(a) == False assert taille(a) == 3 assert depiler(a) == 20 assert a == {'nb': 2, 'max': 10, 'data': [5, 10, None, None, None, None, None, None, None, None]} empiler(40, a) assert a == {'nb': 3, 'max': 10, 'data': [5, 10, 40, None, None, None, None, None, None, None]} assert depiler(a) == 40 assert depiler(a) == 10 assert depiler(a) == 5 assert a == {'nb': 0, 'max': 10, 'data': [None, None, None, None, None, None, None, None, None, None]} print("... Dépilement : ok") print("FIN DU JEU DE TESTS.") # --- Programme principal --- if __name__ == '__main__': import doctest DEBUG = False # Passer la constante à True pour activer le jeu de tests via assertions if DEBUG: tester() doctest.testmod() # Pour utiliser la documentation comme jeu de tests

✎ 09° Voici la notation :

  1. Implémentation correcte : 8 points
  2. Utiliser autant que possible les primitives déjà réalisées pour réaliser de nouvelles fonctions : 2 points
  3. Justification du type de coût en lecture, en insertion et en suppression : 2 points
  4. Création d'une fonction swap() qui inverse le sommet et l'élément suivant pour une pile d'au moins 2 éléments : 2 points
  5. Création d'une fonction vider() qui vide la pile NON VIDE : 2 points
  6. Documentation des différentes fonctions d'interface : 2 points
  7. Réalisation d'assertions pour vérifier que les piles à gérer ne sont pas vides avant lecture ou suppression, ni pleine en cas d'insertion, que la pile contient bien au moins deux éléments avant swap... : 2 points.

La prochaine fois, on passe aux files.

Activité publiée le 14 10 2020
Dernière modification : 16 10 2021
Auteur : ows. h.