PSI - 2014-2015 1 Structures de données linéaires 1 Introduction Lorsque l’on programme ou que l’on décrit un algorithme, on a l’habitude d’utiliser des types (entiers, réels, caractères, chaines, listes . . . ). Tous les langages de programmation ne possèdent pas les mêmes types (cf les entiers et entiers longs entre les versions 2 et 3 de Python). Il n’est pas nécéssaire de savoir comment est représenté un tel type du moment que l’on sait le manipuler. Les types prédéfinis sont peu nombreux, c’est pourquoi des constructeurs de types permettent de définir des types plus complexes, et donc des structures de données. Une structure de données est l’implémentation explicite d’un ensemble organisé d’objets avec la réalisation des opérations d’accès, de construction et de modification afférentes. Un type de données abstrait est la description d’un ensemble organisé d’objets et des opérations de manipulations sur cet ensemble. On s’intéresse ici aux types de données linéaires : on se donne une suite finie (e1 , e2 , . . . , en ) d’éléments d’un ensemble E. On ne dit rien sur l’accès à ces éléments de manière générale. On parle de liste linéaire (la liste est vide si n = 0). Il existe plusieurs variantes des listes linéaires et quelques implémentations usuelles. Dans une liste, on distingue les deux côtés, le début et la fin, et les opérations de manipulation peuvent se faire de chacun des ceux côtés. • Les piles sont des listes où l’insertion et la suppression ne se font que d’un seul et même côté. • Les files permettent l’insertion d’un côté et la suppression de l’autre. • Les files bilatères permettent l’insertion et la suppression des deux côtés. • Les listes pointées autorisent des insertions et suppressions également « à l’intérieur » et pas seulement aux extrémités. C’est le type de liste de Python, mais on appelle plus souvent cela un tableau. Certaines applications ont besoin de cet accès direct (tri bulles par exemple), mais pas tous (tri insertion). 2 Structure de pile On parle de pile ou LIFO (Last In, First Out). Les éléments d’une pile sont organisés comme une liste linéaire. L’accès aux éléments se fait d’un seul côté. C’est vraiment l’image de la pile que l’on se fait, par exemple une pile d’assiettes posées sur la table ou une pile de cartes. Les opérations possibles sont : • Construire une pile vide. • Tester si la pile est vide • Ajouter un élément dans la pile, on dit empiler. • Accéder au dernier élément ajouté dans la pile. C’est le sommet. • Supprimer le dernier élément ajouté dans la pile, on dit dépiler (bien sur, la pile doit être non vide). Remarque : pour l’opération dépiler, deux choix sont possibles : on peut ne rien renvoyer, ou renvoyer l’élément dépilé Propriété : Sur une pile, l’enchainement empiler-dépiler laisse la pile invariante. 3 3.1 Implémentation par les listes Piles de capacité finie La manière la plus simple consiste à utiliser une liste de longueur N + 1 où N est le nombre maximal d’éléments pouvant être stockés dans la pile. Les éléments sont rangés dans l’ordre où PSI - 2014-2015 2 ils ont été empilés. Pour pouvoir empiler ou dépiler, il faut connaître la position du sommet, donc le nombre d’éléments dans la pile. On le stocke dans la première case (p[0]), puis ensuite les éléments dans les cases allant de p[1] à p[N ]. n n éléments places disponibles Exemple : — p = créerpile(5) 0 1 a — empiler(p, a) 2 a b — empiler(p, b) 1 a — dépiler(p) Voici l’implémentation en Python : def creer_pileb(c):# creer une pile bornee de capacite c p=(c+1)*[None]# on remplit chaque case avec None p[0]=0# il y a 0 elements dans la pile return p def est_videb(p):#teste si le nb d’elements est nul return(p[0]==0) def empilerb(p,x): n=p[0]# on recupere le nombre d’elements if n>=len(p)-1: return (’La pile est pleine’) else: n=n+1#on augmente de 1 le nombre d’elements p[0]=n#on actualise la valeur de la premiere case p[n]=x#on stocke x a sa place (le sommet) def sommetb(p): if p[0]==0: return(’La pile est vide’) else: return(p[p[0]]) def depilerb(p): if p[0]==0: return(’La pile est vide’) else: n=p[0] p[0]=n-1 return(p[n])#renvoie l’element depile def taille(p):#renvoie le nb d’elements dans la pile return(p[0]) 3.2 Piles non bornées Un défaut de la structure précédente est sa capacité bornée car il faut être capable de déterminer la taille maximale dont on aura besoin, ce qui n’est pas toujours possible. On va donc présenter une structure de pile non bornée qui exploite une propriété des listes en Python : on peut ajouter ou supprimer un élément à l’extrémité droite de la liste en un temps constant O(1). PSI - 2014-2015 3 Remarque : C’est la complexité amortie : les listes Python sont des tableaux redimensionnables, c’est à dire dont la taille peut varier avec le temps. On utilise un tableau plus grand dont seuls certains éléments sont significatifs. Lorsqu’il s’agit d’augmenter la taille, disons de 1, deux cas se présentent : soit il reste de la place et il n’y a rien à faire, soit il ne reste plus de place et on alloue un nouveau tableau deux fois plus grand que le premier dans lequel tous les éléments sont recopiés et qui prend la place de l’ancien tableau. Les opérations sont toujours les mêmes. Voici alors l’implémantation : def creer_pile(): return[] def est_vide(p): return len(p)==0 def empiler(p,x):#ajouter un element p.append(x) def sommet(p):#acceder au dernier element empile if not(est_vide(p)): return p[-1] def depiler(p):#depile sans renvoyer le sommet if not(est_vide(p)):# on verifie que la pile n’est pas vide p.pop() def depilerbis(p): n=len(p) if n>0: del p[n-1] def depiler2(p):#depile et renvoie le sommet if not(est_vide(p)): return p.pop() Remarques sur le choix du côté par lequel on empile : • Ce choix est en cohérence avec le modèle de pile à capacité finie • Il permet d’utiliser la méthode l.append pour empiler qui est plus rapide que l = l + [x]. 4 4.1 Applications Tri insertion à l’aide d’une pile La structure de pile est tout à fait adaptée au tri insertion. On suppose que l’on dispose de deux piles. Une pile p donnée en entrée et une pile s qui représentera la sortie. Deux choix sont possibles : soit le sommet est le plus petit élément, soit c’est le plus grand (ce qui affichera le même résultat que nos algorithmes de tri). def Tri_insertion1(p):#le sommet de la pile est le plus grand element s=creer_pile() while not(est_vide(p)): e=sommet(p) depiler(p) while not(est_vide(s)) and e<sommet(s):#si s n’est pas vide, on compare empiler(p,sommet(s)) #son sommet Ĺ l’element e depiler(s)#tant qu’il est + grand, on l’empile sur p PSI - 2014-2015 4 empiler(s,e)#on insere e Ĺ sa place while not(est_vide(p)) and sommet(p)>sommet(s): empiler(s,sommet(p))#on place dans s les elements de p depiler(p)#plus grands que le sommet de s return(s) Si on utilise la fonction dépiler qui renvoie l’élément, la fonction s’écrit alors : def Tri_insertion1bis(p):#le sommet de la pile est le plus grand element s=creer_pile() while not(est_vide(p)): e=depiler2(p) while not(est_vide(s)) and e>sommet(s): empiler(p,depiler2(s)) empiler(s,e) while not(est_vide(p)) and sommet(p)<sommet(s): empiler(s,depiler2(p)) return(s) 4.2 Analyse d’une expression bien parenthésée Etant donnée une chaîne de caractères ne contenant que des caractères ’(’ et ’)’, déterminer s’il s’agit d’un mot bien parenthésé, i.e. : • soit vide, • soit la concaténation de deux mots bien parenthésés, • soit un mot bien parenthésé mis entre parenthèses. Exemples : ()() est bien parenthésée, (()) aussi, ainsi que (()())(). En revanche, ( ou )( ou ()( ne sont pas bien parenthésées. On veut écrire une fonction qui prenne en argument une expression et qui renvoie False si elle n’est pas bien parenthésée et qui renvoie la liste des couples des parenthèses se correspondant. L’idée consiste à parcourir le mot de la gauche vers la droite et à utiliser une pile pour indiquer les indices de toutes les parenthèses ouvertes et non encore fermées, vues jusqu’à présent. def BienParenthese(m): l=[] p=creer_pile() for i in range(len(m)): if m[i]==’(’: empiler(p,i) else: if taille(p)>0: l=l+[[depiler(p),i]] else: return False if est_vide(p): return(True,l) else: return(False) Terminaison : BienParenthese termine car c’est une boucle for. Correction : notons li la liste l après le i-ème passage dans la boucle et pi la pile p. Vérifions l’invariant de boucle suivant : PSI - 2014-2015 5 li contient les couples d’indices de parenthèses ouvrante et fermante correspondant parmi les i premiers éléments de m et pi contient les indices des parenthèses ouvrantes rencontrées parmi les i premiers éléments de m, non encore fermées. Il est bien vérifié pour i = 0. Lors du i + 1-ème passage dans la boucle, soit on rencontre une parenthèse ouvrante, on empile dans p son indice et pi+1 ainsi modifiée et li+1 = li satisfont bien l’invariant de boucle. Soit on rencontre une fermante. Ou bien elle ferme la dernière ouvrante : on dépile pi et on ajoute le couple d’indices dans li et donc li+1 et pi+1 vérifient l’invariant de boucle, ou bien il n’y a plus d’ouvrante (p est vide), on sort alors de la boucle et False est renvoyé : l’expression n’est pas bien parenthésée. Après la dernière boucle, soit p est vide : chaque ouvrante a trouvé une fermante, l’expression est bien parenthésée et l contient ce que l’on veut et on renvoie ce qui est demandé. Soit p n’est pas vide : des parenthèses ouvrantes n’ont pas été fermées, l’expression n’est pas bien parenthésée et on renvoie False. La fonction est donc bien correcte. 4.3 Exaluation d’une fonction récursive La notion de pile d’évaluation peut être utilisée pour l’évaluation de fonctions récursives. 5 Structure de file (hors programme) Le type de données file est très similaire au type pile avec cependant une différence notable : l’élément auquel on a accès n’est pas le dernier ajouté mais le plus ancien dans la file. Une file est à comparer à une file d’attente au cinéma. On parle de FIFO (First In, First Out : premier arrivé, premier sorti). Les opérations sur les files sont les suivantes : • Créer une file vide • La file est-elle vide ? • ajouter un élément, on dit enfiler • défiler un élément (et récupére sa valeur) : cet élément sera le premier à avoir été enfilé. Propriété : Sur une file non vide, les opérations défiler et enfiler sont commutatives. Comme pour les piles, ces opérations doivent être faites en temps constant. La structure de liste Python n’est pas adaptée car l’insertion (ou la suppression) en début de liste ne se fait pas en temps constant (car Python décale les autres valeurs). Il existe un type supplémentaire : deque, dans le module collections, permettant de simuler les files : from collections import deque #importe la structure de file def creer_file():#creer une file vide return deque() #est_vide fonctionne aussi pour une file def tete_de_file(f): if not (est_vide(f)): return f[-1] def enfiler(f,x): f.appendleft(x)#ajoute un element en fin de file PSI - 2014-2015 6 def defiler(f,x):#defile et renvoie la tete de file if not(est_vide(f)): return f.pop() Pour céer une file non vide, on peut taper dans le shell : >>> d=deque((4,5,6)) >>> d deque([4, 5, 6]) >>> Début et fin sont interchangeables car il existe aussi les fonctions suivantes qui s’exécutent en temps constant : d.popleft() et d.append(v)