Structures de données linéaires 1 Introduction 2 Structure de pile 3

publicité
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)
Téléchargement