Partie II - Structures de donnees · Chapitre 5

Structures de données

3 sections de cours · fiche d'exercices corrigee

1Types abstraits et panorama des structures

Jusqu'ici, une variable contenait une seule valeur. Des qu'il s'agit de manipuler des collections — les notes d'une classe, les sommets d'un graphe, les pixels d'une image — il faut des structures de donnees. Le choix de la structure determine directement l'efficacite des algorithmes qui l'utilisent : c'est souvent la decision la plus importante d'un programme.

Definition — Type abstrait / implementation

Un type abstrait de donnees (TAD) decrit un ensemble de valeurs et les operations qu'on peut effectuer dessus, sans dire comment elles sont realisees. Une implementation (ou structure de donnees) est une realisation concrete en memoire. Un meme TAD admet plusieurs implementations, de couts differents.

Exemple : le TAD « pile » offre empiler, depiler, est_vide. Il peut etre implemente par un tableau dynamique ou par une liste chainee.

Les quatre criteres de classification

CritereQuestionExemples
Mutabilitepeut-on modifier la structure apres creation ? mutable : list, dict, set
immuable : str, tuple, frozenset
Ordreles elements ont-ils une position ? ordonne : list, tuple, str
non ordonne : set
Doublonsun element peut-il apparaitre deux fois ? oui : list ; non : set, cles d'un dict
Accescomment atteint-on un element ? par indice : list ; par cle : dict ; par appartenance : set

Panorama des structures Python

t = [3, 1, 4, 1, 5]              # list  : ordonnee, mutable, doublons
u = (3, 1, 4)                    # tuple : ordonnee, IMMUABLE, doublons
s = "MPSI"                       # str   : ordonnee, immuable, caracteres
e = {3, 1, 4, 1, 5}              # set   : NON ordonne, mutable, sans doublon -> {1,3,4,5}
d = {"a": 1, "b": 2}             # dict  : association cle -> valeur
Definition — Hachabilite

Un objet est hachable si l'on peut lui associer un entier stable pendant toute sa vie (sa valeur de hachage). Seuls les objets hachables peuvent etre elements d'un ensemble ou cles d'un dictionnaire. En pratique : les objets immuables sont hachables, les objets mutables ne le sont pas.

{(1, 2), (3, 4)}         # OK : les tuples sont hachables
{[1, 2]}                 # TypeError: unhashable type: 'list'
d = {(0, 0): "origine"}  # une cle peut etre un tuple (utile pour une grille)

Operations communes aux sequences

OperationSensVaut pour
len(x)nombre d'elementstous
x[i]element d'indice ilist, tuple, str
x[a:b:p]tranchelist, tuple, str
v in xappartenancetous
x + yconcatenationlist, tuple, str
x * nrepetitionlist, tuple, str
for v in xparcourstous

Indexation et tranches

t = [10, 20, 30, 40, 50]
#     0    1    2    3    4      indices positifs
#    -5   -4   -3   -2   -1      indices negatifs

t[0]        # 10   premier
t[-1]       # 50   dernier (idiome a connaitre)
t[1:4]      # [20, 30, 40]   de 1 inclus a 4 EXCLU
t[:3]       # [10, 20, 30]   depuis le debut
t[2:]       # [30, 40, 50]   jusqu'a la fin
t[::2]      # [10, 30, 50]   un sur deux
t[::-1]     # [50, 40, 30, 20, 10]   renversee (copie)
t[:]        # copie superficielle complete
Indice vs tranche

t[10] leve IndexError si la liste est trop courte, mais t[5:10] renvoie tranquillement []. Une tranche ne provoque jamais d'erreur de bornes : c'est pratique, mais cela masque parfois un bug.

2Cout des operations et choix d'une structure

Le choix d'une structure se fait sur le cout des operations qu'on va effectuer le plus souvent. Voici les couts a connaitre par coeur : ils justifient toutes les decisions de conception du reste du programme.

Couts des operations (complexite en temps)
Operationlistdict set
acces t[i] / d[k]\(\Theta(1)\) \(\Theta(1)\) en moyenne—
test v in x\(\Theta(n)\) \(\Theta(1)\) en moyenne\(\Theta(1)\) en moyenne
ajout en fin (append / add) \(\Theta(1)\) amorti\(\Theta(1)\)\(\Theta(1)\)
insertion / suppression en tete\(\Theta(n)\) ——
suppression d'une valeur\(\Theta(n)\)\(\Theta(1)\) \(\Theta(1)\)
parcours complet\(\Theta(n)\)\(\Theta(n)\)\(\Theta(n)\)
tri\(\Theta(n\log n)\)——
La difference n'est pas theorique
import time

n = 200000
liste = list(range(n))
ensemble = set(liste)

t0 = time.perf_counter()
for k in range(2000):
    k in liste           # Theta(n) a chaque fois
t1 = time.perf_counter()
for k in range(2000):
    k in ensemble        # Theta(1) a chaque fois
t2 = time.perf_counter()

print(f"liste    : {t1-t0:.3f} s")     # ~ 2 s
print(f"ensemble : {t2-t1:.6f} s")     # ~ 0.0004 s

Facteur 5000 sur une operation d'apparence identique. Ecrire if x in ma_liste dans une boucle est l'erreur de performance la plus courante en Python.

Methode — choisir sa structure
Le besoinLa structure
l'ordre compte, on ajoute a la fin, on accede par indicelist
on teste souvent l'appartenance, l'ordre est indifferentset
on associe une information a une cledict
la donnee ne doit pas changer, ou doit servir de cletuple
dernier entre, premier sortipile = list + append/pop
premier entre, premier sortifile = collections.deque

