Aller au contenu principal

Listes, piles et files

TAD et structures de données45 min

Tableaux, maillons, LIFO, FIFO et buffer circulaire.

Dans cette leçon

Une liste dans un tableau fixe

Une liste est une suite ordonnée. Une pile et une file imposent en plus une règle sur l'ordre des sorties. Le contrat ne choisit toujours pas le rangement : tableau, nœuds et buffer circulaire peuvent chacun porter une partie de ces comportements.

Commençons par un tableau de quatre cases. La taille compte les valeurs utiles. La capacité compte les cases réservées.

size = 3capacity = 41 case réservée

La case d'index 3 existe déjà, mais elle n'appartient pas encore à la liste. get(3) doit donc être refusé. La capacité décrit la mémoire disponible, pas les index logiques autorisés.

Tableau fixe

  • Accès à une position par calcul d'adresse.
  • Cases compactes et voisines.

Quand il est plein

  • Aucune case suivante n'appartient au bloc.
  • Insérer au milieu décale les valeurs suivantes.

Tableau dynamique

  • Réserver un bloc plus grand ailleurs.
  • Recopier les valeurs puis continuer.

Grandir sans changer le contrat

Un tableau dynamique garde le même accès par index, mais remplace son bloc quand la capacité manque. Dans cet exemple pédagogique, la nouvelle capacité est le double de l'ancienne.

ArrayList.java

Avant la croissance

Ancien bloc à @256. Il devient un bloc fantôme pendant la copie.

Après la croissance

Nouveau bloc à @512. Il n'est pas une extension du bloc @256.

  1. Allouer ailleurs

    Quand size == values.length, un nouveau tableau deux fois plus grand est créé à un autre endroit.
  2. Copier les valeurs

    Chaque valeur logique est lue dans l'ancien bloc puis écrite dans le nouveau.
  3. Changer la référence

    Le champ values désigne le nouveau bloc. L'ancien bloc n'est plus utilisé.
  4. Effectuer l'ajout

    La nouvelle valeur est écrite à l'ancienne position de size, puis la taille augmente.
add avec place : 1 écritureadd quand plein : allocation + copiesremove(0) : size - 1 décalages

Pour un appel isolé, add sur un tableau plein demande même plus de travail que remove(0) : il faut allouer, copier toutes les valeurs, puis écrire la nouvelle. La différence apparaît dans la répétition. En doublant la capacité, la croissance ne revient qu'occasionnellement, tandis que remove(0)décale les valeurs à chaque appel.

La croissance ne corrige pas les décalages

Un ArrayList peut grandir, mais il reste contigu. Retirer l'index 0 oblige toujours chaque valeur restante à glisser d'une case.

Rendre de la mémoire sans s'agiter

Après beaucoup de retraits, la taille peut devenir bien plus petite que la capacité. Java garde cette marge. Un ArrayList ne rend pas sa mémoire tout seul.

trimToSize.java

trimToSize() ne raccourcit pas le bloc en place. Il crée un bloc d'exactement size cases, copie les valeurs et remplace la référence. Rendre la marge a donc un prix immédiat.

Le piège du seuil naïf

Imaginez une capacité de 8 et une taille qui passe de 5 à 4. Un shrink vers 4 cases copie les valeurs. L'ajout suivant remplit déjà le bloc et provoque une croissance vers 8. Un nouveau retrait revient à 4. Cette alternance de copies s'appelle le thrashing.
ShrinkPolicies.java

Hystérésis

  • • La croissance et le shrink utilisent des seuils séparés.
  • • Une marge reste après le shrink.
  • • Une petite oscillation de size ne recopie pas tout.

Même frontière

  • • Le bloc réduit est immédiatement plein.
  • • Un add puis un remove alternent croissance et shrink.
  • • Chaque petite opération déclenche une grande copie.

Le choix de Java

Garder la capacité rend les prochains ajouts prévisibles et évite les copies répétées. Appelez trimToSize() seulement lorsque vous avez une vraie raison de rendre la marge.

Remplacer les cases par des liens

Une liste chaînée remplace le grand bloc par des nœuds. Chaque nœud garde une valeur et la référence next. La liste conserve head, tail et size.

LinkedList.java

Ajouter à la fin

tail désigne déjà le dernier nœud. Il suffit de relier son next au nouveau nœud, puis de déplacer tail.

Lire l'index i

