Vous êtes sur la page 1sur 46

Chapitre 3 Les structures de base : listes, piles et files

1. Introduction Le but de ce chapitre est de dcrire des reprsentations des structures de base utilises en informatique telles les listes en gnral et deux formes restreintes: les piles et les files. Lautre but recherch est premirement de donner des exemples de sparations des reprsentations logiques travers les TDA des implmentations physiques, et, deuximent, dillustrer lutilisation de da dtermination de la complexit dun algorithme. 2. Les listes Les listes sont des structures informatiques qui permettent, au mme titre que les tableaux par exemple, de garder en mmoire des donnes en respectant un certain ordre: on peut ajouter, enlever ou consulter un lment en dbut ou en fin de liste, vider une liste ou savoir si elle contient un ou plusieurs lments.

La structure de liste particulirement souple et dont les lments sont accessibles tout moment. Toute suite dinformations inscrites les unes aprs les autres est une liste. Exemple : liste dtudiants, liste dinventaire, liste des absents, Une liste est dfinie comme tant une suite ordonne dlments. Lordre signifie que chaque lment possde une position dans la liste. Limplmentation doit supporter le concept de position courante. Cela est fait en dfinissant la liste en termes de partition de droite et de gauche. Chacune de ces partitions peut tre vide. Les partitions sont spares par un sparateur (courant ou fence ). Llment courant est le premier lment de la partition de droite <20, 23 | 12, 15>

2. 1 Les oprations sur les listes : Les oprations sur les listes sont trs nombreuses, parmi elles, on peut citer les plus frquentes: - Crer une liste vide - Tester si une liste est vide - Ajouter un lment la liste - ajouter en dbut de liste (tte de liste) - ajouter la fin de la liste (fin) - ajouter un lment une position donne - ajouter un lment aprs une position donne - ajouter un lment avant une position donne - Afficher ou imprimer les lments d'une liste - Ajouter un lment dans une liste trie (par ordre ascendant ou descendant) - Supprimer un lment dune liste - supprimer en dbut de liste - supprimer en fin de liste - supprimer un lment une position donne - supprimer un lment avant ou aprs une position donne. 2.2 Implantation par tableau La liste peut tre implante l'aide d'un tableau. Dans ce cas, il faut prvoir une taille maximum pour le tableau. L'opration d'impression des lments d'une liste et de recherche d'un lment se font en temps linaire. La recherche du k-me lment se fait O(1) i.e. en un temps constant. Cependant les insertions et les suppressions d'lments sont plus coteuses. Par exemple, l'insertion la position 0 ncessite le dplacement de tous les lments de la liste pour librer la position 0.

Par consquent la cration d'une liste l'aide de n insertions successives partir de la position 0 ncessite un temps en O(n 2 ) (pourquoi?). Ce temps est dit quadratique

// fichier LISTE.H // Dclaration de la classe Liste #ifndef LISTE_H #define LISTE_H #define Taille_de_Liste 100 template <classe T> ; class Liste { public : Liste ( const int = Taille_de_Liste ) ; // Constructeur ~Liste () ; // Destructeur void ViderListe () ; // Vide la liste void Inserer ( const TElement & ) ; // insre un lment la position courante void InsererFin ( const TElement & ) ;// insre un lment la fin de la liste TElement Supprimer () ; // Supprime et retourne l'lment la position courante void FixerTete () ; // met la position courante la tte de la liste void Precedent () ; // Dplace la position courante la position prcdente void Suivant () ; // Dplace la position courante la position suivante int Longueur () const ; // retourne la longueur courante de la liste void FixerPosition ( const int ) ; // met position courante position donne void FixerValeur ( const TElement & ) ; // met jour la valeur la position courante TElement ValeurCourante () const ;//retourne la valeur d'lment la position courante. bool ListeVide () const ; // retourne vrai si la liste est vide bool EstDansListe () const ; //retourne vrai si position courante est dans la liste bool Trouver ( const TElement & ) ;// recherche une valeur dans la // liste partir de la position courante. private : T TailleMax ; // taille maximum de la liste T NbElement ; // nombre d'lments effectifs dans la liste T Courant ; // position de l'lment courant T * TabElement ; // tableau contenant les lments de la liste }; #endif Note : remarquez que le mot cl const est utilis la fin des dclarations de fonction membres en mode lecture seule, savoir EstDansListe (), ValeurCourante () et Longueur (). Afin de mieux protger un programme, il y a tout intrt dclarer comme constantes les fonctions qui ne sont pas censes modifier ltat des objets auxquels elles sont lies. On bnficiera ainsi dune aide supplmentaire du compilateur qui empchera tout ajout de code modifiant ltat de lobjet dans la fonction.

// Fichier LISTE.CPP #include <cassert> #include "LISTE.H" Liste::Liste ( const int taille ) // Constructeur { TailleMax = taille ; NbElement = Courant = 0 ; TabElement = new TElement [taille] ; } Liste::~Liste () { delete [] TabElement ; } // Destructeur

void Liste::ViderListe () { NbElement = Courant = 0 ; // noter que le tableau nest pas dtruit } // Insre un lment la position courante void Liste::Inserer (const TElement & element ) { // le tableau ne doit pas tre plein et Courant doit tre une position lgale assert (( NbElement < TailleMax) && (Courant >= 0) && (Courant <= NbElement)) ; for ( int i = NbElement; i > Courant; i-- ) TabElement[i] = TabElement[i-1] ; // Dcale les lments vers le haut TabElement[Courant] = element ; NbElement++ ; } void Liste::InsererFin ( const TElement & element ) { assert ( NbElement < TailleMax ) ; // la liste ne doit pas tre pleine TabElement[NbElement++] = element ; } TElement Liste::Supprimer () { assert ( !ListeVide() && EstDansListe() ) ; // s'assurer qu'il existe // un lment supprimer TElement temp = TabElement[Courant] ; for ( int i = Courant; i < NbElement-1; i++ ) // dcale les lments // vers le bas
4

TabElement[i] = TabElement[i+1] ; NbElement-- ; return temp ; } void Liste::FixerTete () { Courant = 0 ; } // rend la tte comme position courante

