Données Liste Implémentation

Identification

Infoforall

13 - Implémenter une Liste 2/3


Aujourd'hui, nous allons travailler sur une Liste Itérative sous forme d'une Liste Contiguë de cases mémoires, un tableau.

L'idée abstraite est la suivante :

Nous allons l'implémenter sous forme d'un tableau, ce qui donnera une structure de données proche de ceci :

Principe du tableau

Prérequis : l'activité sur les Listes et la première implémentation sous forme de tuple (pour comprendre l'intérêt de celle-ci).

Documents de cours : open document ou pdf

1 - Implémentation en tableau statique, bonne idée ?

(Rappel 1er) 1.1 RAM

Mémoire vive
Hiérarchie des mémoire

Une mémoire est un ensemble de bits pouvant contenir 0 ou 1, regroupés en octets.

Vous devez être capable de distinguer ces trois types de mémoire :

  1. Les registres : très petite capacité, mais très grande rapidité d'accès ;
  2. La mémoire vive (RAM) : capacité moyenne, vitesse moyenne ;
  3. La mémoire de masse (DD, clé USB...) : grande capacité, mais accès lent.
RAM
La mémoire vive est la zone dans laquelle on place les programmes en cours d'exécution et les données sur lesquelles ils travaillent.

En effet, cette mémoire est asssez grande pour contenir tout cela (contrairement aux registres) et est assez rapide pour que l'exécution ne soit pas trop lente.

RAM est l'acronyme de Random Access Memory, "mémoire à accès aléatoire".

"Accès aléatoire" est à comprendre en opposition à "accès séquentiel" :

  • en accès séquentiel, on doit parcourir les cases 0, 1, 2, 3, ... 98, 99 pour atteindre la case 100.
  • en accès aléatoire, on peut atteindre à coût constant une case mémoire pourvu qu'on connaisse son adresse-mémoire.
Exemple visuel

Voici une petit partie des zones-mémoires d'un ordinateur. Accèder à la case 239 ne prend pas plus de temps qu'accèder à la case 200

Contenu de la mémoire
Un petit apercu de la mémoire

Chaque case représente un octet dont le contenu est donnée comme un entier non signé. On préfère noter 45 plutôt que 00101101.

(Rappel 1er) 1.2 Principe d'un tableau

Espace des noms
L'espace des noms est une table permettant de faire le lien (à coût constant) entre le nom d'une variable (t ci-dessous) et une adresse-mémoire (500 ci-dessous).

Important : une variable (t par exemple) ne "contient" pas le contenu du tableau, juste l'adresse de ce contenu.
Propriétés générales d'organisation
En informatique, un tableau désigne un conteneur de données qui a les propriétés suivantes :
  • Les octets sont consécutifs en mémoire à partir d'une première adresse A.
  • Chaque case encode le même type de données, et comporte C octets.
  • Si on désigne par n le nombre de cases du tableau, il occupe une place-mémoire de L = n*C octets.

Exemple : considérons un tableau ne comportant que 3 integers uniquement (on prendra 4 octets par entier, pour représenter de grandes valeurs, supérieures à 255). Ci-dessous, il s'agit donc des zones en orange, bleu et vert :

    Principe du tableau dans l'espace des noms
  • Le premier octet du tableau est stocké à l'adresse A = 500.
  • Chaque case contient C = 4 octets.
  • Le tableau occupe donc L = 3 * 4 = 12 octets consécutifs :
    • L'adresse du premier octet est 500 à l'adresse ;
    • L'adresse du dernier octet est 511 : 500 + (12 - 1).
    • Les 4 octets d'adresse 500-501-502-503 (oranges) représentent collectivement le premier entier.
