Structures de données fondamentales : sac, file d'attente et pile

Structures de données de collection

De nombreux types de données fondamentaux reposent sur la gestion d'une collection d'objets. La valeur du type est un ensemble d'objets, et les opérations consistent à ajouter, supprimer ou accéder à ces objets. Nous examinerons trois structures de données de ce type : le Sac (Bag), la File d'attente (Queue) et la Pile (Stack). Leur différence réside dans l'ordre dans lequel les éléments sont accessibles ou supprimés.

Le type Sac (Bag)

Un sac est une collection qui ne permet pas la suppression d'éléments. L'ordre d'itération n'est pas spécifié et est indépendant de la séquence d'ajout.

API du Sac


public class Bag<Item> implements Iterable<Item>
    Bag()                           // Crée un sac vide
    void add(Item item)             // Ajoute un élément
    boolean isEmpty()               // Vérifie si le sac est vide
    int size()                      // Retourne le nombre d'éléments

La file d'attente (Queue)

L'utilisation d'une file d'attente permet de conserver les éléments dans leur ordre d'arrivée : le premier ajouté est le premier à être retiré (FIFO - First-In, First-Out).

API de la file d'attente


public class Queue<Item> implements Iterable<Item>
    Queue()                         // Crée une file vide
    void enqueue(Item item)         // Ajoute un élément à la fin
    Item dequeue()                  // Retire l'élément ajouté le plus récemment
    boolean isEmpty()               // Vérifie si la file est vide
    int size()                      // Retourne le nombre d'éléments

La pile (Stack)

La pile (ou pile LIFO - Last-In, First-Out) est une collection où le dernier élément ajouté est le premier à être retiré. Un itérateur parcourt les éléments dans l'ordre inverse de leur insertion. Elle est utile pour inverser un ordre de traitement.

API de la pile


public class Stack<Item> implements Iterable<Item>
    Stack()                         // Crée une pile vide
    void push(Item item)            // Ajoute un élément au sommet
    Item pop()                      // Retire et retourne l'élément au sommet
    boolean isEmpty()               // Vérifie si la pile est vide
    int size()                      // Retourne le nombre d'éléments

Exemple d'application : Évaluation d'expression arithmétique

Un algorithme classique utilise deux piles (une pour les opérateurs, une pour les opérandes) pour évaluer une expression parenthésée. L'expression est lue de gauche à droite selon ces règles :

  • Si c'est un opérande (nombre), on l'empile sur la pile des opérandes.
  • Si c'est un opérateur (+, -, *, /, sqrt), on l'empile sur la pile des opérateurs.
  • On ignore les parenthèses ouvrantes '('.
  • À une parenthèse fermante ')', on dépile un opérateur, on dépile le nombre nécessaire d'opérandes, on calcule le résultat et on l'empile sur la pile des opérandes.

Exemple : Évaluer l'expression ( 1 + ( ( 2 + 3 ) * ( 4 * 5 ) ) ).

Implémentation possible de l'évaluateur :


package algorithm;

import stdLib.StdIn;

public class Evaluateur {

    public static void main(String[] args) {
        Stack<String> operateurs = new Stack<>();
        Stack<Double> operandes = new Stack<>();

        while (!StdIn.isEmpty()) {
            String token = StdIn.readString();

            if(token.equals("fin")){
                System.out.println(operandes.pop());
                break;
            }

            if (token.equals("(")) { /* Ignorer */ }
            else if (token.equals("+")) operateurs.push(token);
            else if (token.equals("-")) operateurs.push(token);
            else if (token.equals("*")) operateurs.push(token);
            else if (token.equals("/")) operateurs.push(token);
            else if (token.equals("sqrt")) operateurs.push(token);
            else if (token.equals(")")) {
                String op = operateurs.pop();
                double resultat = operandes.pop();
                if(op.equals("+")) resultat = operandes.pop() + resultat;
                else if(op.equals("-")) resultat = operandes.pop() - resultat;
                else if(op.equals("*")) resultat = operandes.pop() * resultat;
                else if(op.equals("/")) resultat = operandes.pop() / resultat;
                else if(op.equals("sqrt")) resultat = Math.sqrt(resultat);
                operandes.push(resultat);
            } else {
                operandes.push(Double.parseDouble(token));
            }
        }
    }
}

Saisie console :


( 1 + ( ( 2 + 3 ) * ( 4 * 5 ) ) )
fin
101.0

Implémentation d'une pile

Pile à capacité fixe

La conception la plus simple, non générique et non itérable :


package algorithm;

public class PileCapaciteFixe {
    private String[] tableau;
    private int compteur;

    public PileCapaciteFixe(int capacite){
        tableau = new String[capacite];
    }

    public boolean estVide(){
        return compteur == 0;
    }

    public int taille(){
        return compteur;
    }

