Le type abstrait liste

Structure de données linéaire

Une structure de données est dite linéaire si on peut la représenter sous forme linéaire (en ligne).

Les files et les piles vues dans le cours sd1 sont des structures linéaires

Un exemple de file :

un exemple de pile :

  1. Nous verrons par la suite des structures non linéaires :

    Arbre :

    Graphe :

  2. Dans ce cours, nous allons voir deux autres structures linéaires les listes et les dictionnaires.

Le type abstrait liste

Liste

Une liste est une structure de données d'éléments indicés à partir de 0.
Dans cette structure de données, il est possible d'ajouter et supprimer des éléments à une liste.

$L=\{Bob;x;A7;1011;f\}$

Dans cette liste, l'élément indicé 2 est A7

Cette liste dispose de 5 éléments

Interface d'une liste

On considère la liste abstraite $L=\{Bob;x;A7;1011;f\}$

On donne $L=\{NSI;plaisir; jeux;livres; \pi; E0F1\}$

Expliciter ce que font chacune des commandes suivantes :

  1. $ajouteEnFin(33,L)$ .

  2. $dernier(L)$.

  3. $debut(L)$ .

  4. $supprDernier(L)$.

  5. $taille(L)$.

  6. $get(2,L)$.

Code de déblocage de la correction :

Les piles et les files sont des listes particulières, elle répondent à des problèmes où des structures de type LIFO FIFO sont adaptées.

Implémentation des listes avec des tableaux dynamiques

Tableaux dynamiques.

Les tableaux ont des tailles fixes alors que les listes non. C'est pour cela qu'il nous faut des tableaux dynamiques.

Un tableau dynamique est un tableau dont la taille peut varier. Ainsi, si on a besoin d'ajouter un élément à un tableau, on crée un nouveau tableau plus grand, on copie les éléments de l'ancien tableau dans le nouveau, on ajoute le nouvel élément à la fin, on remplace l'ancien tableau par le nouveau, et enfin on supprime l'ancien tableau.

Les opérations possibles avec le tableaux dynamiques :

Implémentation des listes.

L'utilisation de tableaux dynamiques est l'implémentation choisie par le langage Python pour intégrer les listes.

Cette implémentation est efficace pour accéder à des éléments de la liste mais l'est beaucoup moins pour insérer des éléments. En effet insérer un élément oblige à décaler tous les éléments un par un. C'est pourquoi une autre implémentation est possible celle avec les listes chaînées.

Implémentation de listes avec des listes simplement chaînées.

Listes simplement chaînées.

liste simplement chaînée

Une liste simplement chaînée est une structure de données dans laquelle les objets sont arrangés linéairement. Toutefois, contrairement au tableau, pour lequel l’ordre linéaire est déterminé par les indices, l’ordre d’une liste chaînée est déterminé par un pointeur dans chaque objet.

À partir d'un élément $x$ d'une liste simplement chaînées, on peut accéder à l'élément suivant, noté $succ[x]$.

Chaque élément $x$ d’une liste simplement chaînée $L$ est un objet comportant 2 champs :

On peut ainsi noté un élément $x$ d'une liste $L$ ainsi : $(cle[x],pointeVers[succ[x]])$.

À chaque liste $L$, on peut disposer de sa tête : on notera $tete[L]$ le premier élément d'une liste.

On notera NIL la valeur du pointeur qui ne pointe sur rien. Ce sera le marqueur de fin de la liste.

Ainsi les données : $\{\alpha; \gamma; \phi; \beta\}$ peut être représentées avec une liste simplement chaînée ainsi :

liste_image

Il faut avoir en tête que la modification d'un pointeur a un coût très inférieur à la réaffectation d'un élément.

Voici une vidéo qui illustre concrètement la notion de liste chaînée et qui permet de comprendre pourquoi modifier un pointeur a un coût très inférieur à la réaffectation d'un élément.

Voici une représentation visuelle de la liste abstraite contenant les données $\{\alpha;\gamma;x;\beta;a;3;z;\phi;\omega\}$ :

Interface des listes simplement chaînées

L'interface des listes simplement chaînées se compose des éléments suivants :

La liste vide est composée d'un premier champ clé vide et d'un second champ NIL.

On propose la fonction, écrite en pseudo code, $mystere(L,i)$ où $L$ est une liste et $i$ une clé de cette liste :