Copie superficielle et copie profonde

a = [1, 2, 3]
b = a              # PAS une copie : deux noms pour le MEME objet
b.append(4)
print(a)           # [1, 2, 3, 4]  : a a change !

c = a[:]           # copie superficielle (ou list(a), ou a.copy())
c.append(5)
print(a)           # [1, 2, 3, 4]  : a est protegee
Copie superficielle : la limite
m = [[0, 0], [0, 0]]
n = m[:]              # copie SUPERFICIELLE
n[0][0] = 9
print(m)              # [[9, 0], [0, 0]]  : les sous-listes sont PARTAGEES

Une copie superficielle duplique le contenant, pas les objets contenus. Pour une structure imbriquee, il faut une copie profonde :

import copy
n = copy.deepcopy(m)      # tout est duplique, recursivement
# ou, pour une matrice, l'idiome explicite :
n = [ligne[:] for ligne in m]
Le piege de * n
m = [[0] * 3] * 2         # PIEGE
m[0][0] = 9
print(m)                  # [[9, 0, 0], [9, 0, 0]]  : une seule ligne, dupliquee !

m = [[0] * 3 for _ in range(2)]    # CORRECT : deux listes distinctes
m[0][0] = 9
print(m)                  # [[9, 0, 0], [0, 0, 0]]

liste * n repete la reference, pas l'objet. C'est sans consequence pour des entiers (immuables), catastrophique pour des listes.

Structures lineaires classiques

TADDisciplineOperationsUsage typique
Pile (LIFO)dernier entre, premier sorti empiler, depiler, sommet appels de fonctions, parcours en profondeur, expressions parenthesees
File (FIFO)premier entre, premier sorti enfiler, defiler parcours en largeur, files d'attente, tampons
Tableauacces direct par indice t[i], len tout ce qui se numerote

Ces structures sont detaillees au chapitre 10. Retenir des maintenant que pile et file ne different que par l'ordre de sortie, et que ce simple choix change completement l'algorithme de parcours qui les utilise.

3Construire, copier, comparer

Construire une structure : les comprehensions

carres  = [k*k for k in range(6)]                   # [0, 1, 4, 9, 16, 25]
pairs   = [k for k in range(20) if k % 2 == 0]      # filtrage
couples = [(i, j) for i in range(3) for j in range(2)]   # produit cartesien
ens     = {k % 5 for k in range(20)}                # comprehension d'ensemble
dic     = {k: k*k for k in range(5)}                # comprehension de dictionnaire

Une comprehension se lit « l'expression, pour chaque element, tel que condition ». Elle remplace avantageusement une boucle avec append, mais on ne l'utilise que si elle tient lisiblement sur une ligne ou deux.

Equivalence
# les deux fragments sont equivalents
t = []
for k in range(6):
    if k % 2 == 0:
        t.append(k*k)

t = [k*k for k in range(6) if k % 2 == 0]

Deballage et parcours simultane

a, b, c = [1, 2, 3]                 # deballage
premier, *reste = [1, 2, 3, 4]      # premier = 1, reste = [2, 3, 4]

noms  = ["Amine", "Sara", "Youssef"]
notes = [15, 18, 12]
for nom, note in zip(noms, notes):
    print(f"{nom} : {note}")

for i, nom in enumerate(noms, start=1):
    print(i, nom)

Etude de cas — analyse d'un texte

Le meme probleme, resolu avec trois structures differentes, illustre l'impact du choix.

texte = "le chat dort le chien dort le chat mange"
mots = texte.split()

# 1. Combien de mots distincts ? -> un ensemble
distincts = set(mots)
print(len(distincts))                 # 6

# 2. Combien de fois chaque mot ? -> un dictionnaire
freq = {}
for m in mots:
    freq[m] = freq.get(m, 0) + 1
print(freq)      # {'le': 3, 'chat': 2, 'dort': 2, 'chien': 1, 'mange': 1}

# 3. Les mots par frequence decroissante ? -> une liste triee de couples
classement = sorted(freq.items(), key=lambda c: (-c[1], c[0]))
print(classement[:3])       # [('le', 3), ('chat', 2), ('dort', 2)]

Chaque question appelle sa structure : ensemble pour l'unicite, dictionnaire pour l'association, liste pour l'ordre. Vouloir tout faire avec des listes conduirait a un code quadratique et illisible.

Comparateur de structures : cout d'une recherche

On cherche une valeur absente dans une collection de \(n\) elements. Faire varier \(n\) et observer le nombre de comparaisons effectuees.

Deplacer le curseur puis cliquer sur « Comparer ».

L'essentiel du chapitre

  • Un type abstrait decrit des operations ; une structure de donnees les realise, a un cout donne.
  • Quatre criteres : mutabilite, ordre, doublons, mode d'acces.
  • Seuls les objets immuables sont hachables, donc utilisables comme cles ou elements d'ensemble.
  • v in liste coute \(\Theta(n)\), v in ensemble coute \(\Theta(1)\).
  • b = a n'est pas une copie ; a[:] est une copie superficielle ; [l[:] for l in m] pour une matrice.
  • [[0]*3]*2 partage les lignes : toujours utiliser une comprehension.
Fiche d'exercices — chapitre 5Enonces gradues, du calcul d'application au niveau concours, avec corriges detailles.
Ouvrir la fiche →