• Aucun résultat trouvé

7.4 Les structures de données usuelles

7.4.2 Listes et itérateurs

Une liste est une suite d’éléments qui ne sont pas nécessairement contigus en mémoire mais dans laquelle chaque élément contient les adresses du précédent et du suivant. Il suffit d’alors de connaître l’adresse du premier pour pouvoir accéder à tous les autres « en suivant les liens ».

C’est une alternative à l’usage de std::vector() : beaucoup de fonctions sont iden- tiques mais ont une complexité différente, il y a également certaines fonctions supplémen-

taires et d’autres sont manquantes. Le choix réside essentiellement sur une réflexion sur la complexité des algorithmes que l’on souhaite implémenter.

Nous présentons ici le cas des listes doublement chaînées sans l’utilisation de classes, introduites au chapitre suivant, afin de bien comprendre l’usage des pointeurs. Bien évidemment, l’usage de classes permet d’avoir une syntaxe plus légère : nous vous conseillons chez vous, après avoir lu et compris le chapitre sur les classes de transformer cette section en classe, de migrer les fonctions vers des méthodes, en précisant bien les const nécessaires.

Chaque élément d’une liste d’objets de type TYPE est appelé maillon et peut être décrit par :

typedef struct maillon{

2 TYPE valeur;

struct maillon * precedent;

4 struct maillon * suivant; } Maillon;

Le champ precedent (resp. suivant ) contient l’adresse du maillon précédent (resp. suivant) dans la liste. Pour le premier (resp. dernier) maillon, la valeur de precedent (resp. suivant ) est fixée par convention à NULL. Une liste est alors une structure :

typedef struct {

2 unsigned int taille; Maillon * premier; 4 Maillon * dernier;

} Liste;

qui indique les premiers et derniers maillons, et seulement ces deux.

Imaginons qu’on veuille ajouter une valeur x de type TYPE dans une liste L dans un nouveau maillon juste avant un maillon d’adresse a . La fonction ajout peut par exemple s’écrire :

bool ajout(Liste & L, Maillon * a, TYPE x) { 2 struct Maillon * p=new Maillon;

if( p== NULL) return false; //echec de la creation: rien ne change.

4 p->valeur=x;

L.taille++;//ne pas oublier de tout mettre à jour !

6 // Cas où le maillon est le premier de la liste

if( L.premier == a ) 8 { p->precedent=NULL; 10 p->suivant= L.premier; L.premier = p; 12 a->precedent=p; return true; 14 }

// Cas où le maillon est le dernier de la liste

16 if( L.dernier == a) {

7.4. LES STRUCTURES DE DONNÉES USUELLES 133 18 a->suivant=p; p->precedent=a; 20 p->suivant=NULL; L.dernier=p; 22 return true; } 24 // Autres cas p->suivant=a; 26 p->precedent=a->precedent; a->precedent->suivant=p; 28 a->precedent=p; return true; 30 }

Ce code montre ainsi que l’ajout d’un élément ne nécessite de ne bouger aucun autre élément et que le nombre d’opérations ne dépend pas de la taille de la liste. La complexité d’un ajout est donc en 𝑂(1) en la longueur de la liste.

Supposons à présent que nous souhaitions accéder au 𝑖-ème terme de la liste. La répartition des objets dans la mémoire étant quelconque, il est hors de question de calculer l’adresse du 𝑖-ème terme comme dans les tableaux. Il reste donc à parcourir toute la liste pour la trouver. La fonction d’accès s’écrit donc :

Maillon * acces(const struct Liste & L, unsigned int i) {

2 if( i >= L.taille) return NULL; // element inexistant

Maillon * p=L.premier; 4 while( i>0) { p=p->suivant; 6 i--; } 8 return p; }

Cette fois-ci, la boucle while parcourt la liste jusqu’au bon endroit. La complexité moyenne est donc en 𝑂(𝑁 ).

Nous avons ainsi atteint notre but en construisant une structure qui permet l’accès en 𝑂(𝑁 ) et l’insertion en 𝑂(1). On vérifie également qu’il est facile d’ajouter et supprimer des éléments en début ou fin de liste puisque les adresses du premier et du dernier élément sont écrites dans la structure de liste.

Le concept d’itérateur. Il existe des opérations qui prennent autant de temps dans un tableau que dans une liste et pour lesquelles les algorithmes se retrouvent être les mêmes, par exemple, calculer la somme des éléments d’une liste ou d’un tableau d’objets de type double. Écrivons les trois codes aussi efficaces les uns que les autres pour les trois structures étudiées jusque là :

double somme( const Liste & L) {

2 double s=0.;

4 while( p != NULL) { s += (*p).valeur; 6 p = p-> suivant; } 8 return s; }

double somme(const double * t, unsigned int taille) { 2 double s=0.; unsigned k=0; 4 while( k < taille) { s += t[k]; 6 k++; } 8 return s; }

double somme(const double * t, unsigned int taille) { 2 double s=0.; double * p= t; 4 while( p != t+taille) { s += *p ; 6 p++; } 8 return s; }

Le premier code est le seul moyen possible de parcourir une liste : on entre par le premier terme et on s’arrête quand on sort du dernier. Le deuxième, pour les tableaux, utilise la possibilité de trouver un élément immédiatement mais le tableau est parcouru dans le même ordre que pour une liste. L’algorithme peut être réécrit et donne alors la troisième version qui est très similaire à celle de la liste mutatis mutandis et utilise les mêmes ingrédients :

— ligne 3 : initialisation sur le premier élément — ligne 4 : détection de la sortie de la structure — ligne 5 : accès à la valeur

— ligne 6 passage à l’élément suivant

Cette similarité n’est pas un hasard et est à la base de la notion d’itérateur.

Un itérateur joue le rôle d’un pointeur vers un élément d’une structure et doit être pourvu des opérations suivantes :

— accès à la valeur par l’opérateur * — passage à l’élément suivant par ++ ;

— récupération à partir d’une liste d’un itérateur vers le premier élément et d’une condition de sortie de la structure.

Le modèle de classe std::list de la STL. La STL contient déjà des listes double- ment chaînées avec des fonctionnalités bien plus nombreuses que notre tentative précédente

7.4. LES STRUCTURES DE DONNÉES USUELLES 135

et nous vous encourageons à utiliser directement cette nouvelle solution. En utilisant l’en-tête

#include <list>

on a accès au modèle de classe std::list<TYPE> pour gérer une liste d’objets de type TYPE et d’itérateurs std::list<TYPE>::iterator vers des éléments de liste. Les opéra- tions naturelles sur ces objets sont résumées dans le tableau de la figure7.4 pour des objets déclaré par std::list<TYPE> L; , std::list<TYPE>::iterator i; et TYPE x; . Les fonctionnalités sont similaires aux fonctions précédentes.

Ainsi, si nous souhaitons construire une liste avec les carrés des 100 premiers nombres entiers puis afficher cette liste dans l’ordre inverse, nous pouvons écrire :

#include <list>

2 #include <iostream>

using namespace std;

4 int main() {

list<int> L;

6 for(int i=0 ; i<100; i++) L.push_front(i*i);

list<int>::iterator it;

8 for( it=L.begin(); it != L.end() ; it++) cout << *it << endl;

return 0;

10 }

7.4.3 Listes simplement chaînées, piles, arbres, « skip lists », arbres

Documents relatifs