    public void empiler(String element){
        tableau[compteur++] = element;
    }

    public String depiler(){
        return tableau[--compteur];
    }
}

Pile redimensionnable avec itérateur

Améliorations : générique, tableau dynamique et support de l'itération.


package algorithm;

import java.util.Iterator;

public class PileRedimensionnable<E> implements Iterable<E>{
    private E[] tableau = (E[]) new Object[1];
    private int compteur = 0;

    public boolean estVide(){
        return compteur == 0;
    }

    public int taille(){
        return compteur;
    }

    private void redimensionner(int nouvelleCapacite){
        E[] copie = (E[]) new Object[nouvelleCapacite];
        for(int i=0; i<compteur; i++){
            copie[i] = tableau[i];
        }
        tableau = copie;
    }

    public void empiler(E element){
        if(compteur == tableau.length) redimensionner(2*tableau.length);
        tableau[compteur++] = element;
    }

    public E depiler(){
        E element = tableau[--compteur];
        tableau[compteur] = null; // Aide le ramasse-miettes
        if(compteur > 0 && compteur == tableau.length/4) redimensionner(tableau.length/2);
        return element;
    }

    @Override
    public Iterator<E> iterator() {
        return new IterateurInverse();
    }

    private class IterateurInverse implements Iterator<E>{
        private int position = compteur;

        @Override
        public boolean hasNext() {
            return position > 0;
        }

        @Override
        public E next() {
            return tableau[--position];
        }
    }
}

Cette implémentation offre des performances proches de l'optimal pour les opérations courantes et un espace proportionnel à la taille de la collection. L'inconvénient est que les opérations empiler et depiler qui déclenchent un redimensionnement ont un coût proportionnel à la taille de la pile.

Pile basée sur une liste chaînée

Une alternative qui élimine le coût des redimensionnements.


package algorithm;

public class PileChainee<E>{
    private Noeud sommet;
    private int compteur;

    private class Noeud {
        E donnee;
        Noeud suivant;
    }

    public boolean estVide(){
        return sommet == null;
    }

    public int taille(){
        return compteur;
    }

    public void empiler(E element){
        Noeud ancienSommet = sommet;
        sommet = new Noeud();
        sommet.donnee = element;
        sommet.suivant = ancienSommet;
        compteur++;
    }

    public E depiler(){
        E element = sommet.donnee;
        sommet = sommet.suivant;
        compteur--;
        return element;
    }
}

Cette implémentation en liste chaînée permet de traiter tout type de données, utilise un espace strictement proporitonnel à la taille et garantit des temps d'opération constants.

Implémentation d'une file d'attente par liste chaînée


package algorithm;

public class FileChainee<E> {
    private Noeud tete;
    private Noeud queue;
    private int compteur;

    private class Noeud {
        E donnee;
        Noeud suivant;
    }

    public boolean estVide(){
        return tete == null;
    }

    public int taille(){
        return compteur;
    }

    public void enfiler(E element){
        Noeud ancienneQueue = queue;
        queue = new Noeud();
        queue.donnee = element;
        queue.suivant = null;
        if(estVide()){
            tete = queue;
        } else {
            ancienneQueue.suivant = queue;
        }
        compteur++;
    }

    public E defiler(){
        E element = tete.donnee;
        tete = tete.suivant;
        if(estVide()){
            queue = null;
        }
        compteur--;
        return element;
    }
}

Implémentation d'un sac par liste chaînée

La structure du sac est similaire à celle de la pile, sans opération de suppression. Une méthode ajouter insère au début de la liste chaînée.


package algorithm;

import java.util.Iterator;

public class SacChainee<E> implements Iterable<E> {
    private Noeud tete;

    private class Noeud {
        E donnee;
        Noeud suivant;
    }

    public void ajouter(E element){
        Noeud ancienneTete = tete;
        tete = new Noeud();
        tete.donnee = element;
        tete.suivant = ancienneTete;
    }

    @Override
    public Iterator<E> iterator() {
        return new IterateurListe();
    }

    private class IterateurListe implements Iterator<E>{
        private Noeud courant = tete;

        @Override
        public boolean hasNext() {
            return courant != null;
        }

        @Override
        public E next() {
            E element = courant.donnee;
            courant = courant.suivant;
            return element;
        }
    }
}

Le code itératif peut être adapté aux piles et files chaînées pour les rendre itérables, en appliquant la même logique de parcours de liste. La différence réside dans l'ordre de parcours pour l'utilisateur : LIFO pour la pile, FIFO pour la file.

En résumé, les listes chaînées constituent une alternative fondamentale aux tableaux pour le stockage structuré de données, offrant des compromis performants en espace et en temps pour ces structures de collection.

Étiquettes: structures-de-données Java algorithmes collections pile

Publié le 16 juin à 16h24