1. mystere(L,i)
2. x=tete[L]
3. si cle[x]==i 
4.      tete[L]=succ[x]
5. sinon 
6.      tant que cle[x]!=i et pointeVers[succ[x]]!=NIL
7.          y=x 
8.          x=succ[x]
9.      fin tant que
10.     pointeVers[succ[y]]=pointeVers[succ[x]]

  1. Effectuer une trace d'exécution de $mystere(L,cle[\alpha])$, où $L$ est la liste simplement chaînée contenant les valeurs $\{\alpha;\gamma;x;\beta;a;3;z;\phi;\omega\}$, liste simplement chaînée représentée ici, en reproduisant et en complétant le tableau suivant :

    Numéro de ligne $x$ $succ[x]$ $i$ $L$ $y$ $pointeVers[succ[y]]$ $pointeVers[succ[x]]$ $Tete[L]$ Test
    ... ... ... ... ... ... ... ... ... ...
    ... ... ... ... ... ... ... ... ... ...
  2. Code de déblocage de la correction :

  3. De même, effectuer une trace d'exécution de $mystere(L,cle[a])$ où $L$ est la liste simplement chaînée contenant les valeurs $\{\alpha;\gamma;x;\beta;a;3;z;\phi;\omega\}$, liste simplement chaînée représentée ici.

  4. Proposer un nom pertinent pour la fonction $mystere$ en comprennant le rôle de cette fonction.

  5. Code de déblocage de la correction :

On propose la fonction $mystere(L,k)$ où $L$ est une liste et $k$ une clé de cette liste suivante écrit en pseudo code :

1. mystere(L,k) 
2. x=tete[L]
3. tant que cle[x]!=k et pointeVers[succ[x]]!=NIL
4.    x=succ[x]
5. fin tant que
6. retourner x
                            

  1. appliquer $mystere(L,cle[\alpha])$, où $L$ est la liste simplement chaînée contenant les valeurs $\{\alpha;\gamma;x;\beta;a;3;z;\phi;\omega\}$, liste simplement chaînée représentée ici..

    Code de déblocage de la correction :

  2. appliquer $mystere(L,cle[a])$, où $L$ est la liste simplement chaînée contenant les valeurs $\{\alpha;\gamma;x;\beta;a;3;z;\phi;\omega\}$, liste simplement chaînée représentée ici..

  3. Proposer un nom pour la fonction $mystere$.

  4. Que pensez-vous de la seconde condition de la boucle tant que pointeVers[succ[x]]!=NIL.

  5. Code de déblocage de la correction :

