Pile
Une Pile (ou Stack) est une structure de données linéaire dans laquelle les éléments sont accessibles selon une discipline LIFO (“Last-In First-Out”) : l’élément inséré en dernier est le premier à sortir.
- Ajouter un élément dans la pile est appelé empiler ou pousser (push)
- Supprimer un élément de la pile est appelé dépiler (pop)
L’accès aux objets de la pile se fait grâce à un unique pointeur vers le sommet (top) de la pile.
Interface élémentaire
Rappel sur la notion d'interface
On appelle
interface d’une structure de données l’ensemble des opérations et attributs (les
primitives) à associer à cette structure de données pour permettre sa manipulation.
En POO, cela équivaut à l’ensemble des attributs et méthodes publiques.
est_vide(S)
ou is_empty(S)
renvoie vrai si la pile S
est vide
empiler(S, e)
ou push(S, e)
empile la valeur e
sur la pile S
depiler(S)
ou pop(S)
extrait et renvoie la valeur sur le sommet de la pile S
ATTENTION ! on ne peut dépiler un élément qu’une seule fois ! Une fois dépilé, il n’est plus sur la pile !
Coût en temps
- Obtenir l’élément placé sur le dessus d’une pile : cout constant \(\mathcal{O}(1)\)
- Obtenir l’élément placé en bas d’une pile : coût linéaire \(\mathcal{O}(n)\)
- Obtenir le nombre d’éléments contenus dans une pile : coût linéaire \(\mathcal{O}(n)\)
Applications
- Fonctionnalité Undo/Redo dans les logiciels
- Analyse syntaxique
- …
Opérations
Parcourir tous les éléments d’une pile (pour les afficher par exemple) :
Inconvénient : à la fin de l’opération, la pile initiale est vide !!
Pour éviter cela, on peut employer une pile temporaire, sur laquelle on empile les éléments avant de les afficher :
Mais cette pile temporaire se retrouve inversée par rapport à la pile initiale.
Il faut la reconstruire : on dépile la pile temporaire et on rempile celle d’origine.
Implémentations
Avec une Liste chaînée
Voici un module contenant une classe de liste chaînée ListeC
, possédant, en autres, les méthodes .est_vide()
, .ajouter_debut(m)
, .ajouter_fin(m)
, .supprimer_debut()
, ……..
Module listes_chainees.py
####################################################################################
# Déclaration des Types
####################################################################################
class Maillon():
def __init__(self, data = None):
self.data = data
self.suiv = None # adresse (en Python, on pointe directement vers un Maillon)
def __repr__(self):
t = str(self.data)
if self.suiv is not None:
return t + "→"
return t + "x"
####################################################################################
class ListeC():
def __init__(self):
self.tete = None # adresse (en Python, on pointe directement vers un Maillon)
def __repr__(self):
m = self.tete
l = []
while m is not None:
l.append(str(m))
m = m.suiv
return "\n".join(l)
def est_vide(self):
""" Renvoie True si la liste est vide
"""
return self.tete is None
def taille(self):
""" Renvoie le nombre de maillons de la liste
"""
m = self.tete
l = 0
while m is not None:
l += 1
m = m.suiv
return l
############################################################################
def get_dernier(self):
""" Renvoie le dernier Maillon de la liste
IndexError si la liste est vide
"""
if self.est_vide():
raise IndexError("listeC index out of range")
m = self.tete
while m.suiv is not None:
m = m.suiv
return m
############################################################################
def ajouter_fin(self, v):
""" Ajoute un élément à la fin de la liste chaînée
"""
nM = Maillon(v)
if self.est_vide():
self.tete = nM
else:
dm = self.get_dernier()
dm.suiv = nM
def ajouter_debut(self, v):
""" Ajoute un élément au début de la liste chaînée
"""
nM = Maillon(v)
if self.est_vide():
self.tete = nM
else:
nM.suiv = self.tete
self.tete = nM
############################################################################
def supprimer_debut(self):
""" Supprime et renvoie le premier élément de la liste chaînée
IndexError si la liste est vide
"""
if self.tete is not None:
m = self.tete
self.tete = self.tete.suiv
return m.data
else:
raise IndexError("listeC index out of range")
def supprimer_fin(self):
""" Supprime et renvoie le dernier élément de la liste chaînée
IndexError si la liste est vide
"""
if self.est_vide():
raise IndexError("listeC index out of range")
m = self.tete
if m.suiv is None: # 1 seul élément
self.tete = None
return m.data
a = None # avant dernier
while m.suiv is not None:
a = m
m = m.suiv
a.suiv = None
return m.data
On peut utiliser la classes ListeC
de ce module dans un autre programme en utilisant l’instruction :
from listes_chainees import ListeC
Définir en Python une classe Pile
en utilisant une liste chaînée (objet de type ListeC
du module listes_chainees
) comme conteneur de données.
À l’aide de seulement 3 méthodes de la classe ListeC
, réaliser l’interface complète de cette pile (méthodes est_vide
, empiler
, et depiler
).
À l’aide uniquement des 3 méthodes de l’interface de Pile
, réaliser une méthode de représentation (__repr__
).
Avec un Tableau
Définir en Python une classe Pile
en utilisant une liste Python comme conteneur de données.
À l’aide de seulement 2 méthodes et une fonction, réaliser l’interface complète de cette pile.
Correction (ne pas regarder !)
class Pile:
def __init__(self):
self.__l = [] # attribut privé : on ne doit utiliser que l'interface élémentaire
# Pour internationaliser les méthodes :
self.pop = self.depiler
self.push = self.empiler
self.is_empty = self.est_vide
def __repr__(self):
l = []
p = Pile()
m = 0
while not self.est_vide():
c = self.depiler()
l.append(str(c))
p.empiler(c)
m = max(m, len(str(c)))
# on reconstruit la Pile d'origine
while not p.est_vide():
self.empiler(p.depiler())
# Pour afficher une bordure :
for i in range(len(l)):
l[i] = "│" + l[i].ljust(m) +"│"
return "\n".join(l) + "\n└"+ m*"─" + "┘"
def est_vide(self):
return len(self.__l) == 0
def depiler(self):
return self.__l.pop()
def empiler(self, n):
self.__l.append(n)
Quelques exercices
Tamisage
Écrire une fonction pairs
qui prend en entrée une pile d’entiers et qui la modifie en supprimant les entiers pairs, qui sont renvoyés dans une nouvelle pile.
Par exemple (avec la classe Pile
à base de liste Python définie plus haut) :
>>> P = Pile()
>>> P.push(1)
>>> P.push(4)
>>> P.push(5)
>>> P.push(2)
>>> P.push(3)
>>> P.push(6)
>>> P.push(8)
>>> P
8
6
3
2
5
4
1
>>> pairs(P)
8
6
2
4
>>> P
3
5
1
Donner l'ordre de complexité de cette fonction.
Exercice : entre parenthèses
Implémenter en Python, en utilisant une pile, une fonction permettant de vérifier l’appariement de parenthèses ([]
, ()
ou {}
) dans une chaîne de caractères.
Exemples :
[(x)+(y)]/{2*a-sin(x)}
→ True
[-(b)+sqrt(4*(a*c)])/(2*a)
→ False
[-(b)+sqrt(4*(a*c)])/(2*a
→ False
Pour aller plus loin ...
Modifier cette fonction de sorte qu'elle renvoie les associations de "parenthèses" : pour chaque indice de caractère de la chaîne, s'il s'agit d'une "parenthèse", l'indice de la "parenthèse" associée est spécifié.
Exemples : [(x)+(y)]/{2*a-sin(x)}
→ [8, 3, None, 1, None, 7, None, 5, 0, None, 21, None, None, None , None, None, None, None, 20, None, 18, 10]
.
Exercice : RPN
L'écriture polonaise inverse des expressions mathématiques place l'opérateur après les opérandes. Cette notation ne nécessite aucune parenthèse ni aucune règle de priorité.
Exemples : (1+2*3)*4
s'écrit en notation polonaise inverse 1 2 3 * + 4 *
.
Pour évaluer une telle expression, on utilise une pile pour stocker tous les résultats intermédiaires. Les éléments de l’expression sont évalués un à un :
- s'il s'agit d'un nombre, on le place au sommet de la pile,
- s'il s'agit d'une opérande, on réalise l'opération avec les deux premiers nombres de la pile (le deuxième "par" le premier), puis on replace le résultat sur la pile.
Si l'expression est bien écrite, à la fin du processus, la pile ne comporte plus que le résultat final.
Écrire une fonction rpn(expr)
permettant d'évaluer une expression polonaise inverse composée des opérateurs + - * /
, et dont les éléments sont séparés par des espaces.
Si l'expression est mal écrite, la fonction doit renvoyer une erreur de type SyntaxError
.
File
Une File (ou Queue) est une structure de données linéaire dans laquelle les éléments sont accessibles selon une discipline FIFO (“First-In First-Out”) : le premier élément inséré dans la liste est le premier à en sortir.
- Insérer un élément dans la file est appelé enfiler (enqueue)
- Supprimer un élément de la file est appelé défiler (dequeue)
L’accès aux objets de la file se fait grâce à deux pointeurs, l’un pointant sur l’élément qui a été inséré en premier et l’autre pointant sur celui inséré en dernier.
Interface élémentaire
est_vide(F)
ou is_empty(F)
renvoie vrai si la file F
est vide
enfiler(F, e)
ajoute l’élément e
à la queue de la file F
defiler(F)
retire et renvoie l’élément à la tête de la file F
Coût en temps
- Obtenir l’élément placé en tête de file : cout constant \(\mathcal{O}(1)\)
- Obtenir l’élément placé en queue de file : coût linéaire \(\mathcal{O}(n)\)
- Obtenir le nombre d’éléments contenus dans une file : coût linéaire \(\mathcal{O}(n)\)
Applications
- Mémoire tampon (buffer)
- Mise en attente
- …
Opérations
Parcourir tous les éléments d’une file (pour les afficher par exemple) :
Inconvénient : à la fin de l’opération, la file initiale est vide !!
Pour éviter cela, on peut employer une file temporaire, dans laquelle on enfile les éléments avant de les afficher :
La file initiale étant vide, il faut la re-remplir :
Implémentations
Avec une liste chainée
Définir une classe File
en utilisant un maillon de liste chainée. (voir module listes_chainnees
plus haut).
À l’aide uniquement des 3 méthodes de l’interface de File
, réaliser une méthode de représentation (__repr__
).
Avec deux piles
Définir en Python une classe File
à l’aide de deux objets entree
et sortie
de type Pile
(en utilisant une des classes Pile
définies plus haut).
Correction (ne pas regarder !)
class File:
def __init__(self):
self.entree = Pile()
self.sortie = Pile()
def __repr__(self):
""" La même que précédemment ... """
return
def est_vide(self):
return self.entree.est_vide() and self.sortie.est_vide()
def enfiler(self, n):
# On remet la pile d'entrée en place avant de pousser
while not self.sortie.est_vide():
self.entree.push(self.sortie.pop())
self.entree.push(n)
def defiler(self):
if self.sortie.est_vide():
raise IndexError("defilage d'une file vide")
while not self.entree.est_vide():
self.sortie.push(self.entree.pop())
return self.sortie.pop() # L'élément qu'il faut renvoyer
Avec un tableau
Définir en Python une classe File
en utilisant une liste Python comme conteneur de données.
À l’aide de seulement 2 méthodes et une fonction, réaliser l’interface complète de cette file.
Correction (ne pas regarder !)
class File:
def __init__(self):
self.__p = []
self.dequeue = self.defiler
self.enqueue = self.enfiler
self.is_empty = self.est_vide
def __repr__(self):
l = []
p = File()
while not self.est_vide():
c = self.defiler()
p.enfiler(c)
l.append(str(c))
while not p.est_vide():
self.enfiler(p.defiler())
t = " ".join(l)
return " "+len(t)*"─" +"\n"+ "→"+t +"\n "+ len(t)*"─"
def est_vide(self):
return len(self.__p) == 0
def enfiler(self, v):
self.__p.append(v)
def defiler(self):
return self.__p.pop(0)
Quelques exercices
Panier de courses
D'après sujet Centre étrangers 2022
Un supermarché met en place un système de passage automatique en caisse.
Un client scanne les articles à l’aide d’un scanner de code-barres au fur et à mesure qu’il les ajoute dans son panier.
Les articles s’enregistrent alors dans une structure de données.
La structure de données utilisée est une file définie par la classe Panier
qui hérite d'une classe File
dotée des primitives habituelles.
class Panier(File):
def __init__(self):
super().__init__()
Le panier d’un client sera représenté par une file contenant les articles scannés.
Les articles sont représentés par des tuples (code_barre, designation, prix, horaire_scan)
où :
code_barre
est un nombre entier identifiant l’article ;
designation
est une chaîne de caractères qui pourra être affichée sur le ticket de caisse ;
prix
est un nombre décimal donnant le prix d’une unité de cet article ;
horaire_scan
est un nombre entier de secondes permettant de connaitre l’heure où l’article a été scanné.
On souhaite ajouter un article dont le tuple est le suivant (31002, "café noir", 1.50, 50525)
.
Écrire le code permettant d’ajouter l’article à l’objet de classe Panier
appelé panier1
.
On souhaite définir une méthode remplir(panier_temp)
dans la classe Panier
permettant de remplir la file avec tout le contenu d’un autre panier panier_temp
qui est un objet de type Panier
.
Écrire le code de cette méthode remplir
.
Pour que le client puisse connaitre à tout moment le montant de son panier, on souhaite ajouter une méthode prix_total()
à la classe Panier
qui renvoie la somme des prix de tous les articles présents dans le panier.
Écrire le code de la méthode prix_total
. Attention, après l’appel de cette méthode, le panier devra toujours contenir ses articles.
Le magasin souhaite connaitre pour chaque client la durée des achats. Cette durée sera obtenue en faisant la différence entre le champ horaire_scan
du dernier article scanné et le champ horaire_scan
du premier article scanné dans le panier du client. Un panier vide renverra une durée égale à zéro.
Écrire une méthode duree_courses
de la classe Panier
qui renvoie cette durée.
Modélisation d'une file d'attente
L'objectif de l'exercice est de modéliser puis simuler le comportement d'une file d'attente (magasin, service public, ...).
Plusieurs modes sont envisageables et il faut les comparer pour déterminer lequel est le plus avantageux :
- Une seule file pour plusieurs guichets
- Le client en tête de file se présente au premier guichet qui se libère
- Une file par guichet
-
- chaque client choisit une file au hasard
- chaque client choisit la file la moins longue
Puisqu'il s'agit de comparer des performances de durée entre plusieurs solutions, il faut compter le temps qui s'écoule. Le temps sera donc rythmé par une variable tick
(horloge) qui augmentera à chaque itération et pourra représenter une unité de temps (par exemple des minutes).
Les données initiales du modèle sont les suivantes :
int ng
: nombre de guichets
int nc
: nombre de clients
list int etat_g
: état de chaque guichet = temps que le client doit encore passer au guichet = un entier qui diminue au fil du temps (0
= guichet libre)
list int durees_client
: durées (en unité de temps) que doivent passer chacun des clients au guichet, nombres entre duree_mini
et duree_maxi
list File files
: les files d'attentes (une seule ou bien une par guichet)
On peut par exemple commencer avec ces paramètres :
ng = 10 # nombre de guichets
etat_g = [0]*ng
nc = 1000 # nombre de clients
duree_mini = 3
duree_maxi = 30