void Liste::Precedent () // met la position courante la position prcdente { Courant-- ; } void Liste::Suivant () { Courant++ ; } int Liste::Longueur () const { return NbElement ; } void Liste::FixerPosition (const int pos ) { Courant = pos ; } void Liste::FixerValeur ( const TElement & valeur ) { assert ( EstDansListe() ) ; // Courant doit tre une position lgale TabElement[Courant] = valeur ; } TElement Liste::ValeurCourante () const { assert ( EstDansListe() ) ; return TabElement[Courant] ; } bool Liste::EstDansListe () const { return ( Courant >= 0 ) && ( Courant < NbElement ) ; }

bool Liste::ListeVide () const { return NbElement == 0 ; } bool Liste::Trouver ( const TElement & valeur ) { while ( EstDansListe() ) if ( ValeurCourante() == valeur ) return true ; else Suivant () ; return false ; } // recherche la valeur partir // de la position courante

2.3 Implantation par liste chane La deuxime approche traditionnelle pour implanter une liste est travers lutilisation des pointeurs. Cette approche fait en sorte que la mmoire est alloue dune manire dynamique dans le sens que les cellules mmoire sont alloues au besoin. Les lments dune liste seront tout simplement chans entre eux. Il existe plusieurs types de listes chanes dpendant de la manire dont on se dplace dans la liste : 1. les listes chanes simples, 2. les listes doublement chanes, 3. les listes circulaires. 2.3.1 Liste chane simple: Cest la liste de base dont chaque lment appel nud contient deux parties : 1. une partie contenant linformation proprement dite 2. et une partie appele pointeur qui lie le nud au nud suivant. Une liste chane simple est reprsente graphiquement comme suit :

info

info

info

tte de liste La liste tant une structure dynamique, son nombre de nuds varie en cours de programme. Il faut donc supposer un certain nombre doprations primitives telles que: - crer (p) : qui permet dobtenir une mmoire nud pointe par p. - librer (p) : qui permet de librer un nud point par p
6

nud (p) : permet daccder au nud point par p info (p) : permet daccder la partie information dun nud point par p. suivant (p) est le pointeur (adresse) du prochain nud

Pseudo-code de quelques oprations a. Crer une liste vide initialiser (Liste) /* Liste est un pointeur*/ Liste nul

nul
Liste b. Crer un nud Creernoeud () : le rsultat est un pointeur vers le nud cr c. Ajout dun lment en tte de liste Exemple : On suppose que lon veut rajouter llment 6 au dbut de la liste suivante :

teteListe a) on commence par crer le nouveau nud et lui donner son contenu p 6

b) le suivant de p est lancienne tte de liste 6 4 3 5 8 2

teteListe

c) Le pointeur p devient tte de liste 6 4 3 5 8 2

teteListe Les oprations a, b et c se traduisent par le pseudo-code suivant : p creernoeud () information (p) x suivant (p) teteListe teteListe p

o x est linformation stocker dans le nud ajout.

Ajout dun lment aprs un nud point par p p

q inserer (p,x) insre llment x aprs le nud point par p. q creernoeud () info (q) x suivant (q) suivant (p) suivant (p) = q; Noter que le nombre doprations ncessaires pour raliser cet algorithme est indpendant du nombre dlments de la liste. Ajout dun lment en fin de liste SI teteListe = nul ALORS ajout en tte de liste SINON // chercher ladresse du nud de fin de liste q teteListe TANT QUE ( suivant (q) nul ) q suivant (q) ajouter llment aprs llment point par q

Suppression dun nud Pour supprimer un nud, il nest pas suffisant de donner un pointeur vers ce nud. Il faut retrouver le prdcesseur de p pour tablir le lien entre le prdcesseur de p et le successeur de p.

Suppression dun lment aprs llment point par p supprimer (p,x) : supprime llment x aprs p et renvoie la valeur de linformation du nud supprim dans x. Les oprations effectuer sont comme suit : q suivant (p) x information (q) suivant (p) suivant (q) librer (q)

2.3.2. Implantation de la liste chane simple en C++ Le nud tant un objet distinct, il est intressant de dfinir une classe pour les nuds. // fichier NOEUD.H // Dclaration de la classe Noeud #ifndef NOEUD_H #define NOEUD_H template < classe TElement > class noeud { public : TElement element ; noeud * Suivant ; noeud ( const TElement & info, noeud * suiv = NULL ) // constructeur1 { element = info ; Suivant = suiv ; }
9

noeud ( noeud * suiv = NULL ) // constructeur 2 { Suivant = suiv ; } ~noeud () { } }; #endif La classe nud contient deux constructeurs, un qui prend une valeur initiale pour la partie information et lautre non. Elle contient galement un destructeur qui ne fait rien de spcial. Dans cette implantation, nous utiliserons trois pointeurs : Tete, fin et Courant qui pointent respectivement vers le premier lment, dernier lment et llment courant de la liste. Par exemple : 20 Tete 23 Courant 12 fin 15

Si on veut insrer une nouvelle valeur la position courante, le nud qui contient la valeur 10, on obtient la liste suivante : 20 23 10 12 15

Tete

Courant

fin

Le nud 23 doit pointer vers le nouveau nud. Une solution est de parcourir la liste depuis le dbut jusqu rencontrer le nud qui prcde le nud point par Courant. Une autre alternative est de faire pointer Courant vers llment qui prcde llment courant. Dans notre exemple si 10 est llment courant, on ferait pointer Courant sur 23. Un problme surgit avec cette convention. Si la liste contient un seul lment, il ny a pas de prcdent pour Courant. Les cas spciaux peuvent tre limins si on utilise un nud entte, comme le premier lment de la liste. Ce nud est un nud bidon, sa partie information est compltement ignore, et il pointe vers le premier lment de la liste. Ce nud nous vitera de considrer les cas o la liste na quun seul lment, les cas o est elle vide et le cas o le pointeur courant est sur le dbut de la liste.

10

Dans ce cas, initialement on a : Courant fin

Tete Si on reprend notre exemple on aurait: 20 23 12 15

Tete

Courant

fin

