Piles et Files

Pile

Stack Of Plates Png - Dishes Stack Png Clipart@pikpng.com

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

 

 

 

Vous aimerez aussi...

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *