• Aucun résultat trouvé

Mémoire

L

data

data data

data

NULL

Figure 3.3 – Repr´esentation en m´emoire d’une liste simplement chaˆın´ee.

fois toute les t insertions. La complexit´e pour ins´erer n ´el´ements est donc : K =n+

nt

i=1

t×i≃t×

n

t(nt + 1)

2 +n= Θ(n2).

Donc en moyenne, la complexit´e de l’insertion d’un ´el´ement est Kn = Θ(n). C’est beaucoup trop !

La bonne solution consiste `a doubler la taille du tableau `a chaque r´eallocation (ou la multiplier par n’importe quelle constante plus grande que 2). Ainsi, pour ins´erern´el´ements dans le tableau il faudra avoir recopi´e une fois n2 ´el´ements, le coup d’avant n4, celui d’avant

n

8... Au total la complexit´e est donc : K =n+

log2n i=0

2i 2n = Θ(n).

Ce qui donne en moyenne une complexit´e par insertion de Kn = Θ(1). Il est donc possible de conserver toutes les bonnes propri´et´es des tableaux et d’ajouter une taille variable sans rien perdre sur les complexit´es asymptotiques.

La r´eallocation dynamique de tableau a donc un coˆut assez faible si elle est bien faite, mais elle n´ecessite en revanche d’utiliser plus de m´emoire que les autres structures de donn´ees : un tableau contient toujours de l’espace allou´e mais non utilis´e, et la phase de r´eallocation n´ecessite d’allouer en mˆeme temps le tableau de taille n et celui de taille n2.

3.2 Listes chaˆın´ ees

Contrairement `a un tableau, une liste chaˆın´ee n’est pas constitu´ee d’un seul bloc de m´emoire : chaque ´el´ement (ou nœud) de la liste est allou´e ind´ependamment des autres et contient d’une part des donn´ees et d’autre part un pointeur vers l’´el´ement suivant (cf.

Figure 3.3). Une liste est donc en fait simplement un pointeur vers le premier ´el´ement de la liste et pour acc´eder `a un autre ´el´ement, il suffit ensuite de suivre la chaˆıne de pointeurs.

En g´en´eral le dernier ´el´ement de la liste pointe vers NULL, ce qui signifie aussi qu’une liste vide est simplement un pointeur versNULL. En C, l’utilisation de liste n´ecessite la cr´eation d’une structure correspondant `a un nœud de la liste. Le typelisten lui mˆeme doit ensuite ˆ

etre d´efini comme un pointeur vers un nœud. Cela donne le code suivant :

1 struct cell {

2 /* ins´erer ici toutes les donn´ees que doit contenir un noeud */

3 cell* next;

4 };

5 typedef cell* list;

3.2.1 Op´ erations de base sur une liste

Insertion. L’insertion d’un ´el´ement `a la suite d’un ´el´ement donn´e se fait en deux ´etapes illustr´ees sur le dessin suivant :

data

data Création du nœud

data

data

data

Liste initiale Insertion dans la liste

data

data data

On cr´ee le nœud en lui donnant le bon nœud comme nœud suivant. Il suffit ensuite de faire pointer l’´el´ement apr`es lequel on veut l’ins´erer vers ce nouvel ´el´ement. Le dessin repr´esente une insertion en milieu de liste, mais en g´en´eral, l’ajout d’un ´el´ement `a une liste se fait toujours par le d´ebut : dans ce cas l’op´eration est la mˆeme, mais le premier ´el´ement de liste est un simple pointeur (sans champdata).