Dans ce cas, linsertion du 10 se fera normalement entre le 20 et le 23. Cette opration qui se fait en trois tapes : 1. crer un nouveau nud, 2. mettre son suivant vers llment courant actuel, 3. faire pointer le champ suivant du nud prcdent le nud courant vers le nouveau nud. Il est facile de voir lopration dinsertion se fait en un temps constant (1). Lopration de suppression dun lment se fait aussi en (1) puisquelle consiste essentiellement en des mises jour de pointeurs. La classe Liste avec cette implantation est : // fichier LISTE.H // Dclaration de la classe Liste #ifndef LISTE_H #define LISTE_H #include "NOEUD.H"

11

class Liste{ public : Liste () ; // Constructeur ~Liste () ; // Destructeur void ViderListe () ; // Vide la liste void Inserer ( const TElement & ) ; // insre un lment la position courante void InsererFin ( const TElement & ) ; // insre un lment la fin de la liste TElement Supprimer () ; // Supprime et retourne l'lment la position courante void FixerTete () ; // met la position courante la tte de la liste void Precedent () ; // Dplace la position courante la position prcdente void Suivant () ; // Dplace la position courante la position suivante int Longueur () const ; // retourne la longueur courante de la liste void FixerPosition ( const int ) ; // met position courante position donne void FixerValeur ( const TElement & ) ; // met jour la valeur la position courante TElement ValeurCourante () const ;//retourne la valeur d'lment la position courante. bool ListeVide () ; // retourne vrai si la liste est vide bool EstDansListe () const ; //retourne vrai si position courante est dans la liste bool Trouver ( const TElement & ) ;// recherche une valeurs dans la // liste partir de la position courante. private : noeud * Tete ; // position du premier lment noeud * fin ; // position du dernier lment noeud * Courant ; // position de l'lment courant }; #endif // Fichier LISTE.CPP #include <cassert> #include "LISTE.H" Liste::Liste () // Constructeur { fin = Tete = Courant = new noeud ; // cre le noeud entte } Liste::~Liste () { while ( Tete != NULL ) { Courant = Tete ; Tete = Tete->Suivant ; delete Courant ; } } // Destructeur

12

void Liste::ViderListe () { // libre l'espace allou aux noeuds, garde l'entte. while ( Tete->Suivant != NULL ) { Courant = Tete->Suivant ; Tete->Suivant = Courant->Suivant ; delete Courant ; } Courant = fin = Tete ; // rinitialise } // Insre un lment la position courante void Liste::Inserer ( const TElement & element ) { assert ( Courant != NULL ) ;// s'assure que Courant pointe vers un noeud Courant->Suivant = new noeud ( element, Courant->Suivant ) ; if ( fin == Courant ) fin = Courant->Suivant ; //l'lment est ajout la fin de la liste. } TElement Liste::Supprimer () // supprime et retourne l'lment courant { assert ( EstDansListe() ) ; // Courant doit tre une position valide TElement temp = Courant->Suivant->element ; //Sauvegarde de l'lment courant noeud * ptemp = Courant->Suivant ; // Sauvegarde du pointeur du noeud Courant Courant->Suivant = ptemp->Suivant ; // suppression de l'lment if ( fin == ptemp ) fin = Courant ; // C'est le dernier lment supprim, mise jour de Fin delete ptemp ; return temp ; } void Liste::InsererFin ( const TElement & element ) // insre en fin de liste { fin = fin ->Suivant = new noeud (element, NULL) ; } void Liste::FixerTete () // rend la tte comme position courante { Courant = Tete ; } void Liste::Precedent () // met la position courante la position prcdente { noeud * temp = Tete ;
13

if (( Courant == NULL ) || ( Courant == Tete) ) // pas d'lment prcdent { Courant = NULL ; return ; } while ( (temp != NULL) && (temp->Suivant != Courant) ) temp = temp->Suivant ; Courant = temp ; } void Liste::Suivant () { if ( Courant != NULL ) Courant = Courant->Suivant ; } int Liste::Longueur () const { int cpt = 0 ; for ( noeud * temp = Tete->Suivant; temp != NULL; temp = temp->Suivant ) cpt++ ; // compte le nombre d'lments return cpt ; } void Liste::FixerPosition ( const int pos ) { Courant = Tete ; for ( int i = 0 ; ( Courant != NULL ) && ( i < pos ); i++ ) Courant = Courant->Suivant ; } void Liste::FixerValeur ( const TElement & valeur ) { assert ( EstDansListe() ) ; Courant->Suivant->element = valeur ; return ; } TElement Liste::ValeurCourante () const { assert ( EstDansListe() ) ; return Courant->Suivant->element ; } bool Liste::EstDansListe () const { return ( Courant != NULL ) && ( Courant->Suivant != NULL ) ; }
14

bool Liste::ListeVide () { return Tete->Suivant == NULL ; } bool Liste::Trouver ( const TElement & valeur ) { while ( EstDansListe() ) if ( Courant->Suivant->element == valeur ) return true ; else Courant = Courant->Suivant ; return false ; } // recherche la valeur partir // de la position courante

3.4 Comparaison des implantations de la liste Limplantation de la liste par tableau impose le choix dune taille maximum de la liste. Beaucoup despace risque dtre inutilis. Dans le cas de limplantation par liste chane, lespace est allou uniquement aux lments qui appartiennent effectivement la liste. Dans le cas de limplantation par tableau aucune mmoire supplmentaire nest ncessaire pour stocker un lment de la liste. Dans le cas de liste chane, un espace pour le pointeur est ajout pour chaque lment de la liste. Laccs un lment par sa position est plus rapide dans le cas de limplantation par tableau, il se fait en un temps constant (O(1)). Dans le cas de la liste chane, nous devons parcourir la liste du dbut jusqu la position dsire (O(n)). Les insertions et les suppressions dlments sont plus coteuses dans le cas dans l implantation par tableau car elles ncessitent des dplacements dlments. Listes avec tableau: 1. 2. 3. 4. Insertion et suppression sont (n). Prcdent et accs direct sont (1). Tout lespace est allou lavance. Pas despace autre que les valeurs.

Listes chanes: 1. 2. 3. 4. Insertion et suppression sont (1). Prcdent et accs direct sont (n). Lespace augmente avec la liste. Chaque lment requiert de lespace pour les pointeurs

15

Les listes doublement chanes : Dans les listes simplement chanes, partir d'un nud donn, on ne peut accder qu'au successeur de ce nud. Dans une liste doublement chane, partir d'un nud donn, on peut accder au nud successeur et au nud prdcesseur. Exemple: 30 35 34

Tte

Courant

fin

Cela a pour consquence quun nud de cette liste va avoir trois champs : 1. un ou des champs information, 2. un pointeur vers le successeur, 3. un pointeur vers le prdcesseur. Dans le cas de la liste doublement chane, on pourrait avoir le pointeur Courant, directement sur llment courant car il est possible daccder au prdcesseur. Cependant, nous allons garder la mme convention que dans le cas de la liste simplement chane (Courant pointe une position avant llment courant) pour: - viter un cas spcial quand on fait une insertion dans une liste vide - permettre linsertion dun nud nimporte quelle position de la liste, y compris en fin de liste. Pour implanter la liste doublement chane, il faut redfinir le nud comme suit : // fichier NOEUD.H // Dclaration de la classe Noeud #ifndef NOEUD_H #define NOEUD_H template <classe Telement> class noeud { public : TElement element ; noeud * Suivant ; noeud * Precedent ; noeud ( const TElement & info, noeud * suiv = NULL, noeud * prec = NULL ) { // constructeur1 element = info ; Suivant = suiv ; Precedent = prec ;
16

} noeud ( noeud * suiv = NULL, noeud * prec = NULL ) // constructeur 2 { Suivant = suiv ; Precedent = prec ; } ~noeud () { } }; #endif La liste doublement chane peut tre implante de la mme faon que la liste simplement chane. La partie prive reste inchange. Cependant, les fonctions dinsertion la position courante, linsertion en fin, la suppression sont dfinir ainsi quune fonction qui nous permet davoir le prdcesseur. Les autres fonctions sur les listes chanes simples peuvent tre utilises sans changement dans le cadre des listes doublement chanes. linsertion la position courante : 20 23 12

Courant

10

lment insrer

20 10 Courant

23

12

Cette insertion est ralise par les instructions suivantes : Courant->suivant = new nud ( 10, Courant->suivant, Courant ) ; if ( Courant->suivant->suivant != NULL ) Courant->suivant->suivant->precedent = Courant->suivant ;

17

Le test permet de sassurer que linsertion nest pas la fin de la liste, auquel cas il faut mettre jour le champ precedent du nud aprs le nouveau nud. Ajout la fin de la liste :

fin->suivant = new nud ( elem, NULL, fin ) ; fin = fin->suivant ; Suppression de llment la position courante : 20 Couran Ltemp 23 12

if ( ltemp->suivant != NULL ) ltemp->suivant->precedent = Courant ; else fin = Courant ; // Cest le dernier lment qui est supprim. Courant->suivant = ltemp->suivant; // ajout dun lment la position courante void Liste::Inserer ( const TElement & element ) { assert ( Courant != NULL ) ; Courant->suivant = new nud ( elem, Courant->suivant, Courant ) ; if ( Courant->suivant->suivant != NULL ) Courant->suivant->suivant->precedent = Courant->suivant ; if ( fin == Courant ) fin = Courant->suivant ; } // ajout dun lment la fin de la liste void Liste::InsererFin ( const TElement & elem ) { fin->suivant = new nud ( elem, NULL, Fin ) ; fin = fin->suivant ; }

18

// Supprime et retourne llment la position courante TElement Liste::Supprimer ( ) { assert ( EstDansListe () ) ; int temp = Courant->suivant->element ; nud * ltemp = Courant->suivant ; if ( ltemp->suivant != NULL ) ltemp->suivant->precedent = Courant ; else fin = Courant // Cest le dernier lment qui est supprim. Courant->suivant = ltemp->suivant ; delete ltemp ; return temp; } // Dplace le pointeur Courant la position prcdente void Liste::Precedent() { if ( Courant != NULL ) Courant = Courant->precedent ; }

Les listes circulaires Une liste o le pointeur NULL du dernier lment est remplac par ladresse du premier lment est appele liste circulaire. Dans une liste circulaire tous les nuds sont accessibles partir de nimporte quel autre nud. Une liste circulaire na pas de premier et de dernier nud. Par convention, on peut prendre le pointeur externe de la liste vers le dernier lment et le suivant serait le premier lment de la liste. Par ailleurs, un pointeur nul signifie une liste vide. Une liste ciculaire peut tre simplement chane ou doublement chane. Notez que la concatnation de deux listes circulaires peut se faire sans avoir parcourir les deux listes.

19

Les Piles

3-1 Dfinition et exemple La pile est une structure trs utilise en informatique, surtout dans les langages de programmation (appel de procdures, calcul des expressions arithmtiques,). La pile est une liste ordonne sans fin dlments dans laquelle on ne peut introduire ou enlever un lment qu' une extrmit appele tte de pile ou sommet de pile. Exemple une pile de livres, une pile dassiettes, une pile un jeu de cartes si lon se refuse le droit de manipuler plus dun de leurs lments la fois. Noter que dans une pile, le dernier lment insr sera le premier tre supprim, retir : on parle de liste LIFO Last In First Out.

3-2 Oprations sur les piles : Les oprations de base sur les piles sont trs simples. Empiler (p,e) (en anglais push) : empile llment e dans la pile p Dpiler (p) (en anglais pop): dpile un lment de la pile et retourne la valeur de cet lment (e = Depiler (p))

Toutes les oprations sont effectues sur la mme extrmit; on parle de structure en FIFO.

Il ny a aucune limite au nombre dlments quon peut empiler. Par contre, on ne peut dpiler dune pile vide. A cet effet, on a une fonction PileVide(p) qui retourne Vrai si la pile est vide sinon Faux. InitialiserPile(p) ou ViderPile(p) : vide la pile p On peut vouloir aussi connatre le sommet de pile sans le supprimer (dpiler). e= SommetDePile(p) : Ce nest pas rellement une nouvelle opration car on peut lexprimer comme suit : e = Dpiler (p)

20

Empiler (e,p) SommetDePile() et Dpiler() ne fonctionnent pas sur une pile vide. Aussi, on ne peut empiler dans une pile pleine. Implantation par tableau Il faut tre bien conscient quun tableau est diffrent dune pile. Un tableau a une taille fixe alors que la pile est une structure dynamique: sa taille change chaque fois que de nouveaux lments sont empils ou dpils. Cependant, on peut utiliser un tableau pour accueillir une pile en respectant les conditions suivantes : Dimensionner le tableau au nombre dlments que peut contenir la pile. Restreindre les lments de la pile un type identique. Utiliser une variable spciale, sommet, contenant tout instant lindice de la case du tableau o se trouve llment du sommet de pile.

Une pile en C++ est une classe dfinie comme suit. // Fichier Pile.h // declaration de la classe pile #ifndef PILE_H #define PILE_H La pile gnrique (template) : #include <cassert> const Taille_de_Pile = 10; template <class T> class Pile { private : int Taille; int Sommet; T * TabElement;

// Taille Maximum de la pile // indice de la premire position libre dans le tableau // Tableau contenant les lments de la pile

public : Pile (const int dim = Taille_de_Pile ) ; // Constructeur ~Pile () ; // destructeur : libre l'espace allou la pile void ViderPile (); // enlve tous les lments de la pile void Empiler ( const T & );// ajoute un lment au dessus de la pile T Depiler (); // retire un lment du sommet de la pile T Sommet_De_Pile () const; // Renvoie la valeur du sommet de la pile bool PileVide () const; // Renvoie vrai (true) si la pile est vide };
21

template <class T> Pile<T>::Pile ( const int dim ) // Constructeur { Taille = dim; Sommet = 0; TabElement = new T[dim]; } template <class T> Pile<T>::~Pile () // destructeur : libre l'espace allou la pile { delete TabElement; } template <class T> void Pile<T>::ViderPile () { Sommet = 0; } template <class T> void Pile<T>::Empiler ( const T & elem ) { assert ( Sommet < Taille ); TabElement[Sommet++] = elem; } template <class T> T Pile<T>::Depiler () { assert( !PileVide() ); return TabElement[--Sommet]; } template <class T> T Pile<T>::Sommet_De_Pile () const { assert ( !PileVide() ); return TabElement[Sommet-1]; } template <class T> bool Pile<T>::PileVide () const { return Sommet == 0;
22

} La dclaration Pile <int> p1(6) ; cre une instance de pile dentiers de 6 lments. La dclaration Pile <char> p1(6) ; cre une instance de pile de caractres de 6 lments. Noter lutilisation de assert ( !PileVide ); Cette instruction permet de vrifier que la condition indique est vraie. Ici, si la condition est vraie cest--dire PileVide() est faux, le programme se poursuit sinon, il sarrte. Une autre possibilit est de prvoir dans la fonction Depiler() et Sommet_De_Pile(), une variable boolenne qui indique si la fonction a russi ou chou. Cette variable sera teste chaque retour de lappel de la fonction. Pour utiliser la fonction assert() dans un programme, il faut inclure le fichier <cassert>. Il en est de mme pour la fonction Empiler(), il serait prfrable de prvoir une variable qui indiquera au programme appelant si la pile est pleine ou pas, ceci permettra au programme appelant deffectuer les actions ncessaires dans un pareil cas. Il faut cependant noter que la contrainte de pile pleine nest pas une contrainte lie la structure de donne pile. Thoriquement, il ny a pas de limite sur le nombre dlments dune pile. En pratique, son implantation dans un tableau limite le nombre dlments Taille.

