Structure de liste¶
03-04-2023
Objectifs
connaître la structure de données liste
une première structure de données récursive
listes non mutables ou mutables
savoir en donner une implantation
savoir l’utiliser
Motivation¶
Structure de données importante en informatique :
les listes sont partout
Remarque
Les listes existent déjà dans Python. Alors pourquoi les étudier et en donner des implantations ?
Voici quelques éléments de réponse :
Les listes en Python permettent un accès direct (du moins en apparence, car il faudrait voir ce qu’il en est réellement) à leurs éléments à l’aide d’indices. Ce qui fait que les listes de Python peuvent être utilisées comme ce que dans d’autres langages de programmation on appelle tableau (ou array).
Les tableaux sont des structures de données statiques, c’est-à-dire dont l’occupation mémoire est fixé une fois pour toute. Un tableau à \(n\) éléments est une zone mémoire de taille \(n\times t\) octets consécutifs, \(t\) étant la taille (en octets) d’un élément de chacun des éléments du tableau. De cette façon si on connaît l’adresse en mémoire \(adr\) du premier élément du tableau, celui d’indice 0, alors l’élément d’indice \(i\) se trouve à l’adresse \(adr + i\times t\). C’est ce simple calcul qui explique la facilité d’accès à n’importe quel élément d’un tableau.
Les listes ne sont a priori pas des structures de données statiques, mais plutôt des structures de données dynamiques. Rien ne détermine a priori le nombre d’éléments que peut contenir une liste.
Il est intéressant de comprendre comment on peut gérer le caractère dynamique d’une structure de données.
Vous êtes là pour apprendre !
Qu’est ce qu’une liste ?¶
Nous savons tous intuitivement ce qu’est une liste. Une liste est une collection finie d’éléments qui se suivent. C’est donc une structure de données séquentielle ou linéaire.
Une liste peut contenir un nombre quelconque d’éléments y compris nul (la liste vide).
Nous allons essayer de dégager une définition plus formalisée de ce qu’est une liste afin d’en dégager les opérations primitives et une structure essentielle qui nous permettra d’en donner des implantations.
Prenons une liste comme par exemple \(\ell_1 = [3, 1, 4]\). C’est une liste à trois éléments (ou de longueur trois) dont le premier est \(3\), le deuxième \(1\), et le dernier \(4\).
Une façon de redécrire cette liste consiste à dire que
la liste \(\ell_1\) possède un premier élément \(3\) qu’on nommera élément de tête,
et que vient après cet élément de tête la liste \(\ell_2 = [1, 4]\) des éléments qui suivent, liste qu’on nommera reste.
Ce qu’on vient de dire de la liste \(\ell_1\) peut être répété pour la liste \(\ell_2\) qui est donc constituée :
d’un élément de tête : \(1\),
et d’un reste : \(\ell_3 = [4]\).
À nouveau on peut répeter le même discours pour la liste \(\ell_3\) qui est donc constituée :
d’un élément de tête : \(4\),
et d’un reste : \(\ell_4 = []\).
La liste \(\ell_4\) étant vide, elle ne possède pas d’élement de tête, et ne peut donc pas être décomposée comme nous venons de le faire à trois reprises.
Si on convient d’utiliser la notation \((x,\ell)\) pour désigner le couple constitué de l’élément \(x\) de tête, et du reste \(\ell\) d’une liste, on peut alors écrire :
On conçoit aisément que ce qui vient d’être fait pour notre exemple de liste \(\ell_1\) peut être reproduit pour n’importe quelle liste.
On peut conclure cette approche en donnant une définition abstraite et formelle des listes d’éléments appartenant tous à un ensemble \(E\).
Définition
Une liste d’éléments d’un ensemble \(E\) est
soit la liste vide
soit un couple \((x,\ell)\) constitué d’un élément \(x\in E\) et d’une liste \(\ell\) d’éléments de \(E\).
Il ressort de cette définition que les listes peuvent être vues comme des structures de données récursives.
Opérations primitives sur les listes (non mutables)¶
En nous appuyant sur la définition formelle qui vient d’être établie, nous sommes en mesure de dégager les opérations primitives qui suivent.
Constructeur¶
D’après la définition, une liste est
soit la liste vide,
soit un couple constitué de l’élément de tête suivi de la liste des éléments qui suivent.
Le constructeur de liste doit donc permettre de produire une liste vide et pour cela aucun argument n’est nécessaire, soit produire une liste à partir de deux arguments.
- class aplst.ApLst(*args)¶
- Paramètres
args (tuple) –
- Build
a new empty list if args is empty, or a list whose head is first element of args, and tail list is second element of args.
- UC
len(args) in {0, 2} and if len(args) == 2, args[1] must be a ApLst
- Raise
ApLstError
if UC is not satisfied
>>> list = ApLst() >>> list.is_empty() True >>> list.head() Traceback (most recent call last): ... ApLstError: head: empty list >>> list2 = ApLst(1, list) >>> list2.is_empty() False >>> list2.head() 1 >>> list2.tail().is_empty() True >>> print(ApLst(2, list2)) [2, 1] >>> print(list2) [1] >>> len(ApLst(1, ApLst(2, ApLst(3, ApLst())))) 3
Sélecteurs¶
Les listes non vides possèdent une tête et un reste. Il nous faut les méthodes, appelées sélecteurs, pour accéder à ces deux composantes.
- ApLst.head()¶
- Renvoie
head element of self
- Raise
ApLstError
if self is empty
- ApLst.tail()¶
- Renvoie
tail list of self
- Raise
ApLstError
if self is empty
Prédicat¶
Un prédicat testant la vacuité d’une liste peut s’avérer nécessaire.
- ApLst.is_empty()¶
- Renvoie
True if self is empty
False otherwise
- Type renvoyé
bool
- UC
none
Remarque¶
Note
Voici quelques relations qu’on peut établir à partir de ces opérations primitives.
pour toute liste
l
et tout élémentx
, on aList(x, l).tail() == l
etList(x, l).head() == x
,et pour toute liste non vide, on a
List(l.head(), l.tail()) == l
.
Exemples¶
Voici quelques exemples d’utilisation de ces opérations.
>>> l = List(3, List())
>>> l.head()
3
>>> l.is_empty()
False
>>> l.tail().is_empty()
True
Opérations primitives spécifiques aux listes mutables¶
Toutes les opérations primitives décrites ci-dessus permettent de travailler avec des listes non mutables, c’est-à-dire des listes dans lesquelles il est impossible
de changer la valeur d’un élément d’une liste,
et d’en changer la structure, en particulier sa longueur.
Les listes non mutables sont fréquentes dans les langages fonctionnels comme Haskell et OCaml.
Mais dans d’autres langages, les listes sont mutables. Par exemple en Python, on peut
changer la valeur d’un élément d’une liste :
l[i] = x
changer l’ordre de tous les éléments :
l.sort()
augmenter la longueur d’une liste :
l.append(x)
diminuer la longueur d’une liste :
l.pop()
…
Si on veut des listes mutables, il nous faut des opérations primitives supplémentaires.
Implantation des listes¶
Pour trouver une implantation des listes, il suffit de s’en tenir à la définition récursive d’une liste : une liste est soit la liste vide, soit un couple constitué de la tête de la liste et du reste de la liste.
Nous allons vous fournir une classe List
permettant de
manipuler les listes. Vous ne connaissez pas encore les classes, mais
vous en avez manipulées souvent (les entiers, les chaînes de
caractères , les listes python, … sont des classes).
Il n’est pas nécessaire de savoir programmer une classes pour effectuer les exercices de TD et de TP, seulement de l’utiliser.
Nous allons quand même analyser certaines parties du code source de la classe.
Il semble assez naturel qu’un objet de classe List
soit représenté par un attribut (privé) dont la valeur est
̀`None`` pour la liste vide,
un couple ou un dictionnaire à deux champs pour une liste non vide.
Constructeurs¶
Nous choisissons un attribut __content
dont la valeur est
()
(tuple vide) pour la liste vide, ou un couple (tuple à deux
éléments) pour les listes non vides.
Prédicat¶
Le prédicat de test de vacuité d’une liste peut s’écrire très simplement même sans savoir comment une liste vide est implantée.
Sélecteurs¶
Les sélecteurs sont simples à écrire, mais il faut tenir compte du cas des listes vides par le biais d’un traitement de l’exception par exemple.
Exemples d’utilisation de ces opérations¶
Calcul de la longueur d’une liste¶
Voici une implantation possible d’une méthode donnant la longueur d’une liste.
>>> List().__len__()
0
>>> List(1, List(2, List(3, List()))).__len__()
3
Note
Remarque
Le calcul de la longueur effectué par la fonction ci-dessus
nécessite un parcours complet de la liste. On pourrait envisager
une implantation des listes avec un attribut contenant la longueur
de la liste qui permettrait à la méthode __len__
d’avoir à
parcourir l’intégralité de la liste.
Le nom de la méthode est encadré par deux blancs soulignés. Cela permet
d’utiliser la fonction len
de python avec nos listes.
Représentation sous forme d’une chaîne de caractères¶
>>> str(List())
'[]'
>>> print(List(1, List(2, List(3, List()))).__len__())
[1, 2, 3]