TP et projets#
Structures linéaires : piles, files, listes chaînées
TP Piles et files au travail#
deux implémentations, quatre applications
Durée : 3 h environ (deux séances) · Sur machine, seul ou par deux
Fichiers à télécharger
But du TP
Construire soi-même une pile et une file à partir d’une liste chaînée, mesurer ce que l’on gagne, puis mettre ces structures au service de quatre programmes réels : un vérificateur de code et de pages HTML, une calculatrice qui respecte les priorités, les boutons « Annuler / Rétablir » d’un éditeur, et la simulation de la file d’attente d’un guichet. ▶ signale ce qui est à programmer et tester.
Consignes
-
Les parties 2 à 5 sont indépendantes une fois la partie 1 terminée.
-
Le fichier
tp_structures_lineaires_depart.pyest à télécharger (lien ci-dessus) : il contient les classes du cours, des fonctions toutes prêtes et, en fin de fichier, des tests. Relancer le fichier après chaque partie : chaque ligne[A FAIRE]doit devenir[OK]. -
Dans les parties 2 à 5, on n’utilise que l’interface des piles et des files (
empiler,depiler,sommet,enfiler,defiler,est_vide,taille). -
Usage de l’IA, indiqué sur chaque exercice : aucune ; en appui (déboguer, reformuler, vérifier ; la réponse finale est écrite et comprise par vous) ; l’exercice s’appuie sur une réponse d’IA. Voir la charte d’usage de l’IA.
Deux implémentations pour une même interface#
Le cours a donné une pile et une file qui cachent une list Python (classes PileListe et FileListe du fichier fourni) et a signalé que pop(0) rendait la file lente. On construit ici l’autre implémentation, avec la classe Cellule du cours.
★ Exercice 1 — Une pile chaînée #
Dans PileChainee, l’attribut _tete désigne la cellule du sommet (ou None si la pile est vide) et _taille le nombre d’éléments.
La pile obtenue après empiler(5), empiler(8), empiler(3).
-
Dessiner la pile après un
empiler(7), puis après undepiler(). Combien de liens faut-il modifier à chaque fois ? Ce nombre dépend-il de la taille de la pile ? -
▶ Compléter les cinq méthodes de
PileChainee.depileretsommetlèventIndexError("pile vide")sur une pile vide (commePileListe;raise: voir le chapitre Programmation orientée objet).Coup de pouce
Empiler, c’est créer une cellule devant l’ancienne tête :
Cellule(x, self._tete). Dépiler, c’est mémoriser la valeur de la tête, puis avancer la tête d’une cellule. Ne pas oublier de tenir_tailleà jour. -
▶ Vérifier que
tester_pile(PileListe)ettester_pile(PileChainee)renvoient"OK". Pourquoi la même fonction de test convient-elle aux deux classes ? -
▶ Ajouter une méthode
__repr__pour queprint(p)affichesommet -> [3, 8, 5].
Corrigé
Avec ces solutions, les huit tests du fichier tp_structures_lineaires_depart.py affichent [OK].
1. empiler(7) crée une cellule 7 dont le lien pointe vers l’ancienne tête 3, et _tete désigne désormais cette cellule : 7 → 3 → 8 → 5 → None. depiler() renvoie 7 et ramène _tete sur 3. Chaque opération modifie un lien (plus la création d’une cellule), quelle que soit la taille de la pile : coût constant.
2. et 4.
class PileChainee:
def __init__(self):
self._tete = None
self._taille = 0
def est_vide(self):
return self._tete is None
def empiler(self, x):
self._tete = Cellule(x, self._tete) # nouvelle cellule devant
self._taille = self._taille + 1
def depiler(self):
if self.est_vide():
raise IndexError("pile vide")
v = self._tete.valeur
self._tete = self._tete.suivante # la tete avance d'un cran
self._taille = self._taille - 1
return v
def sommet(self):
if self.est_vide():
raise IndexError("pile vide")
return self._tete.valeur
def taille(self):
return self._taille
def __repr__(self):
texte = ""
c = self._tete
while c is not None: # on parcourt depuis le sommet
texte = texte + str(c.valeur)
if c.suivante is not None: # pas de virgule apres le dernier
texte = texte + ", "
c = c.suivante
return "sommet -> [" + texte + "]"
3. Les deux appels renvoient "OK". La fonction de test n’utilise que les méthodes de l’interface : toute classe qui les fournit avec le bon comportement passe le test, quelle que soit son implémentation.
★★ Exercice 2 — Une file chaînée à deux entrées #
Pour une file, on retire en tête et l’on ajoute en queue. On garde donc deux attributs : _tete (la première cellule, celle qu’on défilera) et _queue (la dernière, derrière laquelle on enfile).
-
Dessiner la file après
enfiler(1), puis aprèsdefiler(). Que se passerait-il, pourenfiler, si l’on n’avait pas l’attribut_queue? -
Deux cas particuliers demandent de l’attention : enfiler dans une file vide, et défiler le dernier élément. Que doivent valoir
_teteet_queuedans chacun de ces cas ? -
▶ Compléter
FileChaineeet vérifiertester_file(FileChainee). Ce test enfile de nouveau après avoir vidé la file : quel oubli détecte-t-il ?Coup de pouce
Enfiler : créer
c = Cellule(x, None); si la file est vide,cdevient la tête, sinon on l’accroche derrière l’ancienne queue (self._queue.suivante = c) ; dans les deux cas,cdevient la queue. Défiler : si la tête devientNone, la queue doit aussi redevenirNone.
Corrigé
1. Après enfiler(1) : 5 → 8 → 3 → 1 → None, _queue sur 1. Après defiler() (qui renvoie 5) : _tete sur 8. Sans _queue, il faudrait parcourir toute la chaîne depuis la tête pour trouver la dernière cellule : enfiler coûterait \(n\) opérations.
2. Enfiler dans une file vide : la nouvelle cellule devient à la fois tête et queue. Défiler le dernier élément : _tete devient None, et _queue doit redevenir None elle aussi (sinon elle désigne une cellule qui n’est plus dans la file).
3.
class FileChainee:
def __init__(self):
self._tete = None
self._queue = None
self._taille = 0
def est_vide(self):
return self._tete is None
def enfiler(self, x):
c = Cellule(x, None)
if self.est_vide():
self._tete = c
else:
self._queue.suivante = c # on accroche derriere la queue
self._queue = c
self._taille = self._taille + 1
def defiler(self):
if self.est_vide():
raise IndexError("file vide")
v = self._tete.valeur
self._tete = self._tete.suivante
if self._tete is None: # la file vient d'etre videe
self._queue = None
self._taille = self._taille - 1
return v
def tete(self):
if self.est_vide():
raise IndexError("file vide")
return self._tete.valeur
def taille(self):
return self._taille
Le test qui ré-enfile après avoir vidé la file vise une erreur fréquente : oublier de remettre _queue à None et tester _queue (au lieu de _tete) pour savoir si la file est vide dans enfiler. Le 9 est alors accroché derrière une cellule fantôme qui n’est plus dans la file, _tete reste None, et le defiler suivant lève IndexError : le test échoue.
★★ Exercice 3 — Mesurer le gain #
La fonction fournie chronometrer_file(Classe, n) renvoie le temps (en secondes) mis pour enfiler puis défiler n entiers.
-
▶ Recopier et compléter sur le cahier le tableau suivant (deux chiffres significatifs suffisent).
n\(10\,000\) \(50\,000\) \(100\,000\) \(200\,000\) FileListe(s)FileChainee(s) -
Quand
ndouble, par combien le temps est-il à peu près multiplié pour chaque classe ? Relier ces observations au coût d’undefilerdans chaque implémentation (voir le cours). -
Les programmes des parties suivantes n’ont pas besoin de savoir quelle implémentation est utilisée. Expliquer en une phrase pourquoi c’est un avantage.
Corrigé
1. Valeurs obtenues sur un ordinateur portable récent (elles varient d’une machine à l’autre, seul l’ordre de grandeur compte) :
n |
\(10\,000\) | \(50\,000\) | \(100\,000\) | \(200\,000\) |
|---|---|---|---|---|
FileListe (s) |
\(0{,}013\) | \(0{,}29\) | \(1{,}1\) | \(4{,}5\) |
FileChainee (s) |
\(0{,}011\) | \(0{,}057\) | \(0{,}12\) | \(0{,}24\) |
2. Quand n double, le temps de FileChainee double (coût linéaire : chaque opération est en temps constant) alors que celui de FileListe est multiplié par environ 4 (coût quadratique) : chacun des \(n\) defiler fait un pop(0) qui décale tous les éléments restants.
3. On peut changer d’implémentation (pour gagner en vitesse, par exemple) sans réécrire ni même relire les programmes qui utilisent la structure : il suffit que l’interface soit respectée.
Un vérificateur pour de vrais programmes#
Le cours vérifie le parenthésage d’une expression courte et répond seulement « correct » ou « incorrect ». Un éditeur de code fait mieux : il travaille sur un programme entier et indique où est l’erreur.
★★ Exercice 4 — Localiser l’erreur dans un programme Python #
On veut écrire verifier_code(code) qui renvoie None si les (), [] et {} du programme code (une chaîne pouvant contenir des "\n") sont bien appariés, et sinon un message parmi les trois formes suivantes :
ligne 1, colonne 26 : ')' ferme '[' ouvert ligne 1, colonne 21
ligne 1, colonne 5 : '(' jamais ferme
ligne 1, colonne 6 : ')' ne ferme rien
Deux difficultés nouvelles : un symbole écrit dans une chaîne (print("(")) ou dans un commentaire (# voir (2)) ne compte pas ; et, pour signaler où se trouve un symbole ouvrant jamais refermé, il faut se souvenir de sa position.
-
Que faut-il empiler, au lieu du seul symbole ouvrant, pour pouvoir écrire ces messages ? Proposer un tuple.
-
On parcourt le code caractère par caractère en tenant à jour
ligneetcolonne, ainsi que deux indicateurs :dans_chaine(vautNone, ou le guillemet"ou’qui a ouvert la chaîne en cours) etdans_commentaire(booléen). À quel caractère chacun de ces deux indicateurs se « remet-il à zéro » ? -
▶ Écrire
verifier_code. Les tests de la partie 2 du fichier fourni doivent passer. Essayer aussi la fonction sur un de vos propres programmes, puis après y avoir supprimé une parenthèse.
Corrigé
1. On empile le triplet (symbole, ligne, colonne) de chaque ouvrant.
2. dans_chaine redevient None au guillemet identique à celui qui a ouvert la chaîne ; dans_commentaire redevient False au retour à la ligne.
3.
def verifier_code(code):
correspond = {')': '(', ']': '[', '}': '{'}
p = PileChainee()
ligne = 1
colonne = 0
dans_chaine = None
dans_commentaire = False
for c in code:
colonne = colonne + 1
if c == "\n":
ligne = ligne + 1
colonne = 0
dans_commentaire = False
elif dans_commentaire:
pass
elif dans_chaine is not None:
if c == dans_chaine:
dans_chaine = None
elif c == "#":
dans_commentaire = True
elif c in "\"'":
dans_chaine = c
elif c in "([{":
p.empiler((c, ligne, colonne))
elif c in correspond:
if p.est_vide():
return f"ligne {ligne}, colonne {colonne} : '{c}' ne ferme rien"
o, lo, co = p.depiler()
if o != correspond[c]:
return (f"ligne {ligne}, colonne {colonne} : '{c}' ferme '{o}'"
f" ouvert ligne {lo}, colonne {co}")
if not p.est_vide():
o, lo, co = p.depiler()
return f"ligne {lo}, colonne {co} : '{o}' jamais ferme"
return None
Sur ’d = {"a": [1, 2],\n "b": (3, 4]}’ (la ligne 2 commence par cinq espaces), la fonction renvoie ligne 2, colonne 16 : ’]’ ferme ’(’ ouvert ligne 2, colonne 11. Limite assumée : les chaînes triples """...""" et les guillemets échappés \" ne sont pas gérés.
★★★ Exercice 5 — Défi — une vraie page HTML #
Une page Web réelle contient des balises avec attributs (<h1 class="titre">) et des balises orphelines, qui n’ont pas de fermante (<br>, <img src="a.png">…). La fonction fournie extraire_balises(html) s’occupe de ces détails : elle renvoie la liste des noms de balises dans l’ordre, une fermante étant précédée de /.
>>> extraire_balises('<p class="a">Un <b>mot</b><br></p>')
['p', 'b', '/b', '/p']
▶ Écrire verifier_html(html) qui renvoie True si les balises de la page sont bien imbriquées, puis la tester sur une page HTML écrite en SNT ou téléchargée (affichage du code source dans le navigateur, puis copier-coller dans une chaîne """...""").
Corrigé
def verifier_html(html):
p = PileChainee()
for b in extraire_balises(html):
if b[0] == "/": # balise fermante
if p.est_vide() or p.depiler() != b[1:]:
return False
else:
p.empiler(b)
return p.est_vide()
La page ’<p>Un <b>gras <i>italique</b></i></p>’ donne False (</b> arrive alors que i est au sommet) ; la page ’<ul><li>un</li><li>deux</ul>’ aussi (li jamais fermé). Les navigateurs, eux, tolèrent ces erreurs et réparent la page comme ils peuvent.
Une calculatrice qui respecte les priorités#
En notation polonaise inverse (NPI), on écrit l’opérateur après ses deux opérandes : "3 4 2 * +" signifie \(3 + 4\times 2\). L’évaluation se fait avec une pile de nombres (feuille d’exercices). On construit ici la calculatrice complète : on traduit d’abord l’expression usuelle en NPI, puis on l’évalue.
★★ Exercice 6 — Une évaluation qui ne plante pas #
▶ Écrire evaluer_npi(expr) pour les quatre opérations (on utilisera la fonction fournie calculer(a, op, b) ; les nombres sont convertis avec float). L’expression peut être mal formée : la fonction lève alors ValueError avec un message explicite, au lieu de planter sur une pile vide.
Coup de pouce
Avant de dépiler les deux opérandes d’un opérateur, vérifier p.taille(). À la fin, une expression correcte laisse exactement un nombre dans la pile. Le premier nombre dépilé est l’opérande de droite.
>>> evaluer_npi("10 4 /")
2.5
>>> evaluer_npi("3 +")
ValueError: il manque un operande pour '+'
>>> evaluer_npi("3 4 5 +")
ValueError: expression incomplete : il reste 2 nombres
Corrigé
def evaluer_npi(expr):
p = PileChainee()
for jeton in expr.split():
if jeton in PRIORITE: # un operateur
if p.taille() < 2:
raise ValueError(f"il manque un operande pour '{jeton}'")
b = p.depiler() # operande de droite
a = p.depiler() # operande de gauche
p.empiler(calculer(a, jeton, b))
else:
p.empiler(float(jeton))
if p.taille() != 1:
raise ValueError("expression incomplete : il reste "
+ str(p.taille()) + " nombres")
return p.depiler()
"10 4 /" donne 2.5 ; "1 0 /" lève ZeroDivisionError (levée par calculer).
★★★ Exercice 7 — De l’écriture usuelle à la NPI #
Pour traduire une expression usuelle (dite infixe), on la lit de gauche à droite, jeton par jeton (la fonction fournie decouper produit la liste des jetons), avec une liste de sortie et une pile d’opérateurs :
-
un nombre va directement dans la sortie ;
-
une parenthèse
(est empilée ; -
une parenthèse
): on dépile les opérateurs vers la sortie jusqu’à trouver la(, que l’on dépile et que l’on jette ; -
un opérateur
op: tant que le sommet de la pile est un opérateur de priorité supérieure ou égale à celle deop(*et/valent \(2\),+et-valent \(1\), dictionnairePRIORITE), on le dépile vers la sortie ; puis on empileop; -
à la fin, on vide la pile dans la sortie.
-
À la main, recopier et compléter sur le cahier la trace pour
2 * (3 + 4) - 5:Jeton Pile d’opérateurs écrite en ligne (sommet à droite) Sortie 2(vide) 2**2(3+4)-5fin -
Pourquoi la règle dit-elle « supérieure ou égale » ? Comparer la traduction de
10 - 4 - 3avec cette règle et avec une règle « strictement supérieure », et évaluer les deux résultats. -
▶ Écrire
infixe_vers_npi(expr)qui renvoie la chaîne NPI (jetons séparés par des espaces). La fonctioncalculatricefournie enchaîne alors traduction et évaluation : vérifier quecalculatrice("12*(3+4)-(5+5)*2")vaut bien \(64\).
Remarque
Cette méthode, publiée par Edsger Dijkstra en 1961, est surnommée « algorithme de la gare de triage » : les opérateurs patientent sur une voie de garage (la pile) avant de rejoindre le train de sortie. Les calculatrices et les compilateurs utilisent toujours cette idée.
Corrigé
1.
| Jeton | Pile écrite en ligne (sommet à droite) | Sortie |
|---|---|---|
2 |
(vide) | 2 |
* |
* |
2 |
( |
* ( |
2 |
3 |
* ( |
2 3 |
+ |
* ( + |
2 3 |
4 |
* ( + |
2 3 4 |
) |
* |
2 3 4 + |
- |
- (* dépilé : priorité \(2 \ge 1\)) |
2 3 4 + * |
5 |
- |
2 3 4 + * 5 |
| fin | (vide) | 2 3 4 + * 5 - |
L’évaluation de 2 3 4 + * 5 - donne \(9\), comme \(2\times(3+4)-5\).
2. Avec « supérieure ou égale », 10 - 4 - 3 devient 10 4 - 3 -, qui vaut \((10-4)-3 = 3\) : c’est le bon résultat, car les opérations de même priorité se font de gauche à droite. Avec « strictement supérieure », le premier - resterait dans la pile : on obtiendrait 10 4 3 - -, soit \(10-(4-3) = 9\), ce qui est faux.
3.
def infixe_vers_npi(expr):
sortie = []
ops = PileChainee()
for j in decouper(expr):
if j in PRIORITE:
while (not ops.est_vide() and ops.sommet() != '('
and PRIORITE[ops.sommet()] >= PRIORITE[j]):
sortie.append(ops.depiler())
ops.empiler(j)
elif j == '(':
ops.empiler(j)
elif j == ')':
while ops.sommet() != '(':
sortie.append(ops.depiler())
ops.depiler() # on jette la '('
else:
sortie.append(j) # un nombre
while not ops.est_vide():
sortie.append(ops.depiler())
texte = "" # on recolle les jetons
for i in range(len(sortie)):
if i > 0:
texte = texte + " " # un espace entre deux jetons
texte = texte + sortie[i]
return texte
"12*(3+4)-(5+5)*2" se traduit en 12 3 4 + * 5 5 + 2 * - et vaut 64.0.
Les boutons « Annuler » et « Rétablir »#
★★ Exercice 8 — Un mini-éditeur #
La classe Editeur possède un attribut texte et deux piles : _annuler contient les états précédents du texte, _retablir les états qui viennent d’être annulés.
-
ecrire(s)ajoutesà la fin du texte ;effacer(k)supprime leskderniers caractères ; -
avant chacune de ces deux actions, l’état courant est empilé dans
_annuler, et la pile_retablirest vidée ; -
annuler()revient à l’état précédent ;retablir()refait ce qui vient d’être annulé ; ni l’une ni l’autre ne fait quoi que ce soit si sa pile est vide. -
Avant de coder : dans
annuler(), l’état courant ne disparaît pas, il est rangé quelque part. Où ? Même question pourretablir(). -
▶ Compléter la classe et faire passer les tests de la partie 4.
-
Pourquoi faut-il vider
_retablirlors d’une nouvelle action ? Donner un exemple de texte absurde obtenu si on l’oublie. -
On sauvegarde ici le texte entier à chaque action. Pour un document de \(100\,000\) caractères modifié \(1\,000\) fois, combien de caractères les piles contiennent-elles environ ? Proposer une autre chose à empiler pour économiser la mémoire.
Corrigé
1. annuler() range l’état courant dans _retablir (pour pouvoir le refaire) ; retablir() le range dans _annuler (pour pouvoir l’annuler de nouveau).
2.
class Editeur:
def __init__(self):
self.texte = ""
self._annuler = PileChainee()
self._retablir = PileChainee()
def _sauver(self):
self._annuler.empiler(self.texte)
self._retablir = PileChainee() # une nouvelle action efface l'avenir
def ecrire(self, s):
self._sauver()
self.texte = self.texte + s
def effacer(self, k):
self._sauver()
self.texte = self.texte[:max(0, len(self.texte) - k)]
def annuler(self):
if not self._annuler.est_vide():
self._retablir.empiler(self.texte)
self.texte = self._annuler.depiler()
def retablir(self):
if not self._retablir.est_vide():
self._annuler.empiler(self.texte)
self.texte = self._retablir.depiler()
(Écrire _sauver une fois évite de dupliquer le code dans ecrire et effacer ; la méthode commence par _ car elle ne fait pas partie de l’interface.)
3. Sans vidage : on tape Le chat dort bien, on annule deux fois (Le chat), on tape mal (Le chat mal), puis retablir() ressort l’état Le chat dort, qui n’a jamais suivi Le chat mal : l’« avenir » annulé n’a plus de sens après une nouvelle action.
4. Environ \(1\,000 \times 100\,000 = 10^8\) caractères, soit une centaine de mégaoctets. On peut empiler plutôt l’action et ce qu’il faut pour la défaire (par exemple ("ecrire", " mal") ou ("effacer", "bien")) : quelques caractères par action. C’est ce que font les vrais éditeurs.
Simuler la file d’attente d’un guichet#
Un guichet sert les clients dans leur ordre d’arrivée : c’est une file. On représente chaque client par un couple (arrivée, durée) en minutes. La simulation avance minute par minute ; à chaque minute t :
-
les clients qui arrivent à la minute
tsont enfilés ; -
si le guichet est libre (il se libère à la minute
libre_a, etlibre_a <= t) et que la file n’est pas vide, on défile un client : son attente vautt - arrivée, et le guichet sera libre àt + durée.
La simulation s’arrête quand tous les clients sont arrivés et que la file est vide.
★ Exercice 9 — À la main #
On utilise les dix clients de la liste CLIENTS du fichier fourni : (0, 4), (1, 3), (2, 2), (3, 5), (5, 1), (6, 3), (6, 2), (9, 4), (12, 1), (13, 2). Recopier et compléter sur le cahier le tableau des cinq premiers clients, avec un seul guichet.
| Client | Arrivée | Début du service | Attente | Fin du service |
|---|---|---|---|---|
| 1 | 0 | 0 | 0 | 4 |
| 2 | 1 | |||
| 3 | 2 | |||
| 4 | 3 | |||
| 5 | 5 |
Corrigé
| Client | Arrivée | Début | Attente | Fin |
|---|---|---|---|---|
| 1 | 0 | 0 | 0 | 4 |
| 2 | 1 | 4 | 3 | 7 |
| 3 | 2 | 7 | 5 | 9 |
| 4 | 3 | 9 | 6 | 14 |
| 5 | 5 | 14 | 9 | 15 |
Pour les dix clients, les attentes sont \(0, 3, 5, 6, 9, 9, 12, 11, 12, 12\) : moyenne \(79/10 = 7{,}9\) minutes.
★★ Exercice 10 — Programmer la simulation #
-
▶ Écrire
simuler(clients)pour un guichet ; elle renvoie le triplet(attente moyenne, attente maximale, longueur maximale de la file). Vérifier qu’on obtient(7.9, 12, 6)surCLIENTS, et que vos cinq premières attentes calculées à la main sont bien celles du programme. -
▶ Ajouter un paramètre
nb_guichets(valeur par défaut1) : on garde une seule file commune, et une listelibre_ad’une case par guichet. Avec deux guichets, on doit obtenir(0.9, 3, 2).Coup de pouce
À chaque minute, après les arrivées, parcourir les guichets : chaque guichet libre prend le client en tête de file, s’il y en a un.
Corrigé
Version directement écrite pour plusieurs guichets (avec nb_guichets = 1, c’est la réponse à la question 1) :
def simuler(clients, nb_guichets=1):
f = FileChainee()
libre_a = [0] * nb_guichets # minute ou chaque guichet se libere
attentes = []
i = 0 # indice du prochain client a arriver
t = 0
file_max = 0
while i < len(clients) or not f.est_vide():
while i < len(clients) and clients[i][0] == t: # a) arrivees
f.enfiler(clients[i])
i = i + 1
for g in range(nb_guichets): # b) services
if libre_a[g] <= t and not f.est_vide():
arrivee, duree = f.defiler()
attentes.append(t - arrivee)
libre_a[g] = t + duree
file_max = max(file_max, f.taille())
t = t + 1
total = 0
attente_max = 0
for a in attentes:
total += a
if a > attente_max:
attente_max = a
return total / len(attentes), attente_max, file_max
Résultats : (7.9, 12, 6) avec un guichet, (0.9, 3, 2) avec deux.
★★ Exercice 11 — Quand la file explose #
La fonction fournie generer_clients(n, proba, duree_max, graine) fabrique n clients : à chaque minute, un client arrive avec la probabilité proba, et son service dure entre \(1\) et duree_max minutes (en moyenne \(3{,}5\) minutes pour duree_max = 6). La graine rend le tirage reproductible : avec la même graine, tout le monde obtient les mêmes clients.
-
▶ Avec
n = 500,duree_max = 6etgraine = 42, recopier et compléter le tableau (attente moyenne arrondie au dixième).probaCharge \(=\) proba\(\times 3{,}5\)Attente moy., 1 guichet Attente moy., 2 guichets \(0{,}20\) \(0{,}70\) \(0{,}25\) \(0{,}30\) \(0{,}35\) -
La charge est le nombre moyen de minutes de travail qui arrivent au guichet par minute. Que se passe-t-il pour l’attente quand la charge dépasse \(1\) ? Expliquer pourquoi, sans calcul.
-
(Pour aller plus loin.) ▶ Tracer, avec
matplotlib, l’attente moyenne en fonction deproba(de \(0{,}05\) à \(0{,}40\) par pas de \(0{,}01\)) pour un et deux guichets. Un responsable veut une attente moyenne inférieure à \(5\) minutes avec un seul guichet : jusqu’à quelle fréquence d’arrivée peut-il tenir ?
Corrigé
1. Valeurs obtenues (graine 42, identiques sur toutes les machines) :
proba |
Charge | 1 guichet | 2 guichets |
|---|---|---|---|
| \(0{,}20\) | \(0{,}70\) | \(3{,}9\) | \(0{,}1\) |
| \(0{,}25\) | \(0{,}875\) | \(8{,}6\) | \(0{,}2\) |
| \(0{,}30\) | \(1{,}05\) | \(48{,}8\) | \(0{,}3\) |
| \(0{,}35\) | \(1{,}225\) | \(172{,}6\) | \(0{,}7\) |
(Les attentes maximales correspondantes, avec un guichet, sont \(22\), \(40\), \(85\) et \(325\) minutes.)
2. Au-delà d’une charge de \(1\), il arrive en moyenne plus de travail par minute que le guichet ne peut en traiter : la file ne se vide jamais et s’allonge sans cesse, donc l’attente croît avec le nombre de clients au lieu de se stabiliser. Même en dessous de \(1\), l’attente grimpe vite à l’approche de \(1\) (le hasard crée des « paquets » d’arrivées que le guichet n’absorbe pas). Avec deux guichets, la charge par guichet est divisée par deux : l’attente reste minime.
3. On construit la liste des valeurs de proba, puis, par compréhension, la liste des attentes moyennes correspondantes pour un et deux guichets.
import matplotlib.pyplot as plt
probas = [k / 100 for k in range(5, 41)]
un = [simuler(generer_clients(500, p, 6, 42), 1)[0] for p in probas]
deux = [simuler(generer_clients(500, p, 6, 42), 2)[0] for p in probas]
plt.plot(probas, un, label="1 guichet")
plt.plot(probas, deux, label="2 guichets")
plt.xlabel("probabilite d'arrivee par minute")
plt.ylabel("attente moyenne (min)")
plt.legend()
plt.show()
Autre méthode : une seule boucle qui remplit les trois listes au fur et à mesure (les clients ne sont alors générés qu’une fois par valeur de proba).
probas = []
un = []
deux = []
for k in range(5, 41):
p = k / 100
probas.append(p)
cl = generer_clients(500, p, 6, 42)
un.append(simuler(cl, 1)[0])
deux.append(simuler(cl, 2)[0])
Avec un guichet, l’attente moyenne reste sous \(5\) minutes jusqu’à proba \(= 0{,}23\) (\(4{,}9\) min) et passe à \(7{,}3\) min pour \(0{,}24\) : environ un client toutes les \(4{,}3\) minutes au maximum.
Bilan à rédiger#
Exercice 12 — Ce que j’ai construit #
Rédiger, en une dizaine de lignes, un bilan qui répond aux questions suivantes.
-
Dans chacune des quatre applications, quelle structure a-t-on utilisée (pile ou file) et pourquoi celle-ci plutôt que l’autre ? Répondre par une phrase par application, en utilisant les mots LIFO et FIFO.
-
Citer une situation du TP où l’on a changé l’implémentation sans toucher au programme qui utilise la structure. Qu’est-ce que cela a changé, et qu’est-ce qui n’a pas changé ?
-
Quelle partie a été la plus difficile ? Quel test (ou quelle trace à la main) vous a permis de trouver une erreur ?
Corrigé
Éléments attendus :
-
Pile (LIFO) pour le vérificateur (la dernière parenthèse ou balise ouverte est la première à fermer), pour la calculatrice (les deux derniers nombres calculés sont les opérandes ; les opérateurs en attente ressortent dans l’ordre inverse) et pour Annuler / Rétablir (on annule d’abord la dernière action). File (FIFO) pour le guichet (premier arrivé, premier servi).
-
Partie 1 :
FileListeremplacée parFileChainee; la vitesse change (coût linéaire au lieu de quadratique), les résultats et le code utilisateur ne changent pas. De même, toutes les applications fonctionnent avecPileListeà la place dePileChainee. -
Réponse personnelle ; par exemple un test précis ou une trace (tableau de la gare de triage, tableau du guichet).