Écrire en pseudo code les fonctions suivantes :

  1. $longueur(L)$ : qui renvoie le nombre d'élément dans la liste.

  2. $insererEnDebut(L,x)$ : L'élément $x$ (de la forme ($clé[x]$,$pointeVers[succ[x]]$)) est inséré à la tête de la liste. (Seule la donnée de l'élément $x$ est insérée, le pointeur dépend de la liste $L$).

  3. $inserer(L,a,e)$ : L'élément $e$ est inséré après l'élément de clé $a$ présent dans la liste $L$. (Seule la donnée de l'élément $e$ est insérée, son pointeur est adapté à la liste $L$).

Code de déblocage de la correction :

Implémentation des listes avec des listes simplement chaînées.

L'objectif de cette partie est d'écrire les éléments de l'interface d'une liste :

À l'aide des éléments vus dans la partie précédente.

Écrire chaque élément de l'interface à l'aide des fonctions écrites dans la propriété sur l'interface des listes simplement chaînées ainsi que celles obtenues aux exercices 2 à 4 précédant.

Code de déblocage de la correction :

Implémentation de l'objet liste simplement chaînée en Python à partir des listes de Python

Créer la classe ListeSimpleCh qui possède comme attribut un objet de type list et sa tête à partir des listes en Python. On donne Le constructeur __init__(self,l,tete) avec deux attributs une liste $l$ et une $tete$ :

class ListeSimpleCh:
    """définition de l’objet liste"""
    
    def __init__(self,l,tete):
        self.l = l
        self.t=tete
    

Créer une instanciation de la classe ListeSimpleCh avec la liste [1,'bos',4,"BG",0]. Afficher la tete de liste.

Vous devez ajouter à cette classe :

Code de déblocage de la correction :

Écrire les fonctions suivantes en Python à partir de la classe définie précédemment :

Comme l'objet de cette classe a comme premier attribut une liste Python, vous pouvez utiliser l'ensemble des méthodes connues sur ces listes Python.

  1. $supprimer(L,i)$ : L'élément de clé $i$ est supprimé de la liste.

  2. $rechercheListe(L,k)$ : qui retourne l'index du premier objet de clé $k$.

  3. $longueur(L)$ : qui renvoie le nombre d'élément dans la liste.

  4. $insererEnDebut(L,x)$ : L'élément $x$ est inséré à la tête de la liste.

  5. Code de déblocage de la correction :

Implémentation de l'objet liste simplement chaînée en Python en programmation objet

L'implémentation précédente est peu satisfaisante, en particulier parce qu'elle utilise un autre type de structure linéaire et aussi parce qu'à travers les attributs crées, la structure d'une liste simplement chaînée n'est pas apparente.
À travers les exercices suivants, créons donc une autre classe ListeCh en programmation objet de manière plus satisfaisante.
Pour simplifier, nous associerons directement la clé d'un élément de cette liste simplement chaînée à sa valeur.

Le but de cet exercice est de créer une classe Element dont chaque instance correspond à un élément d'une liste simplement chaînée.

  1. Créer cette classe avec son constructeur de sorte qu'un objet de cette classe possède deux attributs :

    • cle qui correspond à la valeur de cet élément,

    • pointeur qui correspond au successeur de de cet élément.

    None sera la valeur donnée à un de ces attributs si celui-ci est vide.

  2. Rajouter une méthode estVide qui renvoie un booleen True si cet élément de liste chaînée est vide et False sinon.

  3. Rajouter la méthode __str__ suivante afin de tester la classe créée et pour mieux visualiser chaque instance :

        def __str__(self):
            """permet de visualiser un élément. """
            if not self.estVide():
                if self.pointeur == None:
                    ch = "(" + str(self.cle) + "|NIL)."
                    return ch
                else:
                    ch = "(" + str(self.cle) + "|-->" + str(self.pointeur) + ")...>"
                    return ch
                        

Code de déblocage de la correction :

Voici le début d'une classe ListeSpChainee implémentant la structure linéaire de liste simplement chaînée :

class ListeSpChainee:
    """définition de l’objet liste simplement chaînée comme juxtapositionn d'élément de la classe Element"""

    def __init__(self, debut=None):
        """création d'une liste vide"""
        self.tete = Element(debut)
                    
  1. Créer une méthode lstVide qui renvoie True si la liste simplement chaînée est vide, False sinon.

    Comme chaque élément d'un objet de la classe ListeSpChainee est une instance de la classe Element, il suffit d'utiliser une méthode créée sur cette classe à l'exercice précédent.

  2. Créer une méthode ajouterEltEnFin qui prend en argument une valeur, qui crée un objet de type Element ayant comme cle cette valeur et qui rajoute cet élément en fin de la liste sur laquelle s'applique cette méthode.

    • Penser à traiter le cas où la liste est initialement vide,

    • Penser à gérer le pointeur du dernier élément de la liste initiale.

  3. Rajouter la méthode __str__ suivante afin de tester la classe créée et pour mieux visualiser chaque instance :

        def __str__(self):
            ch = ""
            if not self.lstVide():
                lecteur = self.tete  # position de l'élément regardé (on commence toujours par la tête pour une liste simplement chaînée)
                while lecteur.pointeur != None:
                    ch += "(" + str(lecteur.cle) + "|-->" + str(lecteur.pointeur.cle) + ")...>"
                    lecteur = lecteur.pointeur   # on passe à l'élément suivant.
                ch += "(" + str(lecteur.cle) + "|NIL)."
            return ch
                        

Code de déblocage de la correction :

  1. Rajouter à la classe précédente ListeSpChainee une méthode get_tete qui renvoie l'élément en tête de l'objet si celui n'est pas vide.

  2. Rajouter à cette classe ListeSpChainee une méthode succ qui prend en argument une valeur elt, qui cherche dans la liste chaînée l'élément contenant cette valeur et renvoie le successeur de cette valeur dans le cas où un tel élément a été trouvé.
    Penser à afficher des messages d'erreurs dans les cas où l'élément n'a pas été trouvé.

Code de déblocage de la correction :

Rajouter à la classe précédente ListeSpChainee une méthode inserer qui prend comme premier argument une valeur (à insérer comme élément) et un second argument qui correspond à la clé de l'élément de la liste après lequel doit s'insérer cette valeur.

Code de déblocage de la correction :

Rajouter à la classe précédente ListeSpChainee une méthode supprimer qui prend comme premier argument une valeur qui correspond à la clé de l'élément de la liste qui doit être supprimé.
Dans le cas où cet élément n'existe pas dans la liste, penser à afficher des messages d'erreurs clairs.

S'aider du pseudo-code de l'exercice 2 de ce chapitre.

Code de déblocage de la correction :

Les dictionnaires

Dictionnaires

Un dictionnaire est une structure de données qui permet d'associer une valeur à une clé. Cette clé peut être un mot ou un entier. l'ensemble clé-valeur est appelé entrée.

On peut modéliser un individu d'un répertoire téléphonique avec un dictionnaire. On pourrait prendre comme clés : nom et téléphone.

Rendez vous sur le cours de première pour retravailler l'ensemble des notions concernant les dictionnaires en Python.

Un enseignant de NSI stocke les résultats de ses élèves à l'aide d'un dictionnaire :

Voici le dictionnaire de 3 de ses élèves :

resultats = {'Alice': {'DS1' : [15.5, 3], 
                       'DM1' : [14.5, 1], 
                       'DS2' : [17, 4], 
                       'PROJET1' : [18, 5], 
                       'DS3' : [16, 4]}, 
                'Bob':{'DS1' : [6 , 3], 
                       'DM1' : [14.5, 1], 
                       'DS2' : [8.8, 4], 
                       'PROJET1' : [10, 5], 
                       'IE1' : [7, 2], 
                       'DS3' : [8, 4], 
                       'DS4' :[10, 4]},
              'Chaïn':{'DS1' : [13.5, 3], 
                       'DM1' : [16, 1], 
                       'DS2' : [14.5, 4], 
                       'PROJET1' : [16, 5], 
                       'IE1' : [16, 2], 
                       'DS4' : [19, 4]}}
                    

Le problème est que le professeur n'arrive pas à créer une fonction moyenne prenant en paramètre le dictionnaire resultat ainsi qu'un nom sous forme de chaîne de caractères et qui renvoie la moyenne des notes de l'élève ayant ce nom nom dans le cas où son nom apparaît dans le dictionnaire resultat.

Proposer une telle fonction moyenne afin d'aider cet enseignant. Il en sera surement reconnaissant !

Code de déblocage de la correction :

Exercices

Préciser quelle est la structure de données à privilégier pour chacune de ces tâches :

  1. Représenter un répertoire téléphonique.

  2. Stocker l'historique des actions effectuées dans un logiciel et disposer d'une commande Annuler.

  3. Envoyer des fichiers au serveur impression.

  4. On souhaite stocker un texte très long que l'on souhaite pouvoir modifier.

  5. Code de déblocage de la correction :

Dans chacun des cas suivants, déterminez quelle structure de données est la plus adaptée :

  1. Gérer le flux des personnes arrivant à la caisse d'allocations familiales.

  2. Mise en place d'un mécanisme annuler/refaire pour un traitement de texte.

  3. Garder les nombres premiers parmi les nombres de 2 à 1000 en utilisant la méthode du crible d'Eraosthène.

  4. Lors d'une partie échec, on veut enregistrer l'ensemble des coups joués et pouvoir les consulter.

Code de déblocage de la correction :

  1. Que signifie l'acronyme "FIFO" ?
    À quelle structure de données correspond cet acronyme ?

  2. Que signifie l'acronyme "LIFO" ?
    À quelle structure de données correspond cet acronyme ?

Code de déblocage de la correction :

Une usine européenne fabrique des voitures.
Chaque véhicule en cours de fabrication ou terminé possède un numéro de série pour l'identifier.

  1. Quelle structure de données est à privilégier pour gérer les véhicules sur la chaîne de production sur laquelle les portes sont fixées à la carrosserie ?

  2. Quelle structure de données est à privilégier pour gérer le lien entre le numéro de série du véhicule terminé avec l'ensemble de ces caractéristiques ?

  3. Les véhicules de luxe terminé sont stockés avant envoi dans des longs hangars étroits cul à cul.
    Sachant que ces hangars ne possèdent qu'une seule porte d'entrée à véhicule, quelle est la structure de données à privilégier pour gérer l'entrepôt de ces véhicules ?

  4. L'entreprise travaille avec de nombreux sous-traitants européens.
    Quelle structure de données est à privilégier pour gérer l'ensemble des noms sous-traitants européens ?

  5. Pour communiquer avec une entreprise sensible se trouvant aussi dans l'Union Européenne, l'entreprise vérifie toutes les minutes le routage des paquets entre elle et cette entreprise afin de s'assurer de ne passer que par des routeurs au sein de l'Europe.
    Quelle structure de données est à privilégier afin de gérer chaque minute l'ensemble des routeurs par lesquels semblent transiter les paquets transmis ?

Code de déblocage de la correction :

La pile et la poo

Voilà une définition de la classe pile en Python :

class Pile:

    def __init__(self, haut, reste=None):
        self.h = haut
        self.r = reste

    def estVide(self):
        if self.h == None:
            return True
        else:
            return False
            
    def empiler(self, valeur):
        self.r = Pile(self.h, self.r)
        self.h = valeur
    
    def depiler(self):
        if self.r == None:
            haut = self.h
            self.h = None
            return haut
        else:
            haut = self.h
            self.h = self.r.h
            self.r = self.r.r
            return haut 

    def affiche(self):
        if self.r == None:
            print(self.h)
        else:
            Pile.affiche(self.r)
            print(self.h)

On considère la pile : P=("bob", (3, ("nsi", (5, ("vacances", vide))))) .

  1. Expliquer comment est implémentée la fonction empiler.

  2. Expliquer comment est implémentée la fonction depiler.

  3. Créer une pile vide et tester qu'elle est bien considérée comme vide.

  4. Implémenter P sur votre machine en utilisant les méthodes (constructeurs et modificateurs) de la classe Pile.

  5. Améliorer la méthode affiche pour quelle affiche la pile dans le bon sens.

  6. Écrire une méthode hauteur qui renvoie la hauteur de la pile.

  7. Écrire une méthode depil2 qui enlève de la Pile sur laquelle s'applique la méthode l'élément situé en deuxième position de la pile et renvoie cet élément.

  8. Écrire une fonction insere(P, elt) qui insère dans une pile P l'élément elt à la position 2. On considère la position 1 correspond au haut de la pile.

  9. Écrire une fonction acces(P, pos) qui renvoie l'élément situé en position pos.

Code de déblocage de la correction :

L'objet file en Python

Voila une définition de l'objet file en Python ( implémenter avec une liste doublement chaînées)

On commence par définir un élément d'une liste doublement chaînées :

class Element:
    # chaque élément a pour attribut : le précèdent , le suivant et la valeur de l'élément

    def __init__(self, x):
        self.val = x
        self.precedent = None
        self.suivant = None

    def __str__(self):      # méthode qui permet de lance un print sur un tel objet
        return str(self.val) + "-" + str(self.suivant)

on définit ensuite l'objet file :

class File:
    #ici une file est la donnée de deux attributs : la file complète de type Element et le dernier élément de la file de type Element 

    def __init__(self):
        self.tete = None
        self.queue = None

    def enfile(self, x):
        e = Element(x)        # on transforme l'élément à ajouter en un objet Element de listes doublement chaînées
        if self.tete == None:
            self.tete = e   # file vide la tete est remplacée par l'élément e
        else:
            e.precedent = self.queue #le précédent de l'élément pointe sur l'ancienne queue de la file
            self.queue.suivant = e #l'ancienne queue de la file pointe sur e avec suivant
        self.queue = e #on redéfinit self.queue par e.
        
    def file_vide(self):
        return self.tete is None #renvoie True si None et False sinon
    
    def defile(self):
        if not self.file_vide():
            e = self.tete #on stocke l'élément à defiler
            if e.suivant is None: #cas où il n'y a qu'un élément 
                self.tete = None
                self.queue = None
            else:
                self.tete = e.suivant
                self.tete.precedent = None
            return e.val
        
    def __str__(self):
        return str(self.tete)

Voici une vidéo pour vous aider à comprendre les deux classes ci-dessus ainsi que la notion de liste doublement chaînée :

On considère la file : F=(5,11115,("vacances","Bob",7,"nsi",12)).

Revoir l'implémentation de la file vue au cours sd1 en cas de besoin

  1. Identifier, dans l'exemple, la tête, la queue et le cœur de F.

  2. Créer une file vide et tester qu'elle est bien considérée comme vide.

  3. Implémenter F sur votre machine en utilisant les méthodes (constructeurs et modificateurs) de la classe File.

  4. Écrire une fonction longueur(F) qui renvoie la longueur de la file.

  5. Écrire une procédure affiche_coeur(F) qui affiche le coeur de la file.

Code de déblocage de la correction :

Demander le programme !

Licence Creative Commons
Les différents auteurs mettent l'ensemble du site à disposition selon les termes de la licence Creative Commons Attribution - Pas d’Utilisation Commerciale - Partage dans les Mêmes Conditions 4.0 International