Exemple dutilisation de la classe Pile: On considre un programme qui lit 5 entiers, les empile dans une pile et les affiche.

#include <iostream> using namespace std ; #include "Pile.h" int main () { int n ; Pile pile1(5) ;

// 5 est la taille de la pile

cout << "Entrez 5 entiers : " ; for ( int i = 1; i <= 5; i++ ) { cin >> n ; pile1.Empiler(n) ; // remplissage de la pile } while ( !pile1.PileVide() ) // affichage des lments de la pile cout << pile1.Depiler() << " " << endl ; return 0 ;
23

Implantation par liste chane Limplantation dune pile par une liste (simplement) chane est une version simplifie de celle dune liste par liste chane. template <class Elem> class Pile { private: noeud * top; // sommet de la pile int size; // nombre dlment dans la pile public: Pile () // constructeur { top = NULL; size = 0; } ~Pile() { clear(); } // Destructeur void clear() { while (top != NULL) { // Delete link nodes noeud * temp = top; top = top->suivant; delete temp; } size = 0; } bool push(const Elem& item) { top = new noeud(item,top); size++; return true; } bool pop(Elem& it) { if (size == 0) return false; it = top->element; noeud * ltemp = top->suivant; delete top; top = ltemp; size--; return true; } bool sommet_pile(Elem& it) const { if (size == 0) return false; it = top->element; return true; }
24

int length() const { return size; } // retourne la taille de la pile }; Comparaison des deux implantations Toutes les implantations des oprations dune pile utilisant un tableau ou une liste chane prennent un temps constant i.e. en O(1). Par consquent, dun point de vue defficacit, aucune des deux implantations nest mieux que lautre. Dun point de vue de complexit spatiale, le tableau doit dclarer une taille fixe initialement. Une partie de cet espace est perdue quand la pile nest pas bien remplie. La liste peut augmenter ou rapetisser au besoin mais demande un extra espace pour mmoriser les adresses des pointeurs.

Quelques applications des piles

1. Reconnaissance syntaxique : Soit une chane de caractres dfinie par la rgle suivante : Une chane quelconque S suivie du caractre *, suivi de la chane S inverse. Exemple abc*cba La chane peut contenir nimporte quel caractre alphanumrique. La chane se termine par le marqueur fin de ligne. Conception : Pour dterminer si la chane est lgale, il faut : 1) Lire jusqu * les caractres un un en les empilant dans une pile. 2) Aprs *, jusqu la fin de la chane, lire un caractre, dpiler le caractre au sommet de la pile et comparer les deux, sils ne sont pas gaux, la chane est invalide. 3) Si tous les caractres sont gaux et que la pile sest vide, la chane est valide. Une autre application est celle de la vrification du nombre de parenthses ouvrante et fermantes dans une expression arithmtique. 2. Calcul arithmtique arithmtique : Une application courante des piles se fait dans le calcul arithmtique: l'ordre dans la pile permet d'viter l'usage des parenthses. La notation postfixe (polonaise) consiste placer les oprandes devant l'oprateur. La notation infixe (parenthse) consiste entourer les oprateurs par leurs oprandes. Les parenthses sont ncessaires uniquement en notation infixe. Certaines rgles permettent d'en rduire le nombre (priorit de la multiplication par rapport l'addition, en cas d'oprations unaires reprsentes par un caractre spcial (-, !,...). Les notations prfixe et postfixe sont d'un emploi plus facile puisqu'on sait immdiatement combien