Il n'existe aucune formule pour sauter au nœud i. La boucle part de head et suit i références.

L'invariant du dernier retrait

Quand removeFirst() retire l'unique nœud, head devient null. Il faut aussi remettre tail à null, sinon tail désigne un nœud qui n'appartient plus à la liste.

Curseur et double chaînage

Une insertion liée est locale seulement si vous connaissez déjà le bon nœud. Si vous partez de head pour le chercher, le parcours reste à payer.

  1. Parcourir une fois

    Un curseur garde la position actuelle et les références utiles autour d'elle.
  2. Modifier localement

    Une insertion près du curseur change quelques liens, sans repartir de head.
  3. Continuer depuis cette position

    En Java, ListIterator formalise ce principe. Il ne transforme pas la liste en accès direct par index.
DoublyLinkedNode.java

Le double chaînage ajoute previous. Vous pouvez alors avancer et reculer localement. En échange, chaque nœud prend une référence de plus et chaque modification doit maintenir les deux sens.

Le curseur aide

  • • Vous êtes déjà positionné au bon endroit.
  • • Vous enchaînez plusieurs modifications voisines.
  • • Vous devez avancer et reculer localement.

Le curseur n'aide pas

  • • Vous demandez sans cesse un index arbitraire.
  • • Vous recréez le curseur depuis head à chaque opération.
  • • Vous oubliez de mettre à jour previous ou next.

La pile travaille du même côté

Une pile respecte LIFO. Le cours utilise put, take et peek. Les noms standards push et pop décrivent les mêmes gestes.

sommet : index size - 1put : écrit à droitetake : retire à droite
ArrayStack.java

Pourquoi le tableau convient

  • Toutes les opérations portent sur la fin logique.
  • Aucun voisin ne doit combler un trou.

Ce que la pile interdit

  • Retirer arbitrairement au milieu.
  • Consulter une position quelconque par son contrat.

Ce que gagne le client

  • Une règle LIFO impossible à contourner.
  • Un cas vide traité explicitement.

Le bon côté du tableau

Une pile sur tableau place son sommet à la fin. Le placer à l'index 0 provoquerait des décalages à chaque opération sans apporter de gain au contrat.

Trois façons de construire une file

Une file respecte FIFO. On ajoute derrière et on retire devant. Les trois implémentations suivantes donnent le même ordre logique, mais pas les mêmes gestes en mémoire.

ArrayList

  • put ajoute simplement à la fin.
  • Les valeurs restent dans un bloc compact.

Le retrait en tête

  • take appelle remove(0).
  • Toutes les valeurs restantes se décalent.

Deux réponses

  • Une file liée avec head et tail.
  • Un tableau dont les indices tournent.
QueueArrayList.java
5 valeurstake : 4 décalagesordre FIFO correct

La file sur ArrayList n'est pas fausse. Elle retourne les valeurs dans le bon ordre. Son défaut est le travail caché de remove(0).

QueueLinkedList.java

Deux extrémités, deux responsabilités

head est le prochain élément à sortir. tail reçoit le prochain élément. Ajouter et retirer changent quelques références, sans déplacer les autres valeurs.

Faire tourner les indices

Le buffer circulaire conserve le tableau compact sans fermer le trou après chaque retrait. head avance vers le plus ancien élément et tail vers la prochaine case d'écriture. Arrivés au bout, les deux reviennent à l'index 0.

head = 1tail = 1size = 4

L'ordre physique est 50, 20, 30, 40. Depuis head, l'ordre FIFO est 20, 30, 40, 50. head et tail sont égaux parce que la file est pleine.

CircularQueue.java
  1. Écrire à tail

    put place la valeur dans elements[tail], puis avance tail avec le modulo.
  2. Lire à head

    take conserve la valeur de elements[head], puis avance head. Aucune autre case ne bouge.
  3. Traduire un index logique

    L'index logique i se trouve à la position physique (head + i) % elements.length.
  4. Distinguer vide et plein

    Quand head égale tail, size indique s'il reste zéro valeur ou si toutes les cases sont occupées.

Quatre invariants à vérifier

  • 0 ≤ size ≤ elements.length
  • head désigne le prochain élément à sortir.
  • tail désigne la prochaine case d'écriture.
  • Chaque avance reboucle avec le modulo.

Choisir selon les opérations

Il n'existe pas de gagnant absolu. Partez de l'opération qui revient souvent, puis regardez le geste réel.