Suppression. Pour supprimer un nœud c’est exactement l’op´eration inverse, il suffit de faire attention `a bien sauvegarder un pointeur vers l’´el´ement que l’on va supprimer pour pouvoir lib´erer la m´emoire qu’il utilise.

§ 3.2 Listes chaˆın´ees ENSTA – cours IN101

Liste initiale

data

data

data

Sauvegarde du pointeur

Suppression dans la liste Libération de la mémoire data

data

data tmp

data

data tmp

data

data

data data

tmp

Ici encore, le dessin repr´esente une suppression en milieux de liste, mais le cas le plus courant sera la suppression du premier ´el´ement d’une liste.

Parcours. Pour parcourir une liste on utilise uncurseurqui pointe sur l’´el´ement que l’on est en train de regarder. On initialise le curseur sur le premier ´el´ement et on suit les pointeurs jusqu’`a arriver sur NULL. Il est important de ne jamais perdre le pointeur vers le premier ´el´ement de la liste (sinon les ´el´ements deviennent d´efinitivement inaccessibles) : c’est pour cela que l’on utilise une autre variable comme curseur. Voila le code C d’une fonction qui recherche une valeur (pass´ee en argument) dans une liste d’entiers, et remplace toutes les occurrences de cette valeur par des 0.

1 void cleaner(int n, list L) {

2 list cur = L;

3 while (cur != NULL) {

4 if (cur->data == n) {

5 cur->data = 0;

6 }

7 cur = cur->next;

8 }

9 return;

10 }

Notons qu’ici, la listeL est pass´ee en argument, ce qui cr´ee automatiquement une nouvelle variable locale `a la fonctioncleaner. Il n’´etait donc pas n´ecessaire de cr´eer la variablecur.

Cela ayant de toute fa¸con un coˆut n´egligeable par rapport `a un appel de fonction, il n’est pas gˆenant de prendre l’habitude de toujours avoir une variable d´edi´ee pour le curseur.

Mémoire

L

L_end

data

data data

NULL

NULL

Figure 3.4 – Repr´esentation en m´emoire d’une liste doublement chaˆın´ee.

3.2.2 Les variantes : doublement chaˆın´ ees, circulaires...

La structure de liste est une structure de base tr`es souvent utilis´ee pour construire des structures plus complexes comme les piles ou les files que nous verrons `a la section suivante. Selon les cas, un simple pointeur vers l’´el´ement suivant peut ne pas suffire, on peut chercher `a avoir directement acc`es au dernier ´el´ement... Une multitude de variations existent et seules les plus courantes sont pr´esent´ees ici.

Listes doublement chaˆın´ees. Une liste doublement chaˆın´ee (cf. Figure 3.4) poss`ede en plus des listes simples un pointeur vers l’´el´ement pr´ec´edent dans la liste. Cela s’accom-pagne aussi en g´en´eral d’une deuxi`eme variable L end pointant vers le dernier ´el´ement de la liste et permet ainsi un parcours dans les deux sens et un ajout simple d’´el´ements en fin de liste. Le seul surcoˆut est la pr´esence du pointeur en plus qui ajoute quelques op´erations de plus `a chaque insertion/suppression et qui occupe un peu d’espace m´emoire.

Listes circulaires. Les listes circulaires sont des listes chaˆın´ees (simplement ou dou-blement) dont le dernier ´el´ement ne pointe pas vers NULL, mais vers le premier ´el´ement (cf. Figure 3.5). Il n’y a donc plus de r´eelle notion de d´ebut et fin de liste, il y a juste une position courante indiqu´ee par un curseur. Le probl`eme est qu’une telle liste ne peux jamais ˆetre vide : afin de pouvoir g´erer ce cas particulier il est n´ecessaire d’utiliser ce que l’on appelle une sentinelle.

Sentinelles. Dans une liste, une sentinelle est un nœud particulier qui doit pouvoir ˆetre reconnaissable en fonction du contenu de son champ data (une m´ethode classique est d’ajouter un entier au champ data qui est non-nul uniquement pour la sentinelle) et qui sert juste `a simplifier la programmation de certaines listes mais ne repr´esente pas un r´eel

´

el´ement de la liste. Une telle sentinelle peut avoir plusieurs usages :