Indice des cases
Lien entre indice et adresse-mémoire
  • On identifie chaque case de 4 octets par un numéro qu'on appelle indice.
  • t[0] représente donc le contenu des cases 500 - 501 - 502 - 503.
  • t[1] représente donc le contenu des cases 504 - 505 - 506 - 507.
  • Principe des octets consécutifs
    Exemple visuel

    Sur cet autre exemple, les données, stockées à partir de l'adresse 200, ont toutes une taille de 4 octets.

    Mémoire avec groupement de 4 octets
    Les indices correspondant aux groupements de 4 octets
    1.3 Accès aux cases à coût CONSTANT

    Accès à Coût CONSTANT
    L'accès à n'importe quelle case du tableau se fait à coût constant.

    Voyons pourquoi.

    Explications

    1 - Trouver l'adresse du tableau : coût constant

    L'espace des noms fait le lien entre le nom d'un tableau et l'adresse A de son premier octet.

    2 - Calcul de l'adresse de la case d'indice i : coût constant

    Adresse du premier octet à lire : Ai = A + i * C

    A désigne l'adresse du tableau et C la largeur en octets de ses cases.

    Si on prend A = 500 et C = 4 :

    • t[0] : commence à 500 + 0*4 = 500
    • t[1] : commence à 500 + 1*4 = 504
    • t[2] : commence à 500 + 2*4 = 508
    • t[3] : commence à 500 + 3*4 = 512
    • ...

    3 - RAM : coût constant

    L'accès à un octet dont on connait l'adresse est à coût CONSTANT par définition même de la RAM.

    4 - Lire 4 cases : coût constant

    4 actions à coût CONSTANT donc coût CONSTANT au total.

    5 - Conclusion : obtenir la valeur t[i] : coût CONSTANT

    1.4 - Une case en plus ou en moins par compréhension

    Création d'un tableau de 5 cases contenant 0
    >>> t = [0 for x in range(5)] >>> t [0, 0, 0, 0, 0]

    La variable de boucle x va prendre d'abord la valeur 0, puis 1, puis 2, puis 3 et finalement 4. Sa valeur n'a pas d'importance ici mais elle prend bien 5 valeurs différentes. Pour chaque valeur, on va rajouter une case contenant 0 dans le tableau.

    On lit une construction par compréhension de la droite vers la gauche. En mode pas à pas, cela donne quelque chose comme :

    t = [0 for x in range(5)] t = [0 for x in (0, 1, 2, 3, 4)] t = [rajoute une case contenant 0 à la fin du tableau pour chaque x que tu vas lire] t = [0, 0, 0, 0, 0]

    S'agissant d'une boucle POUR, cette création est donc réalisée avec un coût linéaire.

    Autant de cases que l'original

    Si on veut autant de cases qu'un autre tableau, il suffit d'utiliser la fonction native len() lors de la création :

    >>> a = [7, 15, 12, 25] >>> t = [0 for x in range( len(a) )] >>> t [0, 0, 0, 0]
    Une case en plus

    Si on veut une case en plus par rapport à un autre tableau, il suffit d'utiliser la fonction native len() et de rajouter 1 au résultat :

    >>> a = [7, 15, 12, 25] >>> t = [0 for x in range( len(a)+1 )] >>> t [0, 0, 0, 0, 0]
    Une case en moins

    Si on veut une case en moins par rapport à un autre tableau, il suffit d'utiliser la fonction native len() et de soustraire 1 au résultat :

    >>> a = [7, 15, 12, 25] >>> t = [0 for x in range( len(a)-1 )] >>> t [0, 0, 0]
    1.5 - coûts des actions sur les tableaux en Python

    Accès à une case ou au tableau

    Coût CONSTANT.

    Créer un tableau d'environ n cases

    Puisqu'on utilise une boucle POUR avec n boucles environ lorsqu'on veut créer un tableau de taille n-1, n ou n+1, cette création est une action à coût LINEAIRE.

    Déterminer le nombre de cases

    L'utilisation de la fonction native len() est à coût CONSTANT.

    Nous allons maintenant implémenter l'idée abstraite d'une Liste itérative sous forme d'une Liste contigüe en utilisant des tableaux statiques. Nous allons donc réaliser un module contenant les fonctions d'interface d'une Liste.

    Le MODULE version 1 : PRIMITIVES de la liste contiguë de ce TP

    Ci-dessous vous trouverez le module non finalisé implémentant une "Liste contiguë basée sur un tableau statique".

    Les Places portent le nom de Cases dans ce type d'implémentation.

    On notera que fournir ieme() va permettre de manipuler notre liste sans se soucier des Cases directement. C'est pour cela que acces(), contenu() et successeur() sont grisées : elles existent mais ieme() permet de s'en passer.

    Voici les opérations primitives qu'on sélectionne classiquement pour manipuler un tableau (ou Liste contiguë) :

    1. nouvelle_liste_vide() → Liste VIDE
    2. est_liste_vide(lst:Liste) → bool
    3. longueur(lst:Liste) → int
    4. inserer(lst:Liste, elt:Elément, i:int VALIDE) → Liste NON VIDE
    5. supprimer(lst:Liste NON VIDE, i:int VALIDE) → Liste
    6. ieme(lst:Liste NON VIDE, i:int VALIDE) → Element

    Dans la documentation du module :

    • V indique que la fonction est déjà réalisée.
    • x indique que la fonction sera à réaliser.
    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 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
    """Première ébauche du module Liste contiguë basée sur des tableaux statiques Description des types non natifs utilisés dans les fonctions + Element : le type des éléments disponibles dans votre Liste si elle est homogène. + Case : la référence d'un tuple (Liste, Indice) + Liste : la référence d'un tableau. Opérations primitives de la Liste contiguë ----------------------------------------- V nouvelle_liste_vide() -> Liste VIDE V est_liste_vide(lst:Liste) -> bool V longueur(lst:Liste) -> int V ieme(lst:Liste NON VIDE, i:int VALIDE) -> 'Elément' x inserer(lst:Liste, elt:Element, i:int VALIDE) -> Liste NON VIDE x supprimer(lst:Liste NON VIDE, i:int VALIDE) -> Liste Fonctions d'interface supplémentaires ------------------------------------- V inserer_tete(lst:Liste, elt:Element) -> Liste NON VIDE x supprimer_tete(lst:Liste NON VIDE) -> Liste V representer_liste(lst:Liste) -> str """ # Importation : aucune # Déclaration des CONSTANTES : aucune # Déclaration des fonctions d'interface fondamentales def nouvelle_liste_vide() -> 'Liste VIDE': """Renvoie une liste vide""" return [] def est_liste_vide(lst:'Liste') -> bool: """Prédicat qui renvoie True si la liste est vide""" return lst == [] def longueur(lst:'Liste') -> int: """Renvoie le nombre d'éléments stockés dans lst""" return len(lst) def ieme(lst:'Liste NON VIDE', i:'int VALIDE') -> 'Element': """Renvoie le contenu de la Case de position i VALIDE dans la liste NON VIDE lst""" return lst[i] def inserer(lst:'Liste', elt:'Elément', i:'int VALIDE') -> 'Liste NON VIDE': """Renvoie une Liste basée sur lst où elt est en position VALIDE i""" pass def supprimer(lst:'Liste NON VIDE', i:'int VALIDE') -> 'Liste': """Renvoie une Liste basée sur lst NON VIDE en supprimant la case i""" pass # Déclaration des fonctions d'interface supplémentaires def inserer_tete(lst:'Liste', elt:'Elément') -> 'Liste NON VIDE': """Renvoie une Liste basée sur lst où elt est en Tête""" nb = len(lst) + 1 nouveau = [None for _ in range(nb)] # 1 - nouveau tableau avec une case en plus for k in range(0, len(lst)): # 2 - pour chaque indice possible de lst nouveau[k+1] = lst[k] # décalage à droite des éléments nouveau[0] = elt # 3 - elt à la position 0 return nouveau def supprimer_tete(lst:'Liste NON VIDE') -> 'Liste': """Renvoie une Liste basée sur lst NON VIDE en supprimant la Tête""" pass def representer_liste(lst:'Liste') -> str: """Renvoie une représentation de la Liste sous forme d'une séquence commençant par la tête""" return str(lst).replace(',',' -> ').replace('[','').replace(']','') # Instructions du programme principal if __name__ == '__main__': # Ces fonctions ne seront en mémoire qu'en lancement direct def tester_inserer_tete(): a = nouvelle_liste_vide() a = inserer_tete(a, 5) assert a == [5] a = inserer_tete(a, 2) assert a == [2, 5] b = inserer_tete(a, 20) assert a == [2, 5] assert b == [20, 2, 5] print('Test OK pour inserer_tete()') def tester_supprimer_tete(): a = nouvelle_liste_vide() a = inserer_tete(a, 5) a = inserer_tete(a, 2) b = supprimer_tete(a) assert a == [2, 5] assert b == [5] c = supprimer_tete(b) assert est_liste_vide(c) print('Test OK pour supprimer_tete()') def tester_longueur(): a = nouvelle_liste_vide() b = inserer_tete(a, 5) c = inserer_tete(b, 2) assert longueur(a) == 0 assert longueur(b) == 1 assert longueur(c) == 2 print('Test OK pour longueur()') def tester_ieme(): a = nouvelle_liste_vide() b = inserer_tete(a, 5) c = inserer_tete(b, 2) assert ieme(b, 0) == 5 assert ieme(c, 0) == 2 assert ieme(c, 1) == 5 print('Test OK pour lire()') def tester_inserer(): a = nouvelle_liste_vide() b = inserer_tete(a, 5) c = inserer_tete(b, 2) d = inserer(c, 20, 1) assert d == [2, 20, 5] print('Test OK pour inserer()') def tester_supprimer(): a = nouvelle_liste_vide() b = inserer_tete(a, 5) c = inserer_tete(b, 2) d = inserer(c, 20, 1) e = supprimer(d, 2) assert d == [2, 20, 5] assert e == [2, 20] print('Test OK pour supprimer()') print("Module lancé directement, il ne s'agit pas d'une importation")

    01° Mettre le module ci-dessus en mémoire en le lançant directement. Observer les 4 fonctions suivantes :

    • nouvelle_liste_vide(),
    • est_liste_vide(),
    • longueur() et
    • ieme().
    Répondre ensuite à ces questions :

    1. Quel est le type natif de Python utilisé pour réaliser notre nouvelle implémentation ?
    2. Puisqu'on veut utiliser des tableaux statiques, va-t-on avoir le droit d'utiliser les méthodes append() pop() ?
    3. L'utilisateur du module a-t-il le droit d'utiliser liste[i] pour lire le contenu de ses données ?
    4. Estimer le coût de ces 4 primitives en analysant leur code respectif.
    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
    def nouvelle_liste_vide() -> 'Liste VIDE': """Renvoie une liste vide""" return [] def est_liste_vide(lst:'Liste') -> bool: """Prédicat qui renvoie True si la liste est vide""" return lst == [] def longueur(lst:'Liste') -> int: """Renvoie le nombre d'éléments stockés dans lst""" return len(lst) def ieme(lst:'Liste NON VIDE', i:'int VALIDE') -> 'Element': """Renvoie le contenu de la Case de position i VALIDE dans la liste NON VIDE lst""" return lst[i]

    ...CORRECTION...

    1. Nous utilisons le type natif list.
    2. Dans un tableau statique, le nombre de cases ne peut pas évoluer. Vous ne devrez donc pas utiliser les deux méthodes du type natif list :
      • append() rajoute une case à la fin
      • pop() supprime l'actuelle case de fin
    3. Non, l'utilisateur devra se limiter à utiliser nos fonctions d'interface. S'il manipule directement les données, ses programmes risquent de ne plus fonctionner si nous décidons de modifier les codes internes des primitives lors d'une mise à jour.
    4. Puisqu'elles ne contiennent toutes que des instructions à coût constant et aucune boucle, elles sont à coût constant.
    Insertion en tête et tableau statique

    Les listes sont ici IMMUABLES puisque les fonctions d'insertion et de suppression renvoient une nouvelle Liste.

    • inserer(lst:Liste, elt:Elément, i:int VALIDE) → Liste NON VIDE
    • supprimer(lst:Liste NON VIDE, i:int VALIDE) → Liste

    On doit donc créer un deuxième tableau avec une case en plus et y décaler les éléments vers la droite.

    Exemple : on veut insérer l'élément 23 en tête dans la liste initiale (5, 10, 15, 25, 30, 35)

    Principe de l'insertion dans une liste sur une implémentation tableau

    Ce tableau lst2 :

    1. doit posséder une case en plus
    2. contient les éléments de la première liste mais avec un décalage de 1 vers la droite : lst2[i+1] = lst1[i]
    3. possède le nouvel élément en indice 0

    02° Observer la fonction inserer_tete() (version simplifiée de la primitive inserer() que vous aurez à concevoir plus loin) :

    1 2 3 4 5 6 7 8 9 10
    def inserer_tete(lst:'Liste', elt:'Elément') -> 'Liste NON VIDE': """Renvoie une Liste basée sur lst où elt est en Tête""" nb = len(lst) + 1 nouveau = [None for _ in range(nb)] # 1 - nouveau tableau avec une case en plus for k in range(0, len(lst)): # 2 - pour chaque indice possible de lst nouveau[k+1] = lst[k] # --> décalage à droite des éléments nouveau[0] = elt # 3 - elt à la position 0 return nouveau

    Questions

    1. Cette fonction modifie-t-elle la liste en place ou crée-t-elle une copie qu'elle renvoie ?
    2. Pourquoi rajouter une case au nouveau tableau ?
    3. Un élève ne comprend pas pourquoi on effectue un décalage à droite à l'aide de la ligne surlignée. Justifier cette affirmation en traduisant en français cette ligne.

    ...CORRECTION...

    1. Pas d'effet de bord : on crée un tableau ayant une case en plus, on le remplit puis on le renvoie.
    2. On insère une nouvelle valeur, il faut bien une case en plus non ?
    3. La ligne surlignée veut dire "Place l'élément k du tableau lst dans la case (k+1) du nouveau tableau.". Puisque la case (k+1) est à droite de la case k, nous décalons bien toutes les valeurs.
    Suppression en tête et tableau statique
    Principe de la suppression dans une liste sur une implémentation tableau

    03° Quelques questions :

    1. Combien de cases va devoir avoir le nouveau tableau contenant la liste modifiée ?
    2. Va-t-on devoir décaler les cases vers la gauche (on diminue l'indice) ou vers la droite (on augmente l'indice) ?
    3. Doit-on décaler tous les indices du tableau initial ou y-a-t-il un indice à ne pas décaler ?

    ...CORRECTION...

    1. Une case en moins, puisqu'on supprime.
    2. On voit bien qu'on va devoir décaler vers la gauche, en diminuant les indices des éléments qu'on va déplacer.
    3. On ne doit pas décaler l'indice 0 du tableau initial puisqu'on veut le faire disparaître.

    04° Compléter la fonction supprimer_tete() dont on vous donne le prototype et la documentation.

    PRECONDITION : la liste fournie est NON VIDE.

    Prototype

    supprimer_tete(lst:'Liste NON VIDE') -> 'Liste'

    Déclaration à modifer

    1 2 3
    def supprimer_tete(lst:'Liste NON VIDE') -> 'Liste': """Renvoie une Liste basée sur lst NON VIDE en supprimant la Tête""" pass

    Vous pouvez utiliser la fonction tester_supprimer_tete() pour tester votre proposition.

    ...CORRECTION...

    1 2 3 4 5 6 7 8 9
    def supprimer_tete(lst:'Liste NON VIDE') -> 'Liste': """Renvoie une Liste basée sur lst NON VIDE en supprimant la Tête""" nb = len(lst) - 1 nouveau = [None for _ in range(nb)] # 1 - nouveau tableau avec une case en moins for k in range(1, len(lst)): # 2 - décalage à gauche des éléments d'indice 1 et plus nouveau[k-1] = lst[k] return nouveau

    Pour l'instant, on ne parvient à agir que sur les têtes mais on commence à voir que cela ressemble bien à une liste.

    05° Enregistrer le module exactement sous le nom liste_contigue_statique.py. Enregistrer au même endroit ce programme :

    1 2 3 4 5 6 7 8 9 10 11 12 13
    from liste_contigue_statique import nouvelle_liste_vide as new from liste_contigue_statique import inserer_tete as mettreDevant from liste_contigue_statique import representer_liste as visuel from random import randint NBR = randint(4, 10) a = new() for _ in range(NBR): a = mettreDevant(a, randint(0, 20)) print(visuel(a))

    Questions

    1. Quel mot-clé permet de changer localement le nom d'un module ou d'une fonction importée ?
    2. A quelle fonction fait-on appel en réalité lorsqu'on utilise visuel() dans ce programme ?
    3. L'utilisateur n'utilisant que la fonction d'interface autorisée visuel() pourra-t-il se rendre compte qu'on implémente notre structure de données dans un tableau ?

    ...CORRECTION...

    1. Il faut utiliser le mot-clé as.
    2. visuel() est le nom local de representer_liste
    3. 3
      from liste_contigue_statique import representer_liste as visuel
    4. Comme on peut le voir sur les exemples, l'utilisateur ne voit que la représentation de la liste comme une séquence ordonnée. Il ne peut donc pas savoir sous quelle forme elle est réellement implémentée.
    5. 20 -> 3 -> 8 -> 18 -> 2 -> 14 -> 16 -> 11 -> 2

      Par contre, s'il demande directement le contenu sans passer par l'interface, il pourra voir qu'il s'agit d'un tableau.

      >>> a [20, 3, 8, 18, 2, 14, 16, 11, 2]

    Je vous ai montré comment supprimer la tête et insérer une tête. Il vous reste à le faire à n'importe quelle position.

    Insertion n'importe où et tableau statique

    Un exemple d'insertion d'un élément valant 23 sur l'indice 2 dans une Liste contiguë a :

    >>> inserer(a, 23, 2)
    Principe de l'insertion dans une liste sur une implémentation tableau

    On voit que certains éléments auront la même position lors de la recopie, alors que d'autres seront déplacés.

    06-A° Répondre aux question ci-dessous :

    1. Pour insérer en indice 2, quels sont les indices des cases qui ne seront pas décalées ?
    2. Pour insérer en indice 2, quels sont les indices des cases qui seront décalées ?
    3. Pour insérer en indice i, quels sont les indices des cases qui ne seront pas décalées ?
    4. Pour insérer en indice i, quels sont les indices des cases qui seront décalées ?

    ...CORRECTION...

    1. On ne décale pas les cases de 0 à 1.
    2. On décale vers la droite les cases 2 et plus
    3. On ne décale pas les cases de 0 à (i-1).
    4. On décale vers la droite les cases i et plus

    06-B° Compléter la fonction inserer() dont on vous donne le prototype et la documentation dans le module. N'utilisez pas la fonction inserer_tete() à l'interne : on veut réaliser une fonction indépendante puisqu'on désire en faire une primitive. Par contre, vous pourrez vous inspirer de inserer_tete().

    Précondition : i doit être un indice VALIDE. Attention, fournir un indice correspondant au nombre d'éléments de la liste de base est valide dans le cadre d'une insertion : cela veut dire d'insérer en fin de liste.

    Prototype

    1
    def inserer(lst:'Liste', elt:'Elément', i:'int VALIDE') -> 'Liste NON VIDE':

    Vous pouvez utiliser la fonction tester_inserer() pour tester votre implémentation.

    ...CORRECTION...

    1 2 3 4 5 6 7 8 9 10 11 12
    def inserer(lst:'Liste', elt:'Elément', i:'int VALIDE') -> 'Liste NON VIDE': """Renvoie une Liste basée sur lst où elt est en position VALIDE i""" nb = len(lst) + 1 nouveau = [None for _ in range(nb)] # 1 - nouveau tableau avec une case en plus for k in range(0, i): # 2 - copie les éléments des indices 0 à i-1 sans décalage nouveau[k] = lst[k] for k in range(i, len(lst)): # 3 - décalage à droite des éléments d'indice i et plus nouveau[k+1] = lst[k] nouveau[i] = elt # 4 - elt à la position i return nouveau
    Suppression n'importe où et tableau statique
    Principe de la suppression dans une liste sur une implémentation tableau

    07-A° Répondre aux question ci-dessous :

    1. Pour supprimer l'élément en indice 2, quels sont les indices des cases qui ne seront pas décalées ?
    2. Pour supprimer l'élément en indice 2, quels sont les indices des cases qui seront décalées ?
    3. Pour supprimer l'élément en indice i, quels sont les indices des cases qui ne seront pas décalées ?
    4. Pour supprimer l'élément en indice i, quels sont les indices des cases qui seront décalées ?

    ...CORRECTION...

    1. On ne décale pas les cases de 0 à 1.
    2. On décale vers la gauche les cases 3 et plus
    3. On ne décale pas les cases de 0 à (i-1).
    4. On décale vers la gauche les cases i+1 et plus

    07-B° Compléter la fonction supprimer() dont on vous donne le prototype et la documentation dans le module. N'utilisez la fonction supprimer_tete() à l'interne : il faut faire une fonction indépendante. Par contre, vous pourrez vous inspirer de supprimer_tete().

    Préconditions : la Liste doit être NON VIDE et l'indice i doit être VALIDE, c'est à dire qu'on ne peut demander que de supprimer une case qui existe réellement.

    Prototype

    1
    def supprimer(lst:'Liste NON VIDE', i:'int VALIDE') -> 'Liste':

    Vous pouvez utiliser la fonction tester_supprimer() pour tester votre implémentation.

    ...CORRECTION...

    1 2 3 4 5 6 7 8 9 10 11
    def supprimer(lst:'Liste NON VIDE', i:'int VALIDE') -> 'Liste': """Renvoie une Liste basée sur lst NON VIDE en supprimant la case i""" nb = len(lst) - 1 nouveau = [None for _ in range(nb)] # 1 - nouveau tableau avec une case en moins for k in range(0, i): # 2 - copie les éléments des indices 0 à i-1 sans décalage nouveau[k] = lst[k] for k in range(i+1, len(lst)): # 3 - décalage à gauche des éléments d'indice i+1 et plus nouveau[k-1] = lst[k] return nouveau

    08° Justifier que le coût des quatre fonctions suivantes est linéaire en n :

    • inserer(),
    • supprimer(),
    • inserer_tete() et
    • supprimer_tete()

    ...CORRECTION...

    Avec des phrases

    Le calcul du nombre de cases nb est à coût CONSTANT car affectation, soustraction et len() sont à coût constant.

    La création par compréhension du nouveau tableau contenant None est LINEAIRE en n.

    Avec les deux boucles, on déplace n éléments pour l'insertion, n-1 éléments pour la suppression. Chaque déplacement est à coût constant, le déplacement total est donc LINEAIRE en n.

    On remplit une case lors de l'insertion : coût CONSTANT.

    On renvoie la référence du nouveau tableau : coût CONSTANT.

    Le coût total est donc un coût LINEAIRE en n.

    On notera qu'il n'y a pas de pire des cas ou de meilleur des cas comme avec la liste chaînée. Avec un tableau statique, insertion et suppression sont toujours linéaire.

    Avec les notations asymptotiques

    Pour l'insertion :

    𝞗(3 + n + n*1 + 2)

    = 𝞗(n + n)

    = 𝞗(n)

    C'est donc un coût linéaire.

    Pour la suppression :

    𝞗(3 + n + (n-1)*1 + 2)

    = 𝞗(n + n)

    = 𝞗(n)

    C'est donc un coût linéaire.

    Nous avons maintenant tout ce qu'il faut pour parvenir à gérer notre Liste, même si les insertions et les suppressions ne seront clairement pas de bonne qualité puisqu'on a un coût linéaire partout.

    Nous n'avons pas réellement besoin des 3 primitives acces(), successeur() et contenu() puisqu'on peut gérer tout cela à la main en utilisant l'indice de la case.

    Par contre, pour vous montrer que notre Liste est bien rigoureusement une Liste, nous allons les créer artificiellement.

    1.6 Gestion des cases dans notre Liste contiguë

    Problématique

    Pour accéder à une Case, on utilise par exemple a[i], qui contient deux informations :

    1. La structure a dans laquelle se trouve la case
    2. L'indice i de la case

    La seule connaissance de i n'est donc pas suffisant.

    Réponse possible

    Nous faisons le choix de faire des cases de simple tuples contenant :

    Case = (Liste contenant cette Case, Indice de cette case)

    Comme ce sont des tuples, on choisit de représenter VIDE (l'absence de Successeur) par un tuple vide.

    1 2 3
    def acces(lst:'Liste NON VIDE', i:'int VALIDE') -> 'Case': """Renvoie la référence de la case d'indice i dans la liste lst NON VIDE""" return (lst, i) # on crée un tuple-case (quelle liste ?, quel indice ?)

    Puisqu'une case contient bien les informations "Quelle Liste ?" et "Quel numéro ?", on peut facilement réaliser les deux autres fonctions :

    1 2 3 4 5 6
    def contenu(c:'Case') -> 'Elément': """Renvoie le contenu de la case valide fournie""" lst = c[0] # on récupère la liste contenant notre case i = c[1] # on récupère l'indice de notre case return lst[i] # on renvoie l'élément stocké à cet endroit
    1 2 3 4 5 6 7 8
    def successeur(c:'Case') -> 'Case|VIDE': """Renvoie la case qui succède à la case fournie """ lst = c[0] # on récupère la liste contenant notre case i = c[1] # on récupère l'indice de notre case if i == len(lst) - 1: # si la case est la dernière de la Liste return () # on renvoie un Successeur vide else: return (lst, i+1) # on crée le tuple-case et on renvoie

    09-A° Lire les 3 fonctions ci-dessous gérant les Cases et estimer leurs coûts.

    1 2 3
    def acces(lst:'Liste NON VIDE', i:'int VALIDE') -> 'Case': """Renvoie la référence de la case d'indice i dans la liste lst NON VIDE""" return (lst, i) # on crée un tuple-case (quelle liste ?, quel indice ?)
    1 2 3 4 5 6
    def contenu(c:'Case') -> 'Elément': """Renvoie le contenu de la case valide fournie""" lst = c[0] # on récupère la liste contenant notre case i = c[1] # on récupère l'indice de notre case return lst[i] # on renvoie l'élément stocké à cet endroit
    1 2 3 4 5 6 7 8
    def successeur(c:'Case') -> 'Case|VIDE': """Renvoie la case qui succède à la case fournie """ lst = c[0] # on récupère la liste contenant notre case i = c[1] # on récupère l'indice de notre case if i == len(lst) - 1: # si la case est la dernière de la Liste return () # on renvoie un Successeur vide else: return (lst, i+1) # on crée le tuple-case et on renvoie

    ...CORRECTION...

    Lire un tuple, lire une case d'un tuple, créer un tuple de deux cases, utiliser len() en Python, faire une affection, faire une soustraction, tester et renvoyer une réponse sont toutes des actions à coût CONSTANT.

    Ces fonctions ne comportent que des actions de ce type et aucune boucle ou appel récursif : elles sont donc à coût CONSTANT.

    09-B° Créer la fonction predecesseur() qui doit renvoyer la case précédente ou VIDE si elle n'existe pas..

    1 2 3
    def predecesseur(c:'Case') -> 'Case|VIDE': """Renvoie la case qui précède la case fournie, ou VIDE""" return c # fausse réponse (du bon type) du prototype

    ...CORRECTION...

    1 2 3 4 5 6 7 8
    def predecesseur(c:'Case') -> 'Case|VIDE': """Renvoie la case qui précède la case fournie, ou VIDE""" lst = c[0] # on récupère la liste contenant notre case i = c[1] # on récupère l'indice de notre case if i == 0: # si la case est la tête de la Liste return () # on renvoie un prédecesseur vide else: return (lst, i-1) # on crée le tuple-case et on renvoie
    1.7 Coûts des primitives d'une liste contiguë basée sur des tableaux statiques

    Contrairement à la Liste tuple (Tête, Queue), les primitives de la Liste "tableau statique" ne sont pas toutes implémentables à coût constant.

    1. CST O(1)   nouvelle_liste_vide() → Liste VIDE
    2. CST O(1)   est_liste_vide(lst:Liste) → bool
    3. CST O(1)   longueur(lst:Liste) → int
    4. LIN θ(n)   inserer(lst:Liste, elt:Elément, i:int) → Liste
    5. LIN θ(n)   supprimer(lst:Liste Non Vide, i:int) → Liste
    6. CST O(1)   acces(lst:Liste Non Vide, i:int) → Case
    7. CST O(1)   contenu(c:Case) → Elément
    8. CST O(1)   successeur(c:Case) → Case|VIDE
    9. CST O(1)   ieme(lst:Liste Non Vide, i:int) → Element

    Avantage : l'accès est TOUJOURS à coût CONSTANT, quelque soit l'endroit.

    Problème majeur : l'insertion et la suppression de données est TOUJOURS à coût LINEAIRE. Pas de primitive à coût constant sur au moins l'une des extrémités.

    C'est la raison pour laquelle cette implémentation n'est jamais utilisée. En réalité, quelque soit l'utilisation qu'on veut faire de nos données, il existe une meilleure implémentation pour les gérer.

    1.8 Concaténation de deux listes contiguës statiques

    Imaginons qu'on dispose de deux listes implémentées sous forme de tableaux statiques :

    • la première a 20 000 éléments
    • la seconde 20 000 éléments également.

    Avec la liste chaînée tuple (tête, queue), il ne faut que 20 000 opérations liées à la taille de la liste de gauche.

    C'est moins bien avec des tableaux statiques :


    D'abord, il faut réserver une nouvelle place mémoire de 40 000 places :

    Création d'un nouveau tableau

    Ensuite, il faut déplacer un par un les 20 000 éléments du premier tableau.

    Déplacement des éléments de A

    Puis on déplace les 20 000 éléments du deuxième tableau.

    Déplacement des éléments de B

    Au total, 40 000 opérations. Le double de la liste tuple.

    Et imaginez les performances sur la concaténation de 10 éléments à gauche et 1 milliard à droite :

    • 10 opérations pour la liste-tuple
    • 1 milliard et 10 opérations pour la liste-tableau statique.

    Pas terrible.

    Au vu des coûts assez médiocres, nous n'allons pas aller au bout de ce module comme le précédent. Il va par contre falloir lire et comprendre ces fonctions. Lors du DS, je peux vous demander de recréer telle ou telle fonction.

    MODULE Liste Contiguë statique (ATTENTION, c'est une mauvaise implémentation)

    Ce module devra être nommé liste_contigue_statique.py.

    Vous remarquerez que inserer_tete() a bien été recréée comme une fonction utilisant les primitives plutôt qu'une fonction manipulant directement les données.

    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 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 400 401 402 403 404 405 406 407 408
    """INTERFACE d'une structure de données IMMUABLE Liste Contiguë Description des types non natifs utilisés dans les fonctions + Element : le type des éléments disponibles dans votre Liste si elle est homogène. + Case : la référence d'une case : un tuple (Liste, indice) + Liste : la référence d'un tableau statique Opérations primitives de la Liste contiguë ------------------------------------------ + CST O(1) nouvelle_liste_vide() -> Liste VIDE + CST O(1) est_liste_vide(lst:Liste) -> bool + CST O(1) longueur(lst:Liste) -> int + CST O(1) ieme(lst:Liste NON VIDE, i:int VALIDE) -> Elément + LIN θ(n) inserer(lst:Liste, elt:Element, i:int) -> Liste NON VIDE + LIN θ(n) supprimer(lst:Liste NON VIDE, i:int) -> Liste + CST O(1) acces(lst:Liste NON VIDE, i:int VALIDE) -> Case + CST O(1) contenu(c:Case) -> Element + CST O(1) successeur(c:Case) -> Case|VIDE Fonctions d'interface supplémentaires ------------------------------------- + LIN θ(n) inserer_tete(lst:Liste, elt:Element) -> Liste NON VIDE + LIN θ(n) supprimer_tete(lst:Liste NON VIDE) -> Liste + CST O(1) acces_tete(lst:Liste NON VIDE) -> Case + CST O(1) premier(lst:Liste NON VIDE) -> Element + LIN θ(n) inserer_fin(lst:Liste, elt:Element) -> Liste NON VIDE + LIN θ(n) supprimer_fin(lst:Liste NON VIDE) -> Liste + CST O(1) acces_fin(lst:Liste NON VIDE) -> Case + CST O(1) dernier(lst:Liste NON VIDE) -> Element + LI θ(ng+nd) concatener(gauche:Liste, droite:Liste) -> Liste + LIN O(n) rechercher(lst:Liste, elt:Element) -> int + representer_liste(lst:Liste) -> str + LIN θ(n) liste(x:list|tuple|int|float|str) -> Liste + LIN θ(n) copier(lst:Liste) -> Liste + CST O(1) predecesseur(c:Case) -> Case|VIDE """ # Importation : aucune # Déclaration des CONSTANTES : aucune # Déclaration des opérations primitives def nouvelle_liste_vide() -> 'Liste VIDE': """Renvoie une liste vide""" return [] def est_liste_vide(lst:'Liste') -> bool: """Prédicat qui renvoie True si la liste est vide""" return lst == [] def longueur(lst:'Liste') -> int: """Renvoie le nombre d'éléments stockés dans lst""" return len(lst) def ieme(lst:'Liste NON VIDE', i:'int VALIDE') -> 'Element': """Renvoie le contenu de la Case de position i VALIDE dans la liste NON VIDE lst""" return lst[i] def inserer(lst:'Liste', elt:'Elément', i:'int VALIDE') -> 'Liste NON VIDE': """Renvoie une Liste basée sur lst où elt est en position VALIDE i""" nb = len(lst) + 1 nouveau = [None for _ in range(nb)] # 1 - nouveau tableau avec une case en plus for k in range(0, i): # 2 - copie les éléments des indices 0 à i-1 sans décalage nouveau[k] = lst[k] for k in range(i, len(lst)): # 3 - décalage à droite des éléments d'indice i et plus nouveau[k+1] = lst[k] nouveau[i] = elt # 4 - elt à la position i return nouveau def supprimer(lst:'Liste NON VIDE', i:'int VALIDE') -> 'Liste': """Renvoie une Liste basée sur lst NON VIDE en supprimant la case i""" nb = len(lst) - 1 nouveau = [None for _ in range(nb)] # 1 - nouveau tableau avec une case en moins for k in range(0, i): # 2 - copie les éléments des indices 0 à i-1 sans décalage nouveau[k] = lst[k] for k in range(i+1, len(lst)): # 3 - décalage à gauche des éléments d'indice i+1 et plus nouveau[k-1] = lst[k] return nouveau def acces(lst:'Liste NON VIDE', i:'int VALIDE') -> 'Case': """Renvoie la référence de la case d'indice i dans la liste lst NON VIDE""" return (lst, i) # on crée un tuple-case (quelle liste ?, quel indice ?) def contenu(c:'Case') -> 'Elément': """Renvoie le contenu de la case valide fournie""" lst = c[0] # on récupère la liste contenant notre case i = c[1] # on récupère l'indice de notre case return lst[i] # on renvoie l'élément stocké à cet endroit def successeur(c:'Case') -> 'Case|VIDE': """Renvoie la case qui succède à la case fournie """ lst = c[0] # on récupère la liste contenant notre case i = c[1] # on récupère l'indice de notre case if i == len(lst) - 1: # si la case est la dernière de la Liste return () # on renvoie un Successeur vide else: return (lst, i+1) # on crée le tuple-case et on renvoie # Déclaration des fonctions d'interface supplémentaires def inserer_tete(lst:'Liste', elt:'Elément') -> 'Liste NON VIDE': """Renvoie une Liste basée sur lst où elt est en Tête""" return inserer(lst, elt, 0) def supprimer_tete(lst:'Liste NON VIDE') -> 'Liste': """Renvoie une Liste basée sur lst NON VIDE en supprimant la Tête""" return supprimer(lst, 0) def acces_tete(lst:'Liste NON VIDE') -> 'Case': """Renvoie la référence de la case de tête dans lst NON VIDE""" return acces(lst, 0) def inserer_fin(lst:'Liste', elt:'Elément') -> 'Liste NON VIDE': """Renvoie une Liste basée sur lst où elt est en Fin""" return inserer(lst, elt, longueur(lst)) def supprimer_fin(lst:'Liste NON VIDE') -> 'Liste': """Renvoie une Liste basée sur lst NON VIDE en supprimant la fin""" return supprimer(lst, longueur(lst) - 1) def acces_fin(lst:'Liste NON VIDE') -> 'Case': """Renvoie la référence de la case de fin dans lst NON VIDE""" return acces(lst, longueur(lst) - 1) def premier(lst:'Liste NON VIDE') -> 'Element': """Renvoie la valeur de tête de lst NON VIDE""" return ieme(lst, 0) def dernier(lst:'Liste NON VIDE') -> 'Element': """Renvoie la valeur de fin de lst NON VIDE""" return ieme(lst, longueur(lst) - 1) def representer_liste(lst:'Liste') -> str: """Renvoie une représentation de la Liste sous forme d'une séquence commençant par la tête""" return str(lst).replace(',',' -> ').replace('[','').replace(']','') def concatener(gauche:'Liste', droite:'Liste') -> 'Liste': """Renvoie une nouvelle liste commençant par la tête de gauche vers la fin de droite""" nouveau = nouvelle_liste_vide() # 1 - nouvelle liste pour la concaténation ng = longueur(gauche) for i in range(ng): # 2 - on copie les éléments de gauche nouveau = inserer(nouveau, ieme(gauche, i), i) nd = longueur(droite) for i in range(nd): # 3 - on copie les éléments de droite nouveau = inserer(nouveau, ieme(droite, i), ng + i) return nouveau def concatener_version_primitive(gauche:'Liste', droite:'Liste') -> 'Liste': """Renvoie une nouvelle liste commençant par la tête de gauche vers la fin de droite""" nb = len(gauche) + len(droite) nouveau = [None for _ in range(nb)] # 1 - nouveau tableau avec autant de cases que les deux autres for i in range(0, len(gauche)): # 2 - on copie les éléments de gauche nouveau[i] = gauche[i] ng = len(gauche) # on récupère le nb d'élémnets à gauche for i in range(0, len(droite)): # 3 - on copie les éléments de droite nouveau[ng+i] = droite[i] return nouveau def rechercher(lst:'Liste', elt:'Element') -> int: """Renvoie la position éventuelle de elt dans lst, -1 si non trouvé""" for i in range(longueur(lst)): # Pour chaque indice valide de la liste lst if ieme(lst, i) == elt: # si la case i contient l'élément cherché return i # répondre en renvoyant l'indice de la case return -1 # si on arrive ici, c'est qu'on a pas trouvé def liste(x) -> 'Liste': """Renvoie une liste générée à partir de x""" nouvelle = nouvelle_liste_vide() if type(x) == list or type(x) == tuple: for i in range(len(x)-1, -1, -1): nouvelle = [v for v in x] elif type(x) == dict: for couple in dict.items(): nouvelle = inserer_tete(nouvelle, couple) elif type(x) == int or type(x) == float or type(x) == str or type(x) == bool: nouvelle = inserer_tete(nouvelle, x) return nouvelle def copie(lst:'Liste') -> 'Liste': """Renvoie une copie peu profonde de la liste lst envoyée""" return [v for v in lst] def predecesseur(c:'Case') -> 'Case|VIDE': """Renvoie la case qui précède la case fournie, ou VIDE""" lst = c[0] # on récupère la liste contenant notre case i = c[1] # on récupère l'indice de notre case if i == 0: # si la case est la tête de la Liste return () # on renvoie un prédecesseur vide else: return (lst, i-1) # on crée le tuple-case et on renvoie # Instructions du programme principal if __name__ == '__main__': # Ces fonctions ne seront en mémoire qu'en lancement direct def tester_inserer_tete(): a = nouvelle_liste_vide() a = inserer_tete(a, 5) assert a == [5] a = inserer_tete(a, 2) assert a == [2, 5] b = inserer_tete(a, 20) assert a == [2, 5] assert b == [20, 2, 5] print('Test OK pour inserer_tete()') def tester_supprimer_tete(): a = nouvelle_liste_vide() a = inserer_tete(a, 5) a = inserer_tete(a, 2) b = supprimer_tete(a) assert a == [2, 5] assert b == [5] c = supprimer_tete(b) assert est_liste_vide(c) print('Test OK pour supprimer_tete()') def tester_acces_tete(): a = nouvelle_liste_vide() a = inserer_tete(a, 5) a = inserer_tete(a, 2) assert acces_tete(a) == ([2, 5], 0) print('Test OK pour acces_tete()') def tester_contenu(): a = nouvelle_liste_vide() a = inserer_tete(a, 5) b = inserer_tete(a, 2) assert contenu(acces_tete(a)) == 5 assert contenu(acces_tete(b)) == 2 print('Test OK pour contenu()') def tester_successeur(): a = nouvelle_liste_vide() b = inserer_tete(a, 5) c = inserer_tete(b, 2) d = inserer_tete(c, 20) assert successeur(acces_tete(d)) == ([20, 2, 5], 1) assert successeur(acces_tete(c)) == ([2, 5], 1) print('Test OK pour successeur()') def tester_longueur(): a = nouvelle_liste_vide() b = inserer_tete(a, 5) c = inserer_tete(b, 2) assert longueur(a) == 0 assert longueur(b) == 1 assert longueur(c) == 2 print('Test OK pour longueur()') def tester_acces(): a = nouvelle_liste_vide() b = inserer_tete(a, 5) c = inserer_tete(b, 2) assert acces(b, 0) == ([5], 0) assert acces(c, 0) == ([2, 5], 0) assert acces(c, 1) == ([2, 5], 1) print('Test OK pour acces()') def tester_ieme(): a = nouvelle_liste_vide() b = inserer_tete(a, 5) c = inserer_tete(b, 2) assert ieme(b, 0) == 5 assert ieme(c, 0) == 2 assert ieme(c, 1) == 5 print('Test OK pour lire()') def tester_inserer(): a = nouvelle_liste_vide() b = inserer_tete(a, 5) c = inserer_tete(b, 2) d = inserer(c, 20, 1) assert d == [2, 20, 5] print('Test OK pour inserer()') def tester_supprimer(): a = nouvelle_liste_vide() b = inserer_tete(a, 5) c = inserer_tete(b, 2) d = inserer(c, 20, 1) e = supprimer(d, 2) assert d == [2, 20, 5] assert e == [2, 20] print('Test OK pour supprimer()') def tester_concatener(): a = nouvelle_liste_vide() b1 = inserer_tete(a, 5) b2 = inserer_tete(b1, 2) b3 = inserer_tete(b2, 50) c1 = inserer_tete(a, 20) c2 = inserer_tete(c1, 200) d = concatener(b3, c2) assert d == [50, 2, 5, 200, 20] print('Test OK pour concatener()') def tester_rechercher(): a = nouvelle_liste_vide() b = inserer_tete(a, 5) c = inserer_tete(b, 2) d = inserer(c, 20, 1) assert rechercher(d, 30) == -1 assert rechercher(d, 20) == 1 assert rechercher(d, 2) == 0 assert rechercher(d, 5) == 2 print('Test OK pour rechercher()') def tester(): # Tous les tests tester_inserer_tete() tester_supprimer_tete() tester_acces_tete() tester_contenu() tester_successeur() tester_longueur() tester_acces() tester_ieme() tester_inserer() tester_supprimer() tester_concatener() tester_rechercher() print("Module lancé directement, il ne s'agit pas d'une importation") tester()

    10° Lire et comprendre les fonctions non encore vues pour cette implémentation.

    Estimer le coût pour chacune d'entre elles.

    • Coût de premier() ?
    • Coût de dernier() ?
    • Coût de concatener() en fonction de ng et nd, nombre d'éléments à gauche et à droite ?
    • Coût de rechercher() ?

    Un peu d'aide ?

    Il faut se souvenir de la formule d'Euler :

    Formule de la série

    On peut également écrire cette formule ainsi :

    Formule de la série

    Une somme d'opérations de ce type mène donc à un coût quadratique.

    ...CORRECTION...

    • Coût de premier() ? Constant.
    • Coût de dernier() ? Constant.
    • Coût de concatener() en fonction de ng et nd ? C'est plus compliqué.

      On ramène le premier élément et on crée un tableau de 1 élément (en 1 action élémentaire).

      On ramène le deuxième élément et on crée un tableau de 2 éléments (en 2 actions élémentaires).

      Ensuite, on ramène un encore élément et on crée un tableau de 3 éléments (en 3 actions élémentaires).

      Puis un autre (en 4 actions élémentaires) jusqu'à avoir récupérer toutes les valeurs des deux tableaux.

      On doit faire la somme de 1 + 2 + 3 +.... (ng+nd) opérations.

      𝞗( (ng+nd)*(ng+nd+1)/2 )

      = 𝞗( (ng+nd)*(ng+nd)/2 )

      = 𝞗( (ng+nd)*(ng+nd) )

      = 𝞗( (ng+nd)2 )

      Le coût est donc quadratique en 𝞗( (ng+nd)2 ) !

    • Coût de rechercher() ? Linéaire, en 𝓞(n)
    • En effet, dans le pire des cas, l'élément cherché n'est pas présent et on doit faire n tours de boucle et chaque tour de boucle ne comporte que des actions à coût CONSTANT. On a donc du 𝞗(n) dans le pire des cas, et donc dans le cas général on peut écrire 𝓞(n)

    11° Observer la fonction concatener_version_primitive() qui manipule directement le type list de Python.

    1 2 3 4 5 6 7 8 9 10
    def concatener_version_primitive(gauche:'Liste', droite:'Liste') -> 'Liste': """Renvoie une nouvelle liste commençant par la tête de gauche vers la fin de droite""" nb = len(gauche) + len(droite) nouveau = [None for _ in range(nb)] # 1 - nouveau tableau avec autant de cases que les deux autres for i in range(0, len(gauche)): # 2 - on copie les éléments de gauche nouveau[i] = gauche[i] ng = len(gauche) # on récupère le nb d'élémnets à gauche for i in range(0, len(droite)): # 3 - on copie les éléments de droite nouveau[ng+i] = droite[i] return nouveau

    Quel est son coût ?

    Quel est l'avantage de cette version ?

    Quel est le désavantage de cette version ?

    ...CORRECTION...

      1 2 3 4 5 6 7 8 9 10
      def concatener_version_primitive(gauche:'Liste', droite:'Liste') -> 'Liste': """Renvoie une nouvelle liste commençant par la tête de gauche vers la fin de droite""" nb = len(gauche) + len(droite) --> Coût CONSTANT nouveau = [None for _ in range(nb)] --> Coût LIN en (ng+nd) for i in range(0, len(gauche)): --> on boucle ng fois sur une action nouveau[i] = gauche[i] CST --> Coût LIN en ng ng = len(gauche) --> Coût CONSTANT for i in range(0, len(droite)): --> on boucle nd fois sur une action nouveau[ng+i] = droite[i] CST --> Coût LIN en nd return nouveau --> Coût CONSTANT
    • Cette fois, on voit que le coût est linéaire et la complexité en θ(ng+nd)
    • Avantage : linéaire, c'est beaucoup mieux que quadratique
    • Désavantage : non respect de l'interfaçage.

    12° Un utilisateur observant sa liste avec representer_liste() peut-il savoir qu'on travaille avec une implémentation tableau statique ?

    ...CORRECTION...

    Non, un utilisateur ne pourra pas depuis l'extérieur savoir comment notre implémentation est réalisée. L'intérêt pour lui étant que son programme n'aura pas à être modifié même si l'implémentation change mais que les fonctions d'interface restent inchangées.

    2 - Implémentation en utilisant directement list de Python

    Connaissance au programme de NSI : vous devez savoir que le type list de Python n'est pas une liste (chaînée).

    Nous allons voir aujourd'hui ce qui se cache derrière ce type list de Python : une liste contiguë mutable et dynamique (on peut faire varier le nombre de cases).

    2.1 - Le tableau dynamique de Python (hors programme mais utile)

    A - Le type list de Python

    Le type list de Python est un tableau mutable et dynamique :

    • On peut insérer à coût constant à la fin, en utilisant append()
    • On peut supprimer à coût constant à la fin , en utilisant pop()
    B - Le principe de l'insertion dans un tableau dynamique

    Voici comment on parvient à agir à coût constant avec la fin du tableau : on crée un tableau possédant plus de cases que nécessaire.

    Imaginons qu'on veuille initialement créer un tableau de 5 cases :

    >>> t = [1, 3, 4, 1, 2]

    Python réserve en réalité le double de cases : il vous en faut 5, l'interpréteur va générer un tableau STATIQUE de 10 cases. Mais les cases d'indice 05 à 09 sont juste vides. Si vous demandez la taille du tableau, on vous répondra bien 5 car le dernier indice réellement utilisé est 04.

    >>> len(t) 5

    Rajouter un élément est donc facile : il suffit de le placer sur l'indice 5 (qui existe déjà) et de décaler la valeur du dernier indice réellement utilisé.

    >>> t.append(3)

    En mémoire, on a juste décalé de 1 la valeur maximale de l'indice possible et on place le 3 dans cette case finale. L'opération se fait donc à temps CONSTANT.

    Si on continue à rajouter des choses, nous allons aboutir à un tableau totalement rempli.

    >>> t.append(1) >>> t.append(9) >>> t.append(18) >>> t.append(8)

    Problème : notre tableau dynamique est plein !

    Pourtant, on peut continuer à rajouter des éléments... Comment est-ce que cela est possible ?

    >>> t.append(7)

    Le rajout se fait en deux temps.

    ETAPE 1 : Python crée un nouveau tableau deux fois plus grand (on passe de 10 à 20 cases sur notre exemple).

    ETAPE 2 : Python copie le contenu, case par case puis rajoute la nouvelle valeur dans la nouvelle valeur maximale d'indice !

    Cette fois, l'insertion a été faite à coût LINEAIRE, puisqu'il a fallu recopier les valeurs avant de parvenir à mettre le 7 en fin de tableau.

    Par contre, insérer encore un élément se fera à nouveau à coût constant : il suffit de décaler l'indice maximum et d'y placer le nouvel élément.

    >>> t.append(5)

    CONCLUSION : l'insertion n'est pas exactement à coût CONSTANT puisqu'une fois de temps à autre, l'insertion sera à coût LINEAIRE.

    Mais, en moyenne, c'est quasiment CONSTANT.

    Exemple avec un tableau initialement vide.

    • 1er insertion : création d'un tableau de 1 case (1 action).
    • 2e insertion : tableau plein, on double à 2 cases (linéaire à 2).
    • 3e insertion : tableau plein, on double à 4 cases (linéaire à 4)
    • 4e insertion : coût constant (1)
    • 5e insertion : tableau plein, on double à 8 cases (linéaire à 8).
    • 6e insertion : coût constant
    • 7e insertion : coût constant
    • 8e insertion : coût constant
    • 9e insertion : tableau plein, on double à 16 cases (linéaire à 16).
    • 10e insertion : coût constant
    • 11e insertion : coût constant
    • 12e insertion : coût constant
    • 13e insertion : coût constant
    • 14e insertion : coût constant
    • 15e insertion : coût constant

    Si on calcule le nombre d'opérations nécessaires pour insérer les 15 données en moyenne, il faut calculer ceci :

    n = (1+2+4+1+8+1+1+1+16+1+1+1+1+1+1)/15 = 2.73

    Si l'insertion était à coût linéaire, nous aurions eu par exemple :

    n = (1+2+3+4+5+6+7+8+9+10+11+12+13+14+15)/15 = 8.00

    C - Suppression dans un tableau dynamique

    Pour la suppression, c'est encore plus simple : il suffit de décaler l'indice maximum d'une valeur vers la gauche. Même pas besoin de supprimer le contenu de la case, on la rend juste inaccessible pour l'utilisateur.

    >>> t.pop() 5
    L'essentiel

    On peut insérer en fin de list Python avec un coût amorti constant avec append().

    On peut supprimer en fin de list Python avec un coût amorti constant avec pop().

    Maintenant vous savez vraiment pourquoi.

    2.2 - Coûts de certaines méthodes de list

    Vous avez maintenant compris que :

    • append() possède un coût amorti constant.
    • pop() possède un coût constant, si on l'utilise pour supprimer la fin.

    Il reste à voir deux utilisations qui ont l'air "magique", mais qui ont un coût non constant...

    Insertion de nouveaux éléments avec la méthode insert()

    La méthode append() permet de rajouter EN FIN un nouvel élément à d'une list-Python.

    Mais il existe également une méthode nommée insert() qui permet de faire la même chose MAIS en choisissant la position de l'insertion via un indice, comme notre fonction inserer().

    Dans ce cas, il faut faire 3 choses :

    1. On augmente de 1 l'indice maximum.
    2. On décale à droite n-i valeurs : celles d'indice i à n-1, qu'on place dans les cases d'indice i+1 à n.
    3. On place la valeur à insérer dans la case i.

    Coût de l'insertion en i

    L'insertion est donc linéaire et la complexité en 𝞗(n-i). On a donc du 𝓞(n).

    1 2 3
    def inserer(lst:'Liste', x:'Elément', i:'int VALIDE') -> None: """Modifie en place la liste en insérant l'élément elt à l'indice i fourni""" lst.insert(i, x)

    La liste est mutable, les données ont été modifiées directement en mémoire. Votre fonction n'a donc pas besion de renvoyer une nouvelle référence.

    Vu la complexité, on comprend qu'il est plus difficile d'insérer près de la tête que de la fin. C'est l'inverse de la liste chaînée tuple.

    Suppression d'un élément via la méthode pop()

    La méthode nommée pop() fait deux choses : elle supprime une case ET renvoie l'élément correspondant.

    La suppression de l'élément en position i se fait en plusieurs étapes :

    1. On décale à gauche n-i valeurs : celles d'indice i+1 à n-1, qu'on place dans les cases d'indice i à n-2.
    2. On diminue de 1 la valeur de l'indice maximum.

    Coût de la suppression en i

    La suppression est donc linéaire et la complexité en 𝞗(n-i). On a donc du 𝓞(n).

    Pour supprimer ET récupérer l'ancien élément d'indice 0 :

    1
    valeur_extraite = t.pop(0)

    Pour juste supprimer l'ancien élément d'indice 0 sans le mémoriser :

    1
    t.pop(0)

    Voici donc notre nouvelle fonction d'interface supprimer() :

    1 2 3
    def supprimer(lst:'Liste NON VIDE', i:'int VALIDE') -> 'Elément': """Renvoie l'élément supprimé à l'indice i VALIDE dans lst NON VIDE""" return lst.pop(i)
    Bilan

    Si on résume :

    • append(elt)) : un coût amorti constant 𝓞(1) pour l'insertion en fin.
    • pop() : un coût constant 𝓞(1) pour la suppression en fin.
    • insert(i, elt) : un coût linéaire 𝓞(n) pour l'insertion, le pire des cas étant sur l'indice 0.
    • pop(i) : un coût linéaire 𝓞(n) pour la suppression, le pire des cas étant en sur l'indice 0.
    2.3 - Version incomplète de la liste contiguë MUABLE basée sur le type list

    Voici les primitives qu'on sélectionne classiquement pour manipuler une Liste contiguë MUTABLE :

    1. nouvelle_liste_vide() → Liste VIDE
    2. est_liste_vide(lst:Liste) → bool
    3. longueur(lst:Liste) → int
    4. inserer_fin(lst:Liste, elt:Elément) → None
    5. supprimer_fin(lst:Liste NON VIDE) → Element
    6. ieme(lst:Liste NON VIDE, i:int VALIDE) → Element

    Au vu des signatures de l'insertion et la suppression, on voit que la liste est muable. D'ailleurs, puisque supprimer_fin() n'a rien à renvoyer, on en profite pour lui faire renvoyer l'élément qu'on vient de supprimer.

    On pourrait bien entendu générer également les 3 primitives en interaction avec les Cases.

    1. acces(lst:Liste NON VIDE, i:int VALIDE) → Case
    2. contenu(c:Case) → Elément
    3. successeur(c:Case) → Case|VIDE
    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
    """Implémentation d'une LISTE MUABLE en utilisant un tableau dynamique list-Python Description des primitives ------------------------------------ + CST O(1) nouvelle_liste_vide() -> Liste NON VIDE + CST O(1) est_liste_vide(lst:Liste) -> bool + CST O(1) longueur(lst:Liste) -> int + CST O(1) ieme(lst:Liste NON VIDE, i:int VALIDE) -> Elément + CST O(1) inserer_fin(lst:Liste, elt:Elément) -> None + CST O(1) supprimer_fin(lst:Liste NON VIDE) -> Elément Description d'autres fonctions' ------------------------------ + LIN O(n-i) inserer(lst:Liste, elt:Elément, i:int VALIDE) -> None + LIN O(n-i) supprimer(lst:Liste NON VIDE, i:int VALIDE) -> Elément + representer_liste(lst:Liste) -> str """ def nouvelle_liste_vide() -> 'Liste VIDE': """Renvoie une liste vide""" return [] def est_liste_vide(lst:'Liste') -> bool: """Renvoie True si la liste est vide""" return lst == [] def longueur(lst:'Liste') -> int: """Renvoie le nombre de cases utilisées dans le tableau""" return len(lst) def ieme(lst:'Liste NON VIDE', i:int) -> 'Element': """Renvoie la valeur stockée à l'indice VALIDE voulu de lst NON VIDE""" return lst[i] def inserer_fin(lst:'Liste', elt:'Elément') -> None: """Modifie en place la liste en insérant l'élément en fin de liste""" lst.append(elt) def supprimer_fin(lst:'Liste NON VIDE') -> 'Element': """Renvoie l'élément de fin supprimé dans lst NON VIDE""" return lst.pop() def inserer(lst:'Liste', elt:'Elément', i:'int VALIDE') -> None: """Modifie en place la liste en insérant l'élément à l'indice fourni""" lst.insert(i, elt) def supprimer(lst:'Liste NON VIDE', i:'int VALIDE') -> 'Element': """Renvoie l'élément supprimé à l'indice VALIDE dans lst NON VIDE""" return lst.pop(i) def representer_liste(lst:'Liste'): """Renvoie une représentation de la Liste sous forme d'une séquence commençant par la tête""" return str(lst).replace(',',' -> ').replace('[','').replace(']','')

    13° Placer cette nouvelle implémentation en mémoire : cette fois, elle utilise directement les méthodes du type list de Python pour créer une LISTE MUABLE.

    Question : créer un programme de test (avec if __name__ == '__main__': qui crée puis affiche avec print() une liste dont la tête est 200 et les éléments suivants 100 et 10. Attention : la liste est mutable et les fonctions d'interface ne s'utilisent pas de la même façon en MUTABLE et en IMMUABLE...

    14° Compléter le programme de test en utilisant cette fois representer_liste() pour observer le contenu de votre liste mutable. L'utilisateur peut-il savoir que les données de la liste sont stockées sous forme d'un tableau dynamique ?

    ...CORRECTION...

    Non, un utilisateur ne pourra toujours pas savoir comment notre implémentation est réalisée. Normalement, à ce stade des activités d'implémentation, vous devriez avoir compris :o)

    15° Réaliser la fonction concatener() qui devra renvoyer une nouvelle liste. Ni la liste gauche, ni la liste gauche ne devront avoir été donc modifiées.

    Prototype

    def concatener(gauche:'Liste', droite:'Liste') -> 'Liste':

    Votre fonction devra avoir un coût LINEAIRE par rapport au nombre d'éléments de gauche et de droite et devra uniquement manipuler les primitives et pas le type list directement.

    ...CORRECTION...

    1 2 3 4 5 6 7
    def concatener(gauche:'Liste', droite:'Liste') -> 'Liste': nouveau = nouvelle_liste_vide() for i in range(longueur(gauche)): inserer_fin(nouveau, ieme(gauche, i)) for i in range(longueur(droite)): inserer_fin(nouveau, ieme(droite, i)) return nouveau

    16° Même question mais en manipulant directement le type list. Vous devriez obtenir le même coût.

    Prototype

    def concatener(gauche:'Liste', droite:'Liste') -> 'Liste':

    Votre fonction devra avoir un coût LINEAIRE par rapport au nombre d'éléments de gauche et de droite et devra uniquement manipuler uniquement les primitives, pas le type list directement.

    ...CORRECTION...

    1 2 3 4 5 6 7
    def concatener_primitive(gauche:'Liste', droite:'Liste') -> 'Liste': nouveau = [] for i in range(len(gauche)): nouveau.append(gauche[i]) for i in range(len(droite)): nouveau.append(droite[i]) return nouveau
    CONCLUSION sur le type list Python

    Chaque fonction codant une primitive ne possède qu'une seule instruction. Comme vous le voyez, il est plutôt inutile de vouloir créer une interface pour gérer une liste contiguë mutable en utilisant le type list de Python : pourquoi recréer des fonctions d'interface alors que le créateur de Python a justement créer le type list avec cette idée en tête. De plus, beaucoup d'opérations disponibles sur le type list de Python sont optimisées (tri, concaténation...)

    Néanmoins, vous en savez maintenant beaucoup plus sur cette structure de données native de Python.

    Par contre, je rappelle la connaissance à avoir pour l'épreuve du BAC : vous devez savoir que le type list de Python n'implémente pas une liste chaînée mais bien un tableau dynamique.

    3 - FAQ

    On peut avoir le module déjà réalisé ?

    Vous pouvez maintenant directement télécharger ce module :

    MODULE LISTE CONTIGUE TABLEAU STATIQUE

    MODULE LISTE CONTIGUE MUABLE TABLEAU DYNAMIQUE

    On m'a dit de faire attention aux copies de tableaux. C'est vrai ?

    Oui. Si vous voulez obtenir une copie d'un tableau, ne faites surtout pas cela :

    >>> a = [2, 40, 3] >>> b = a >>> b [2, 40, 3]

    Pourquoi ? Ca a l'air de marcher !

    Oui mais non. La variable a ne contient pas les DONNEES du tableau mais son ADRESSE-MEMOIRE (ou son équivalent en Python). Et donc b contient aussi en réalité cette adresse.

    >>> id(a) 50505050 >>> id(b) 50505050 >>> id(a)

    Du coup, a et b sont des alias des mêmes données. Si vous modifiez a, vous modifiez b.

    >>> a [2, 40, 3] >>> b [2, 40, 3] >>> a[0] = 1000 >>> b [1000, 40, 3]

    Comment faire alors ?

    Si votre tableau ne contient que des données immuables, vous pouvez recréer un tableau par compréhension :

    >>> a = [2, 40, 3] >>> b = [valeur for valeur in a] >>> b [2, 40, 3] >>> a[0] = 1000 >>> a [1000, 40, 3] >>> b [2, 40, 3]

    Si votre tableau contient des données muables, le mieux est encore d'utiliser le module copy et sa fonction deepcopy() de copie profonde.

    Exemple avec a qui est un tableau contenant des tableaux

    >>> import copy >>> a = [[4,2,0], [40,20,0], [3,4,8]] >>> b = copy.deepcopy(a) >>> b [[4,2,0], [40,20,0], [3,4,8]] >>> a[0][2] = 1000 >>> a [[4, 2, 1000], [40, 20, 0], [3, 4, 8]] >>> b [[4, 2, 0], [40, 20, 0], [3, 4, 8]]
    Chaînée
    Tuple
    (Immuable)
    Tableau
    dynamique
    (muable)
    Chaînée
    Objet:Tête
    (muable)
    Accès TETE CST CST
    ... insérer en TETE CST LINEAIRE
    𝞗(n)
    ... supprimer la TETE CST LINEAIRE
    𝞗(n)
    Chaînée
    Tuple
    (Immuable)
    Tableau
    dynamique
    (muable)
    Chaînée
    Objet:Tête
    (muable)
    Accès à la position i LINEAIRE
    𝞗(i)
    𝓞(n)
    CST
    ... insérer en position i LINEAIRE
    𝞗(i)
    𝓞(n)
    LINEAIRE
    𝞗(n-i)
    𝓞(n)
    ... supprimer la position i LINEAIRE
    𝞗(i)
    𝓞(n)
    LINEAIRE
    𝞗(n-i)
    𝓞(n)
    Chaînée
    Tuple
    (Immuable)
    Tableau
    dynamique
    (muable)
    Chaînée
    Objet:Tête
    (muable)
    Accès FIN LINEAIRE
    𝞗(n)
    CST
    ... insérer en FIN LINEAIRE
    𝞗(n)
    CST
    ... supprimer la FIN LINEAIRE
    𝞗(n)
    CST
    Chaînée
    Tuple
    (Immuable)
    Tableau
    dynamique
    (muable)
    Chaînée
    Objet:Tête
    (muable)
    Concaténation LINEAIRE
    𝞗(ng)
    LINEAIRE
    𝞗(ng+nd)

    La fois prochaine, nous verrons une dernière implémentation du type abstrait LISTE : la liste chaînée muable en version programmation objet.

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