ReprésentationAccès par indexAjout et retrait utilesMémoire et piège
Tableau dynamique1 accèsFin : une écriture le plus souvent. Début : décalages.Bloc compact avec capacité libre; copie rare lors d'une croissance.
Liste simplement chaînéei + 1 nœuds lusTête locale; fin locale si tail existe.Une allocation et une référence par valeur; recherche préalable souvent oubliée.
Liste doublement chaînéeSuit toujours les liens.Modification locale dans les deux sens avec un curseur.Deux références par nœud et plus de liens à maintenir.
Buffer circulaire1 calcul + 1 accèsput à tail et take à head, sans décalage.Capacité bornée; modulo et distinction vide/plein indispensables.

Questions utiles

  • • Avez-vous besoin d'un index arbitraire ?
  • • Modifiez-vous surtout au début, à la fin ou près d'un curseur ?
  • • La capacité peut-elle être bornée ?
  • • Quel cas vide ou plein devez-vous garantir ?

Mauvais raccourcis

  • • « LinkedList est toujours meilleure pour insérer. »
  • • « ArrayList grandit, donc remove(0) ne coûte rien. »
  • • « head égale tail signifie forcément vide. »
  • • « Une valeur encore visible dans une case reste logique. »

Vérifiez vos réflexes

Vérifiez vos réflexes

Une réponse par question. La correction apparaît tout de suite.

1. Un ArrayList a une taille logique de 3 et une capacité de 8. Vous retirez une valeur sans appeler trimToSize. Quelle est sa capacité ensuite ?
2. Une file stockée dans un ArrayList contient 5 valeurs. Combien de valeurs bougent quand take utilise remove(0) ?
3. Pourquoi le champ tail rend-il l'ajout final d'une liste chaînée local ?
4. Que retourne la séquence put(2), put(5), take() sur une pile ?
5. Buffer de capacité 5: head=3, tail=1, size=3. Où put(9) écrit-il, et quelle devient la valeur de tail ?
6. Pourquoi une file circulaire garde-t-elle size en plus de head et tail ?

Écrivez une file circulaire

Implémentez CircularQueue

Écrivez d'abord. Une IA externe pourra ensuite relire votre travail. Son retour peut être faux. Vérifiez-le avec la leçon.

Implémentez une CircularQueue avec : - un int[] elements, head, tail et size; - un constructeur qui reçoit la capacité; - put, take, peek et isEmpty; - IllegalStateException si put est appelé sur une file pleine, ou take/peek sur une file vide; - aucun décalage et aucune collection Java. Tracez ensuite cette suite pour une capacité de 4 : put(10), put(20), put(30), take(), put(40), put(50). Donnez la valeur retirée, l'ordre logique final, puis head, tail et size. Expliquez en une phrase pourquoi head et tail ont alors la même valeur.

Brouillon enregistré sur cet appareil.

À coller dans l'IA de votre choix.
Voir le prompt qui sera copié
Vous corrigez le travail d'un étudiant qui prépare une remédiation Java 17. Évaluez d'abord son raisonnement, ses invariants et la cohérence de son brouillon. Un code incomplet mais bien expliqué mérite un retour utile. Signalez précisément les erreurs sans inventer d'exigences hors sujet.

Cette leçon évalue une file circulaire bornée. Vérifiez le rôle exact de head, tail et size, le modulo, les cas vide et plein, et l'absence de décalage. Ne remplacez pas le tableau par une collection Java.

Travail demandé:
Implémentez une CircularQueue avec :
- un int[] elements, head, tail et size;
- un constructeur qui reçoit la capacité;
- put, take, peek et isEmpty;
- IllegalStateException si put est appelé sur une file pleine, ou take/peek sur une file vide;
- aucun décalage et aucune collection Java.

Tracez ensuite cette suite pour une capacité de 4 :
put(10), put(20), put(30), take(), put(40), put(50).

Donnez la valeur retirée, l'ordre logique final, puis head, tail et size. Expliquez en une phrase pourquoi head et tail ont alors la même valeur.

Commencez par poser au plus deux questions si une intention reste ambiguë. Donnez ensuite les points corrects, les problèmes précis et la plus petite prochaine étape. Ne fournissez une solution complète que si l'étudiant la demande explicitement.

Réponse de l'étudiant:
[aucune réponse collée]

Continuez le parcours

Centre de commandes

Rechercher une page et s'y rendre