25

d'oprandes il faut rechercher. Dtaillons ici la saisie et l'valuation d'une expression postfixe: La notation usuelle, comme (3 + 5) * 2, est dite infixe. Son dfaut est de ncessiter l'utilisation de parenthses pour viter toute ambigut (ici, avec 3 + (5 * 2)). Pour viter le parenthsage, il est possible de transformer une expression infixe en une expression postfixe en faisant "glisser" les oprateurs arithmtiques la suite des expressions auxquelles ils s'appliquent. Exemple: (3 + 5) * 2 s'crira en notation postfixe (notation polaise): 3 5 + 2 * alors que 3 + (5 * 2) s'crira: 3 5 2 * + Notation infixe: A * B/C. En notation postfixe est: AB * C/. On voit que la multiplication vient immdiatement aprs ses deux oprandes A et B. Imaginons maintenant que A * B est calcul et stock dans T. Alors la division / vient juste aprs les deux arguments T et C. Forme infixe: A/B ** C + D * E - A * C (** tant loprateur dexponentiation) Forme postfixe: ABC ** /DE * + AC * -

26

Evaluation en Postfix
Considrons lexpression en postfixe suivante:
6523+8*+3+*

Algorithm
Initialiser la pile vide; while (ce nst pas la fin delexpression postfixe) { prendre litem prochain de postfixe; if(item est une valeur) empiler; else if(item operateur binaire ) { dpiler dans x; dpiler dans y; effectuer y operateur x; empiler le rsultat obtenu; } else if (item oprateur unaire) { dpiler dans x; effectuer oprateur(x); empiler le rsultat obtenu; } } la seule valeur qui reste dans la pile est le rsultat recherch. Oprateur binaries: +, -, *, /, etc., Oprateur unaires: moins unaire, racine carre, sin, cos, exp, etc. Pour 6 5 2 3 + 8 * + 3 + * Le premier item est une valeur (6); elle est empile. Le deuxime item est une valeur (5); elle est empile. Le prochain item est une valeur (2); elle est empile. Le prochain item est une valeur (3); elle est empile. La pile devient

