l’élément d’indice i, il suffit donc d’aller chercher son adresse dans la (i+1)-ème case et pour
cela d’ajouter ià l’adresse de début de tableau. Cela se fait en temps constant Θ(1). Pour
modifier cet élément, il suffit de faire pointer la case correspondante sur un autre objet Python
ce qui se fait aussi en Θ(1).
Pour ajouter un élément à la liste, il faut modifier n(ce dont nous ne parlerons pas) et ajouter
une case au tableau. Si la case qui suit directement les ncases consécutives est “libre”, pas de
problème, on y met le pointeur qui désigne l’élément ajouté, ce qui se fait en Θ(1). Si ce n’est
pas le cas, on doit alors déplacer tout le tableau vers une zone libre plus grande, où on pourra
le prolonger, ce qui nécessite la recopie intégrale des ncases et se fait en Θ(n). Pour que cela
arrive le moins souvent possible, on utilise d’habitude le procédé suivant : au départ, on prévoit
en général plus de place que les ncases initiales, au cas où on voudrait prolonger la liste. Puis
lors de chaque déplacement forcé, on s’arrange pour doubler la place mémoire disponible et la
réserver. Cela réduit le nombre de déplacements et le coût d’ajout de péléments est alors en
Θ(max(n,p)). Par exemple, si on part d’une liste vide et on ajoute un à un néléments à la liste,
cela fait un nombre maximal de recopies de cases 'n+n
2+n
4· · · =Θ(n), et la complexité de
l’opération globale a le même ordre de grandeur que s’il n’y avait pas eu de déplacement ! En
pratique, on lisse le coût des déplacements et on fait l’approximation que les ajouts se font en
temps constant, convention à adopter dans vos calculs de complexité un jour d’épreuve.
Cependant, si on connaît au départ la taille de la liste, il est vivement conseillé de réserver
dès le départ la zone mémoire nécessaire au stockage du tableau dynamique, par exemple à
l’aide d’une instruction L = [0]*n. Remarquons au passage que la création d’une liste par
compréhension (du type [0 for i in range(n)]) est implémentée de façon très efficace et
peut remplacer la réservation précédente. Dans le cas d’une liste de liste, la “compréhension”
est même la méthode à utiliser, à cause de l’erreur fréquente obtenue avec le code fautif L =
[[0]*n]*p (la modification d’une ligne impactant alors toutes les lignes !).
Pour enlever un élément à la liste, ou insérer un élément à la liste, il faut décaler d’une case
tous les pointeurs à partir de l’endroit de suppression/insertion, ce qui est d’autant plus coûteux
que l’élément supprimé est proche du début du tableau. Cela se fait au pire en Θ(n).
On peut voir plus de détails sur les complexités des opérations sur les listes en CPython à
l’adresse https://wiki.python.org/moin/TimeComplexity.
III Listes chaînées
Une liste chaînée est une suite d’entités, stockées éventuellement à des endroits éloignés les
uns des autres en mémoire, dont chacune :
— contient un pointeur vers un objet
— contient un pointeur vers l’entité suivante
La dernière entité admet comme “suivante” un objet spécifique, usuellement noté NIL, si-
gnifiant la fin de la liste chaînée. La liste est alors donnée par un pointeur vers la première entité.
On peut au besoin créer facilement une telle classe en Python.
Dans le cas d’une implémentation par liste chaînée, l’accès au i-ème élément se fait en
parcourant la liste, donc au pire en Θ(n), ce qui est nettement moins bon que dans le cas d’un
tableau dynamique. En revanche, l’ajout se fait toujours en temps constant, et l’insertion ou la
suppression sont beaucoup plus rapides quand on est proche du début de la liste.
2