Listes, piles, files et deques~: un contrat, plusieurs réalisations
Département de Mathématiques et Informatique, Faculté des Sciences et Techniques, Université Cheikh Anta Diop de Dakar
Ce que la séance construit
Le geste qui ouvre le cours : séparer ce qu’une structure promet (le type abstrait, écrit en interface Java générique) de la manière dont elle tient sa promesse (tableau, nœuds chaînés, tableau circulaire). Chaque réalisation est déroulée sur le code Java du cours, pas à pas, et ses coûts sont comptés.
Iterable, boucle for-each).D’où l’on vient, où l’on va
Amont. Java orienté objet (classes, interfaces, génériques) et l’environnement du TP-1 (Docker, Gradle, JUnit).
Aval. La Séance 2 démontre les coûts annoncés ici (\(\mathcal{O}\), \(\Omega\), \(\Theta\)) ; la Séance 3 range les paires d’une table de hachage dans des nœuds chaînés, la Séance 4 range le tas binaire dans un tableau qui double ; la Séance 5 parcourt les graphes avec la file ArrayDeque du JDK.
Au point Wave de Colobane, le premier arrivé est servi le premier, un nouveau client se place à la fin : la règle se connaît sans savoir si les clients sont debout, assis ou inscrits sur un cahier.
Type abstrait de données (ADT)
Un ensemble de valeurs et d’opérations, spécifiées par leur comportement (ce qu’elles produisent), non par leur programmation.
Structure de données
Une réalisation concrète d’un ADT : une disposition en mémoire et les algorithmes de chaque opération.
Un même ADT admet plusieurs structures : le contrat ne change pas, le coût des opérations change beaucoup. C’est la matière du cours.
Liste<T>Une interface Java énonce les opérations sans aucun corps de méthode : c’est le contrat. Le paramètre <T> est le type des éléments, fixé à l’usage : Liste<String> pour des noms, Liste<Integer> pour des montants.
extends Iterable<T> ajoute au contrat la méthode iterator(), qui permet le parcours par for-each (section 3).
Lecture. Les commentaires font partie du contrat : ajouter(x) écrit en fin, ajouter(i, x) au rang \(i\), de 0 à taille().
Résultat \(\rightarrow\)
Kine Dior 2
Kine Dior 2
Lecture. inscrire ne nomme que l’interface : elle reçoit une ListeTableau puis une ListeChainee, et donne la même sortie. Changer de réalisation ne change pas une ligne du client.
Une confusion fréquente
ArrayList et LinkedList ne sont pas deux ADT, mais deux réalisations de l’interface java.util.List. On déclare le besoin (List<String> l) et l’on ne nomme la classe qu’à la création (new ArrayList<>()).
Compilation \(\rightarrow\)
ExErreur.java:6: error: incompatible types: int cannot be converted to String
noms.ajouter(42); // refuse par le compilateur
^
1 error
Lecture. Le type des éléments est vérifié à la compilation : get rend un String sans conversion, et ajouter(42) est refusé avant toute exécution, avec le numéro de la ligne fautive.
Six affirmations
dequeue() rend l’élément entré le premier.pop() sur une pile vide lève NoSuchElementException.get(i) d’une ListeChainee suit \(i\) références.ajouter(0, x), get(0) rend x.| (a) | (b) | (c) | (d) | (e) | (f) | |
|---|---|---|---|---|---|---|
| contrat | \(\times\) | \(\times\) | \(\times\) | |||
| implémentation | \(\times\) | \(\times\) | \(\times\) |
Correction
ListeTableau<T>Lecture. Les éléments occupent les cases \(0\) à \(\texttt{taille}-1\) : l’élément de rang \(i\) est en case \(i\), à l’adresse « base \(+\;i\times\) taille d’une case », et get y accède en un pas, quel que soit \(i\). Insérer au rang \(i\) décale les \(\texttt{taille}-i\) éléments suivants, du dernier au \(i\)-ième.
Sortie \(\rightarrow\)
[Malick, Yacine, Oumou, Adama, Fallou, null, null, null]
Lecture. agrandir alloue deux fois plus de cases et recopie les taille éléments : une insertion coûte alors taille copies. supprimer(i) décale vers la gauche les éléments qui suivent, puis vide la dernière case pour que le ramasse-miettes récupère l’objet.
ListeTableau<Integer> t = new ListeTableau<>(); // capacite 4
long pire = 0; // copies d'un seul ajout
for (int n = 1; n <= (1 << 20) + 1; n++) {
long avant = t.copies;
t.ajouter(n);
pire = Math.max(pire, t.copies - avant);
if (n == 1024 || n == 1025 || n == 1 << 20 || n == (1 << 20) + 1) {
System.out.printf(Locale.ROOT, "%7d ajouts : %2d agrandissements, "
+ "%7d copies (%.3f par ajout), pire %7d%n",
n, t.agrandissements, t.copies, (double) t.copies / n, pire);
}
}Résultat \(\rightarrow\)
1024 ajouts : 8 agrandissements, 1020 copies (0.996 par ajout), pire 512
1025 ajouts : 9 agrandissements, 2044 copies (1.994 par ajout), pire 1024
1048576 ajouts : 18 agrandissements, 1048572 copies (1.000 par ajout), pire 524288
1048577 ajouts : 19 agrandissements, 2097148 copies (2.000 par ajout), pire 1048576
Lecture. 1 025 ajouts : 2 044 copies, soit \(2n-6\) ; un seul ajout peut en coûter \(n-1\) (pire). Pour 1 048 577 ajouts, 2,000 est l’arrondi de 1,999 994.
Coût amorti (CLRS, § 16.4 ; Sedgewick et Wayne, § 1.4)
Coût amorti : le coût total de \(n\) opérations, divisé par \(n\). Après \(n\geq5\) ajouts, la capacité est passée par \(4, 8, \dots, 2^{j}\), avec \(2^{j-1}<n\) : \(4+8+\dots+2^{j-1}=2^{j}-4\leq2n-6\) copies, moins de 2 par ajout.
Lecture. En ajoutant \(c\) cases à chaque fois, environ \(n^2\)\(/(2c)\) copies : le coût par ajout croît avec \(n\).
Quoi
Les éléments dans des cases contiguës ; quand le tableau est plein, un tableau deux fois plus grand reçoit une copie de tous les éléments.
Pourquoi on l’utilise
Accès par rang en un pas, ajout en fin à coût amorti constant (moins de 2 copies par ajout), une référence par case et rien d’autre. C’est la réalisation de java.util.ArrayList (qui agrandit de moitié : oldCapacity >> 1 dans le code d’OpenJDK 17) et du tas binaire de la Séance 4.
Quand ça échoue
Insertions et retraits en tête : chacun décale tout le tableau. Un ajout isolé peut coûter \(n-1\) copies, gênant quand chaque opération doit être rapide. Juste après un doublement, la moitié des cases est vide.
ListeChainee<T>Lecture. Chaque élément est rangé dans un nœud qui contient aussi une référence au nœud suivant ; les nœuds sont dispersés en mémoire. Atteindre le rang \(i\) oblige à suivre \(i\) références depuis la tête : le compteur parcourus les compte.
public void ajouter(int i, T x) {
verifier(i, taille + 1);
if (i == 0) {
tete = new Noeud<>(x, tete);
if (queue == null) queue = tete;
} else if (i == taille) {
queue.suivant = new Noeud<>(x, null);
queue = queue.suivant;
} else {
Noeud<T> prec = noeud(i - 1);
prec.suivant = new Noeud<>(x, prec.suivant);
}
taille++;
}Lecture. En tête et en fin (grâce à queue), l’insertion écrit deux références sans rien parcourir. Au rang \(i\), elle suit \(i-1\) références jusqu’au nœud précédent, puis le rebranche. supprimer(i) contourne le nœud retiré de la même manière.
Sortie \(\rightarrow\)
Lat
Lecture. Lignes 2, 13 : Sidy, Soda, Gora (nœuds de String, sans référence queue) : \(i=0\), insertion en tête.
Lecture. Lignes 3–4 : le nœud reçoit l’ancienne tête comme suivant, puis tete le désigne.
Lecture. Lignes 2, 6, 14 : ajouter(2, "Lat") : \(i\neq0\) ; prec part de la tête, Mame, au rang 0.
Lecture. Lignes 7–8 : \(k=1\) : prec suit une référence et atteint Sidy, au rang 1.
Lecture. Ligne 7 : \(k=2\) : \(2<2\) est faux : prec est le nœud de rang \(i-1=1\).
Lecture. Ligne 10 : Lat reçoit Soda comme suivant, puis prec.suivant désigne Lat.
Lecture. Ligne 15 : tete.suivant.suivant suit deux références et atteint Lat, au rang 2 ; l’insertion au rang 2 n’en a suivi qu’une (\(i-1\)).
Iterable et for-eachListe<String> l = new ListeChainee<>();
l.ajouter("Daba");
l.ajouter("Mbene");
l.ajouter("Kine");
for (String s : l) {
System.out.print(s + " ");
}
System.out.println();
Iterator<String> it = l.iterator(); // le for-each
while (it.hasNext()) {
System.out.print(it.next().length() + " ");
}
System.out.println();Résultat \(\rightarrow\)
Daba Mbene Kine
4 5 4
Lecture. for-each appelle iterator(), hasNext(), next() : \(n\) références, contre \(n(n-1)/2\) par get(k).
for (int n : new int[] {1000, 4000}) {
ListeTableau<Integer> t = new ListeTableau<>();
ListeChainee<Integer> c = new ListeChainee<>();
for (int k = 0; k < n; k++) {
t.ajouter(0, k); // insertion en tete
c.ajouter(0, k);
}
for (int k = 0; k < n; k++) {
t.get(k); // chaque rang
c.get(k);
}
System.out.println(n + " : tableau " + t.decalages + " decalages, "
+ t.copies + " copies ; chainee " + c.parcourus + " references");
}Résultat \(\rightarrow\)
1000 : tableau 499500 decalages, 1020 copies ; chainee 499500 references
4000 : tableau 7998000 decalages, 4092 copies ; chainee 7998000 references
Lecture. En tête : \(n(n-1)/2\) décalages contre 0 ; get(k) : 0 contre \(n(n-1)/2\) références.
Capacité initiale 2
ListeTableau<>(2) et ListeChainee<>() reçoivent les opérations du tableau, dans l’ordre.
donnees (. : vide).| opération | **donnees** |
déc. | cop. | chaîne | réf. |
|---|---|---|---|---|---|
ajouter(47) |
47 . |
0 | 0 | 47 |
0 |
ajouter(0, 12) |
12 47 |
1 | 0 | 12 47 |
0 |
ajouter(1, 58) |
12 58 47 . |
1 | 2 | 12 58 47 |
0 |
ajouter(33) |
12 58 47 33 |
0 | 0 | 12 58 47 33 |
0 |
supprimer(2) |
12 58 33 . |
1 | 0 | 12 58 33 |
1 |
get(2) |
12 58 33 . |
0 | 0 | 12 58 33 |
2 |
| total | 3 | 2 | 3 |
Correction
ajouter(1, 58) : tableau plein, 4 cases, puis 47 décalé.supprimer(2) : 1 référence ; get(2) : 2.PileTableau<T>Le dernier élément empilé est le premier dépilé (LIFO, last in, first out) ; aucune opération n’atteint le milieu. À la mairie de Rufisque, le dernier dossier posé sur la pile est le premier traité.
private Object[] donnees = new Object[16];
private int taille = 0;
public void push(T x) {
if (taille == donnees.length) agrandir();
donnees[taille] = x; // au sommet
taille++;
}
@SuppressWarnings("unchecked")
public T pop() {
if (estVide()) throw new NoSuchElementException();
taille--;
T sommet = (T) donnees[taille];
donnees[taille] = null;
return sommet;
}Lecture. Le sommet est la case \(\texttt{taille}-1\) : aucun décalage. Avec le sommet en case 0, chaque opération décalerait toute la pile.
Sortie \(\rightarrow\)
true
Lecture. Lignes 2, 15 : l’expression a 17 caractères ; la pile des ouvrantes en attente est vide.
Lecture. Lignes 3–5 : caractère 0, { : ouvrante, empilée ; 1 en attente.
Lecture. Lignes 3–5 : caractère 3, [ : ouvrante, empilée (2 ignorés) ; 2 en attente.
Lecture. Lignes 3–5 : caractère 8, ( : ouvrante, empilée (4 ignorés) ; 3 en attente.
Lecture. Lignes 3, 6–9 : caractère 14, ) : pop() rend (, qui correspond (5 ignorés).
Lecture. Lignes 3, 6–9 : caractère 15, ] : pop() rend [, qui correspond.
Lecture. Lignes 3, 6–9 : caractère 16, } : pop() rend {, qui correspond.
Lecture. Ligne 12 : fin de l’expression : aucune ouvrante en attente, la méthode rend true.
Lecture. Ligne 15 : équilibrée : au plus 3 ouvrantes en attente, la profondeur d’imbrication.
Quatre expressions
estEquilibree du pas à pas précédent.
[(7+2)*3(4*[5+1)]2+3)*(4{[1+2]*(3)}Pour chacune : le verdict, la ligne qui rejette, la hauteur maximale de la pile.
| expression | verdict | rejet | hauteur max. |
|---|---|---|---|
[(7+2)*3 |
false |
ligne 12 : pile non vide | 2 |
(4*[5+1)] |
false |
[ligne 9 : ` et )` |
2 |
2+3)*(4 |
false |
ligne 7 : pile vide | 0 |
{[1+2]*(3)} |
true |
--- | 2 |
Correction
[ : la boucle finit, return pile.estVide() rend false.) dépile [, qui ne lui correspond pas : rejet ligne 9.) arrive sur une pile vide : rejet ligne 7, avant même la lecture de (.
Sortie \(\rightarrow\)
1250
Lecture. Lignes 2, 15 : riz, 3 kg à 450 ; huile, 2 l à 1 200 ; billet de 5 000 FCFA : 9 jetons.
Lecture. Lignes 3–4, 8–9 : 5000 : un nombre, empilé ; 1 opérande en attente.
Lecture. Lignes 3–4, 8–9 : 450 : un nombre, empilé ; 2 opérandes en attente.
Lecture. Lignes 3–4, 8–9 : 3 : un nombre, empilé ; 3 opérandes en attente.
Lecture. Lignes 3–7 : * : b \(=3\), a \(=450\) ; \(450\times3=1350\) est empilé.
Lecture. Lignes 3–4, 8–9 : 1200 : un nombre, empilé ; 3 opérandes en attente.
Lecture. Lignes 3–4, 8–9 : 2 : un nombre, empilé ; 4 opérandes en attente.
Lecture. Lignes 3–7 : * : b \(=2\), a \(=1200\) ; \(1200\times2=2400\) est empilé.
Lecture. Lignes 3–7 : + : b \(=2400\), a \(=1350\) ; \(1350+2400=3750\) est empilé.
Lecture. Lignes 3–7 : - : b \(=3750\), a \(=5000\) ; \(5000-3750=1250\) est empilé.
Lecture. Ligne 12 : le seul élément restant est le résultat : 1 250 FCFA de monnaie.
Lecture. Ligne 15 : au plus 4 opérandes en attente ; un seul parcours, aucune parenthèse.
Résultat \(\rightarrow\)
1200 450 - : 750 ; echanges : -750
1200 450 + : 1650 ; echanges : 1650
6000 4 / : 1500 ; echanges : 0
Lecture. b reçoit le sommet, le second opérande : \(1\,200-450\), et non \(450-1\,200\). L’échange ne change rien pour + et *, commutatifs : un test de Postfixe contient une soustraction ou une division. La division entière donne \(4/6000=0\).
Sur un billet de 2 500 FCFA
4 tickets de bus à 300 FCFA, le reste en pièces de 100 : 2500 300 4 * - 100 /.
| jeton | 2500 |
300 |
4 |
* |
- |
100 |
/ |
|---|---|---|---|---|---|---|---|
| pile | 2500 | 300 2500 |
4 300 2500 |
1200 2500 |
1300 | 100 1300 |
13 |
Correction
- dépile 1 200 puis 2 500 : \(2\,500-1\,200=1\,300\).4.* donne encore 1 200, mais - calcule \(1\,200-2\,500=-1\,300\), puis \(100/(-1\,300)=0\).Premier entré, premier sorti (FIFO, first in, first out). Avec la tête en case 0, chaque dequeue décalerait tous les clients restants : le tableau circulaire déplace l’indice de la tête, pas les éléments.
La fin se calcule : \((\texttt{tete}+\texttt{taille}) \bmod m\), où \(m\) est la capacité ; le modulo ramène la fin, puis la tête, en case 0.
public void enqueue(T x) {
if (taille == donnees.length) agrandir();
int fin = (tete + taille) % donnees.length;
donnees[fin] = x;
taille++;
}
@SuppressWarnings("unchecked")
public T dequeue() {
if (estVide()) throw new NoSuchElementException();
T premier = (T) donnees[tete];
donnees[tete] = null;
tete = (tete + 1) % donnees.length;
taille--;
return premier;
}
Sortie \(\rightarrow\)
Sokhna Assane
Lecture. Lignes 2–3, 13 : Sokhna et Assane en cases 4, 5 ; Thierno en case \((4+2) \bmod 6=0\).
Lecture. Lignes 2–3, 14 : Abdou en case \((4+3) \bmod 6=1\) : cases 4, 5, 0, 1, sans déplacement.
Lecture. Lignes 6, 15 : premier reçoit la case 4 : Sokhna, arrivée la première.
Lecture. Ligne 7 : la case 4 est vidée : plus de référence vers Sokhna.
Lecture. Lignes 8–10 : tete avance en case 5, taille \(=3\) : aucun élément n’a bougé.
Lecture. Lignes 6–10, 15 : Assane sort, case 5 vidée ; tete \(=(5+1) \bmod 6=0\) : la tête boucle.
Lecture. Ligne 15 : Sokhna puis Assane, l’ordre d’arrivée : une case et un indice par opération.
Sortie \(\rightarrow\)
[Arame, Ndiaga, Demba, Samba, Codou, null, null, null]
Lecture. Lignes 1–2, 10 : enqueue("Codou") sur une file pleine : tête en case 2, ordre d’arrivée Arame, Ndiaga, Demba, Samba ; agrandir() alloue 8 cases.
Lecture. Lignes 3–4 : \(k=0\) : la case \((2+0) \bmod 4=2\), Arame, est recopiée en case 0 du nouveau tableau.
Lecture. Lignes 3–4 : \(k=1\) : la case \((2+1) \bmod 4=3\), Ndiaga, est recopiée en case 1.
Lecture. Lignes 3–4 : \(k=2\) : la case \((2+2) \bmod 4=0\), Demba, est recopiée en case 2.
Lecture. Lignes 3–4 : \(k=3\) : la case \((2+3) \bmod 4=1\), Samba, est recopiée en case 3.
Lecture. Lignes 6–7 : le nouveau tableau remplace l’ancien et tete redevient 0 : l’ordre logique coïncide avec l’ordre des cases.
Lecture. Ligne 10 : enqueue reprend : la fin vaut \((0+4) \bmod 8=4\), Codou y est écrit, taille \(=5\).
Lecture. Ligne 11 : 4 copies, dans l’ordre logique : une copie dans l’ordre physique (System.arraycopy(donnees, 0, neuf, 0, taille)) aurait mis Demba en tête de file.
Quoi
Un tableau de \(m\) cases, un indice tete et un compteur taille ; les indices avancent modulo \(m\), et agrandir remet l’anneau à plat dans l’ordre logique.
Pourquoi on l’utilise
enqueue et dequeue écrivent une case et changent un indice : aucun décalage, et des copies en nombre amorti constant, comme pour ListeTableau. C’est la réalisation de java.util.ArrayDeque.
Quand ça échoue
Sans taille, la file vide et la file pleine donnent toutes deux \(\texttt{tete}=\texttt{fin}\) : il faut un compteur, ou sacrifier une case (le choix d’ArrayDeque). Le test tete < fin est faux dès que la file a bouclé. Une recopie dans l’ordre des cases (de 0 à \(\texttt{taille}-1\)) détruit l’ordre de sortie.
Sept cases, tickets 501 à 508
Une FileCirculaire de capacité \(m=7\), avec tete \(=5\) et taille \(=4\) (tickets 501 à 504).
enqueue, puis tete et taille.enqueue(508).tete et taille après deux dequeue.| opération | case écrite | tete |
taille |
capacité |
|---|---|---|---|---|
enqueue(505) |
2 | 5 | 5 | 7 |
enqueue(506) |
3 | 5 | 6 | 7 |
enqueue(507) |
4 | 5 | 7 | 7 |
enqueue(508) |
7 | 0 | 8 | 14 |
dequeue() \(\times2\) |
--- | 2 | 6 | 14 |
Correction
tete \(=0\), 508 en case 7.tete \(=2\), taille \(=6\) : 501 et 502 sont sortis.Un deque (double-ended queue) ajoute et retire aux deux bouts. Restreint à addLast et removeLast, c’est une pile ; à addLast et removeFirst, une file.
Lecture. addFirst recule la tête, addLast écrit après la fin : chaque opération touche une case et au plus un indice, hors agrandissement.
public class ExModulo {
public static void main(String[] args) {
System.out.println((3 - 1) % 8 + " " + (0 - 1) % 8);
System.out.println((0 - 1 + 8) % 8 + " " + Math.floorMod(0 - 1, 8));
String[] donnees = new String[8];
int tete = (0 - 1) % donnees.length; // sans le + donnees.length
donnees[tete] = "Daba";
}
}Résultat \(\rightarrow\)
2 -1
7 7
Erreur \(\rightarrow\)
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index -1 out of bounds for length 8
at ExModulo.main(ExModulo.java:7)
Lecture. % est un reste, du signe du dividende : \((0-1)\,\%\,8=-1\), d’où l’exception. Ajouter \(m\) ramène l’indice entre 0 et \(m-1\) ; Math.floorMod rend le modulo.
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Queue;
Queue<String> guichet = new ArrayDeque<>();
guichet.add("Daba");
guichet.add("Mbene");
guichet.add("Kine");
System.out.println(guichet.remove() + " " + guichet);
Deque<String> dossiers = new ArrayDeque<>();
dossiers.push("acte 1");
dossiers.push("acte 2");
dossiers.push("acte 3");
System.out.println(dossiers.pop() + " " + dossiers);Résultat \(\rightarrow\)
Daba [Mbene, Kine]
acte 3 [acte 2, acte 1]
Lecture. java.util.ArrayDeque, tableau circulaire, réalise Queue et Deque : add en queue, remove en tête ; push est addFirst, pop est removeFirst : le sommet est en tête. La documentation de Stack renvoie à Deque.
ArrayDeque, capacité 8, vide
Les opérations du tableau, dans l’ordre (tete \(=0\) au départ).
tete et taille après les trois ajouts.tete après addFirst(24) si l’on oublie + donnees.length.| opération | case | tete |
taille |
rend |
|---|---|---|---|---|
addLast(61) |
0 | 0 | 1 | --- |
addFirst(24) |
7 | 7 | 2 | --- |
addFirst(85) |
6 | 6 | 3 | --- |
removeLast() |
0 | 6 | 2 | 61 |
addLast(39) |
0 | 6 | 3 | --- |
removeFirst() |
6 | 7 | 2 | 85 |
Correction
addFirst(24) : \((0-1+8) \bmod 8=7\) ; puis 6 pour 85.ArrayIndexOutOfBoundsException à l’écriture.| besoin | ADT | réalisation courante |
|---|---|---|
| accès par rang, ajout en fin | List | tableau redimensionnable |
| insertions et retraits en tête | List | nœuds chaînés |
| annuler, revenir en arrière | Stack | tableau, sommet en fin |
| servir dans l’ordre d’arrivée | Queue | tableau circulaire |
| ajouter et retirer aux deux bouts | Deque | tableau circulaire |
Lecture. Un historique où l’on consulte le plus récent et d’où l’on purge le plus ancien, une fois la capacité atteinte, est un deque : on écrit d’un côté, on retire de l’autre.
for (int n : new int[] {1000, 4000}) {
ListeTableau<Integer> l = new ListeTableau<>();
FileCirculaire<Integer> f = new FileCirculaire<>();
for (int k = 0; k < n; k++) {
l.ajouter(k); // enqueue : en fin
f.enqueue(k);
}
for (int k = 0; k < n; k++) {
l.supprimer(0); // dequeue : tout se decale
f.dequeue();
}
System.out.println(n + " clients : liste " + l.decalages
+ " decalages ; file circulaire " + f.copies + " copies");
}Résultat \(\rightarrow\)
1000 clients : liste 499500 decalages ; file circulaire 1016 copies
4000 clients : liste 7998000 decalages ; file circulaire 4088 copies
Lecture. Contrat respecté, mais \(n(n-1)/2\) décalages pour \(n\) clients : correct n’est pas efficace.
| opération | tableau redimensionnable | nœuds chaînés | tableau circulaire |
|---|---|---|---|
| lecture au rang \(i\) | 1 accès | \(i\) références | 1 accès, case \((\texttt{tete}+i) \bmod m\) |
| ajout en fin | moins de 2 copies (amorti) | 2 références (queue) |
moins de 2 copies (amorti) |
| ajout en tête | \(n\) décalages | 2 références | 1 case (addFirst) |
| retrait en tête | \(n-1\) décalages | 1 référence | 1 case (dequeue) |
| mémoire | \(n\) à \(2n\) cases (ajouts seuls) | \(n\) nœuds (valeur, référence) | \(n\) à \(2n\) cases (ajouts seuls) |
Lecture. Mesures de la séance : \(n=1\,000\) insertions en tête, 499 500 décalages sur le tableau, aucun sur la liste chaînée ; \(n\) lectures get(k), 499 500 références suivies sur la liste chaînée, aucune sur le tableau ; 1 000 clients servis par supprimer(0), 499 500 décalages, aucun par la file circulaire.
À retenir
interface générique, et l’on déclare avec l’interface.Iterable et Iterator : un parcours complet en \(n\) pas, par la boucle for-each.b reçoit le sommet.(i - 1 + m) % m pour reculer, taille pour distinguer vide et plein, agrandissement dans l’ordre logique.Séance 2
Cette séance a dit « en un pas », « \(n\) décalages », « moins de 2 copies par ajout » sur des comptes mesurés. La Séance 2 compte les opérations élémentaires d’un algorithme, définit \(\mathcal{O}\), \(\Omega\) et \(\Theta\), distingue le meilleur cas, le pire cas et le cas moyen, et résout les récurrences des algorithmes récursifs, jusqu’au théorème maître : les coûts de cette séance deviennent des énoncés démontrés.
java.util.ArrayList, java.util.ArrayDeque, java.util.Stack (code source).Structures de données et algorithmes avancés — Séance 1 Dr. El Hadji Bassirou TOURÉ • DMI/FST/UCAD
Lecture. Lignes 2, 10 :
ajouter(1, "Oumou")sur la liste Yacine, Adama, Fallou :taille\(=3\), capacité 4, la case 3 est libre ; pas d’agrandissement.Lecture. Lignes 3–4 : \(k=3\) :
donnees[3] = donnees[2]; Fallou passe de la case 2 à la case 3.Lecture. Lignes 3–4 : \(k=2\) :
donnees[2] = donnees[1]; Adama passe de la case 1 à la case 2.Lecture. Lignes 3, 6–7 : \(k=1\) : la boucle s’arrête ; Oumou est écrit en case 1 et
taillepasse à 4 : le tableau est plein. Deux décalages.Lecture. Lignes 2, 11 :
ajouter(0, "Malick"):taille\(=\)donnees.length\(=4\) :agrandir()alloue 8 cases et y recopie les 4 éléments.Lecture. Lignes 3–4 : \(k=4\) : Fallou passe de la case 3 à la case 4.
Lecture. Lignes 3–4 : \(k=3\) : Adama passe de la case 2 à la case 3.
Lecture. Lignes 3–4 : \(k=2\) : Oumou passe de la case 1 à la case 2.
Lecture. Lignes 3–4 : \(k=1\) : Yacine passe de la case 0 à la case 1.
Lecture. Lignes 3, 6–7 : \(k=0\) : la boucle s’arrête ; Malick est écrit en case 0,
taille\(=5\). Quatre décalages pour une insertion en tête.Lecture. Ligne 12 : deux insertions : 6 décalages et 4 copies. Insérer au rang \(i\) décale les \(\texttt{taille}-i\) éléments qui suivent.