TOS=> 3
2 5 6

Les items restants cette tape sont: + 8 * + 3 + * Le prochain item lu est '+' (operator binaire): 3 et 2 sont dpils et leur somme '5' est ensuite empile:

27

TOS=> 5
5 6

Ensuite 8 est empil et le prochain oprateur *:

TOS=> 8
5 5 6

TOS=> 40
5 6

(8, 5 sont dpils, 40 est empil) Ensuite loprateur + suivi de 3:

TOS=> 3 TOS=> 45
6 45 6

(40, 5 sont dpils ; 45 pushed, 3 est empil Ensuite loprateur +: 3 et 45 sont dpils et 45+3=48 est empil

TOS=> 48
6

Ensuite cest loparteur *: 48 and 6 sont dpils et 6*48=288 est empil

TOS=> 288

28

Il ny plus ditems lire dans lexpression postfixe et aussi il ny a quune seule valeur dans la pile reprsentant la rponse finale: 288. La rponse est trouve en balayant une seule fois la forme postfixe. La complexit de lalgorithme est par consquent en O(n); n tant la taille de la forme postfixe. La pile a t utilise comme une zone tampon sauvegardant des valeurs attendant leur oprateur.

29

Infixe Postfixe
Bien entendu la notation postfixe ne sera pas dune grande utilit sil nexistait pas un algorithme simple pour convertir une expression infixe en une expression postfixe. Encore une fois, cet algorithme utilise une pile.

Lalgorithme
initialise la pile et loutput postfixe vide; while(ce nest pas la fin de lexpression infixe) { prendre le prochain item infixe if (item est une valeur) concatner item postfixe else if (item == () empiler item else if (item == )) { dpiler sur x while(x != () concatner x postfixe & dpiler sur x } else { while(precedence(stack top) >= precedence(item)) dpiler sur x et concatner x postfixe; empiler item; } } while (pile non vide) dpiler sur x et concatner x postfixe; Prcdence des oprateurs (pour cet algorithme): 4 : ( dple seulement si une ) est trouve 3 : tous les oprateurs unaires 2:/* 1:+Lalgorithme passe les oprandes la forme postfixe, mais sauvegarde les oprateurs dans la pile jusqu ce que tous les oprandes soient tous traduits.

30

Exemple: considrons la forme infixe de lexpression a+b*c+(d*e+f)*g Stack TOS=> + Output


ab

TOS=> *
+

abc

TOS=> +

abc*+

TOS=>

* ( +

abc*+de

TOS=>

+ ( +

abc*+de*f

TOS=> +

abc*+de*f+

TOS=> *
+

abc*+de*f+g

empty

abc*+de*f+g*+

31

3. Appels de fonctions et implantation de la rcursivit Lors de lappel dune fonction, toutes les informations importantes, qui ont besoin dtre sauvegardes, telles que les valeurs des registres (correspondant aux noms des variables), ladresse de retour, sont stockes dans une pile (activation). Le contrle est ensuite transfr la nouvelle fonction, qui est libre de remplacer les valeurs des registres avec ses propres valeurs. Si la nouvelle fonction fait un appel, le mme processus est rpt (sauvegarde des infos dans la pile). Quand la fonction termine, elle remet jour les registres laide des informations du sommet de la pile, le contrle est ensuite transfr la fonction appelante ladresse de retour.

Cest suivant ce principe, bien simple, quil est possible au processeur deffectuer lexcution des fonctions rcursives. Voyons cela sur un exemple dune fonction rcursive. Reprenons lexemple de la fonction factoriel (n) avec n =4. On utilise pour indiquer ladresse de retour la fonction appelante. Au dpart, la fonction doit sauvegarder et la valeur 4 est passe factoriel. Ensuite, un appel rcursif factoriel est effectu, cette fois-ci avec la valeur 3. Si 1 est ladresse o cet appel est fait, alors 1 et la valeur 4 sont sauvegardes dans la pile. La fonction factoriel est maintenant excute avec le paramtre 3.

1 4

La fonction factoriel est maintenant excut avec le paramtre 3. De la mme manire, un autre appel est rcursif est effectu avec un paramtre n = 2 ncessitant la sauvegarde de ladresse 2 et
32

la valeur courante de n = 3. Ainsi de suite, jusqu arriver factoriel (1). A ce stade, la pile contient les informations suivantes : 3 2 2

3 2 1 4

Une fois factoriel(1) atteint, les appels rcursifs sarrtent. partir de l, la rcursion commence retourner des rsultats. Ainsi, chaque return de factoriel implique un dpilement de la pile de la valeur de n et celle de ladresse de retour de la fonction appelante. Comme chaque appel de fonction implique une activation est cre, les appels de fonctions sont assez coteux en temps et en espace mmoire. Mme si la rcursion est utilise pour permettre une implantation clair et facile, on souhaite souvent liminer les overheads gnrs par les appels rcursifs. Dans certains cas, la rcursion peut tre facilement remplace par litration(comme cest le cas de la fonction factoriel). Dans notre cas, on a tout simplement empiler les valeur de n jusqu atteindre le test darrt. Ensuite, on commence partir de l dpiler de la pile les valeurs sauvegardes et les multiplier au fur et mesure avec le rsultat courant. Cela nous donne la fonction suivante :

33

long factoriel(int n, Pile<int> & S) { //pour avoir n! dans une variable de type long ncessite n <= 12 assert ( (n >= 0 && (n <= 12)) ; while (n > 1) S.push(n--) ; long result = 1; while (S.pop(val)) result = result * val; retrun result; } En pratique, une forme itrative de la fonction factoriel est plus simple et rapide que la version cidessous utilisant une pile. Malheureusement, il nest pas toujours possible ainsi. Dans certaines situations, la rcursivit est ncessaire pour pouvoir rsoudre le problme en question comme dans le cas des tours de Hanoi, de la travers darbres et de certains algorithmes de tri tels que le tri rapide ou le tri par fusion. Reprenons lexemple de Hanoi : typedef int Pole; #define move(X, Y) cout << " Dplacer " << (X) << " vers " << (Y) << endl void TOH(int n, Pole start, Pole goal, Pole tmp) { // Cas de base if (n == 0) return; TOH(n-1, start, tmp, goal); // Appel rcursif: n-1 disques move(start, goal); // Dplacer un disque TOH(n-1, tmp, goal, start); // Appel rcursif n-1 disques } La fonction TOH fait deux appels rcursifs: un dplacement de n-1 disques de start tmp et un dplacement de n-1 disques de tmp goal. On peut liminer la rcursion en utilisant une pile pour sauvegarder une reprsentation que TOH doit effectuer : 2 appels et un une opration de dplacement. Pour y arriver, nous devons avant tout avoir une des diffrentes oprations, implantes comme une classe dont les membres vont tre sauvegards dans une pile. Cette classe peut tre comme ci-dessous :

34

enum TOHop { DOMOVE, DOTOH }; class TOHobj { public: TOHop op; int num; Pole start, goal, tmp; TOHobj(int n, Pole s, Pole g, Pole t) { op = DOTOH; num = n; start = s; goal = g; tmp = t; } TOHobj(Pole s, Pole g) { op = DOMOVE; start = s; goal = g; } }; La classe Tohobjet stoke 5 valeurs: un indicateur Pour savoir sil sagit dun mouvement ou dune nouvelle opration TOH, le nombre de disques, et les trois poles. Noter que lopration de mouvement a seulement besoin de stoker linformation sur deux poles. Par consquent, il ya deux constructeurs : un pour stoker ltat pour imiter un appel rcursif et un autre pour stocker ltat dune opration de mouvement. La version non-rcusrive de TOH est alors comme suit : void TOH(int n, Pole start, Pole goal, Pole tmp, Stack<TOHobj*>& S){ S.push(new TOHobj(n, start, goal, tmp)); // Initialisation TOHobj* t; while (S.pop(t)) { // Dterminer la prochaine tche if (t->op == DOMOVE) // Dplacement move(t->start, t->goal); else if (t->num > 0) { // 3 tapes de la rcursion en ordre inverse int num = t->num; Pole tmp = t->tmp, goal = t->goal, start = t->start; S.push(new TOHobj(num-1, tmp, goal, start)); S.push(new TOHobj(start, goal)); S.push(new TOHobj(num-1, start, tmp, goal)); } delete t; // Must delete the TOHobj we made } } On dfinit en premier un type dnumration appel TOHop, avec deux valeurs : MOVE et TOH, pour indiquer les appels la fonction MOVE et les appels rcursifs TOH, respectivement. Question : pourquoi utiliser le tableau pour reprsenter la pile. La nouvelle version de TOH commence par placer dans la pile une description du problme initial avec n disques. Le reste e la fonction est simplement une boucle while qui dpile de la pile et excute les oprations appropries. Dans le cas dune opration TOH (pour n>0), nous empilons dans la pile la reprsentation des trois oprations excutes par la version rcursives.

35

Toutefois, elles doivent tre places dan un ordre inverse pour quelles soient dplies dans le bon ordre.

36

Les files

1 Dfinition et exemples

Une file est une structure de donnes dynamique dans laquelle on insre des nouveaux lments la fin (queue) et on enlve des lments au dbut (tte de file). Lapplication la plus classique est la file dattente. La file sert beaucoup en simulation. Elle est aussi trs utilise aussi bien dans la vie courante que dans les systmes informatiques. Par exemple, elle modlise la file dattente des clients devant un guichet, les travaux en attente dexcution dans un systme de traitement par lots, ou encore les messages en attente dans un commutateur de rseau tlphonique. On retrouve galement les files dattente dans les programmes de traitement de transactions telle que les rservations de siges davion ou de billets de thtre. Noter que dans une file, le premier lment insr est aussi le premier retir. On parle de mode daccs FIFO (Fisrt In Fist Out). Comportement d'une pile: Last In First Out (LIFO) Comportement d'une file: First In First Out (FIFO) La file est modifie ses deux bouts.

37

2 Les oprations de base Les oprations primitives sur une file sont DFILER pour le retrait dun lment et ENFILER pour lajout dun lment. opration ENFILER (f, e) ajoute llment e la fin de la file f. Ainsi, la suite des oprations ENFILER (f, A), ENFILER (f, B) et ENFILER (f, C) donnent la file da la figure 1a. Noter quen principe, il ny aucune limite aux nombres dlments que lon peut ajouter dans une file.

tte Figure 1a

fin

Lopration e = DFILER (f) , retire (supprime) un lment de la fin de la file et renvoie sa valeur dans e. Ainsi lopration e = DFILER (f) provoque un changement dtat de la file (voir figure 1b) et met llment A dans e.

tte

fin Figure 1b

L'opration DFILER ne peut seffectuer sur une file vide. Il faudra donc utiliser une opration FILEVIDE qui permet de tester si une file est vide ou pas. Le rsultat des oprations ENFILER (f, D) et ENFILER (f, E) est donn par la figure 1c

tte Figure 1c

fin

38

3 Implantation dune file par un tableau Pour reprsenter une file par un tableau, on utilise deux variables tte et fin qui contiennent respectivement les indices du premier et du dernier lment de la file. Les valeurs initiales de fin est 1 et tte est 0; la file sera vide quand fin < tte. tout instant le nombre dlments de la file est : fin tte + 1. Prenons un exemple avec cette reprsentation. Supposons une file de taille maximum gale 5. Initialement, la file est vide.

tte = 0; fin = -1;

39

Aprs lajout des lments A, B et C, la file devient :

C B A

fin = 2

tte = 0

Aprs le retrait de deux lments on a :

tte = fin = 2

Aprs linsertion des lments D et E, on a :

E D C

fin = 4

tte = 2

Si maintenant nous voulons ajouter llment F dans la file, fin passerait 5; or, lindice maximum dans notre tableau est 4. On ne peut plus ajouter des lments alors quil y a de la place dans le tableau.

40

Une solution possible consiste dcaler vers le bas tous les lments de la file chaque fois quon supprime un lment. Dans ce cas, il nest plus ncessaire davoir la variable tte de file, puisque llment en tte de file sera toujours la position 0 du tableau. La file vide sera reprsente par fin = -1. Ce procd de dcalage dlments est simple raliser mais coteux en temps (pourquoi?). De plus, la suppression dun lment dune file ncessite logiquement la manipulation dun seul lment de la file, celui en tte de file. Implantation par tableau circulaire Un solution plus lgante au problme prcdent consiste utiliser un tableau circulaire. Quand la valeur de la fin de la file atteint le haut du tableau, on effectue les ajouts des lments partir du bas, les indices tte et fin progressent maintenant modulo la taille du tableau. Cest le cas par exemple dune interface de communication entre deux processus non synchroniss comme un programme et un priphrique dentre. Examinons lexemple suivant. Nous partons avec une file qui contient trois lments C, D et E.

E D C

fin = 4

tte = 2

Aprs lajout de llment F la file devient :

E D C tte = 2

fin = 0

41

Aprs la suppression des lments C et D, la file devient

tte = 4

fin = 0

Aprs lajout de llment G la file devient :

tte = 4

G F

fin = 1

la suppression de E, la file devient :

G F

fin = 1 tte = 0

42

travers cet exemple, on voit surgir un autre problme li au test de vacuit (file vide) : la condition fin < tte : cette condition a t vrifie sans que la file soit vide. Une solution possible est de choisir tte comme la position du tableau qui prcde le premier lment et non la position du premier lment lui mme. Comme fin est la valeur de la position du dernier lment dans la file, la file sera vide lorsque la tte est gale la fin.

Examinons dabord un exemple o l'on insre C, D, E, F et G . E D C tte = 1 F fin = 0 fin = 4 E D C tte = 1

E D C G F tte = fin = 1

Dans ce cas, la file est pleine et cest la mme condition (tte = fin) qui est satisfaite quand la file est vide. Il est clair que cette situation nest pas satisfaisante. Une solution consiste laisser au moins lespace dun lment entre la fin et la tte : si la taille de la file est de 100 par exemple, il faudra rserver un tableau 101 lments. Question : voyez-vous une autre manire de rsoudre ce problme? Un file en C++ est une classe dfinie comme suit. Pour simplifier nous allons considrer une file dentiers.

43

// fichier File.h // dclaration de la classe File #ifndef FILE_H #define FILE_H const int Taille_de_File = 100 ; class File { public : File ( int dim = Taille_de_File ) ; // Constructeur ~File () ; // destructeur void ViderFile (); // vide la file void Enfiler ( const int & ); // ajoute un lment en fin de file int Defiler () ; // retire l'lment en tte de file int PremierElement () const; // retourne la valeur en tte de file bool FileVide () const; private : int Taille; // Taille maximum de la file int Tete; int Fin; int *TabElement; // tableau contenant les lments de la file }; #endif

// Fichier File.cpp // Dfinitions des fonctions membres de la classe File #include <cassert> #include "File.h" File::File ( int dim ) // Constructeur { Taille = dim + 1; // ajoute une position supplmentaire Tete = Fin = Taille - 1; TabElement = new int [Taille]; } File::~File () {

44

delete []TabElement; } void File::ViderFile () { Tete = Fin; } void File::Enfiler ( const int & element ) { assert (((Fin+1) % Taille) != Tete ); // la file n'est pas pleine ? Fin = (Fin+1) % Taille; //incrmente fin dans le tableau circulaire TabElement[Fin] = element; } int File::Defiler () { assert ( !FileVide() ); // vrifie que la file n'est pas vide Tete = ( Tete + 1 ) % Taille; return TabElement[Tete]; // retourne la valeur en tte de file. } int File::PremierElement () const // retourne la valeur en tte de file { assert ( !FileVide() ) ; return TabElement[(Tete+1) % Taille]; } bool File::FileVide () const { return Tete == Fin; } // destructeur

45

Rfrences
1. Boudreault et al. Notes de cours 8INF211, UQAC 2. C.A. Shaffer (2001): A practical introduction to data structures and algorithms (chapitre2)

46

Vous aimerez peut-être aussi