Practical activity

Duration2h30

Présentation & objectifs

Cette activité est dédiée à la manipulation des structures de données et à observer empiriquement l’impact du choix d’une structure par rapport à une autre.

Important

Le but de cette séance est de vous aider à maîtriser des notions importantes en informatique. Un assistant de programmation intelligent tel que GitHub Copilot, que vous avez peut-être déjà installé, pourra vous fournir une solution à ces exercices uniquement à partir d’un nom de fichier judicieusement choisi.

Pour des raisons de formation, nous vous conseillons de désactiver d’abord ces outils.

À la fin de l’activité pratique, nous vous suggérons de retravailler l’exercice avec ces outils activés. Suivre ces deux étapes améliorera vos compétences à la fois fondamentalement et pratiquement.

De plus, nous vous fournissons les solutions des exercices. Assurez-vous de ne les consulter qu’après avoir trouvé une solution aux exercices, à des fins de comparaison ! Même si vous êtes sûr que votre solution est correcte, veuillez les consulter, car elles fournissent parfois des éléments supplémentaires que vous auriez pu manquer.

Contenu de l’activité

1 — Implémentation d’une pile basée sur une liste

Dans un fichier nommé list_stack.py, donnez une implémentation d’une pile utilisant la liste python intégrée pour stocker les données. Voici les primitives qui doivent être implémentées.

  • initialize – Initialise la pile.
  • push – Ajoute un élément au sommet de la pile.
  • pop – Retire l’élément au sommet de la pile.
  • is_empty – Renvoie true si la pile est vide, et false sinon.

Ensuite, dans un second fichier nommé test_stack.py, effectuez quelques opérations sur une pile pour vérifier ses primitives.

Correction
"""
    This module provides an implementation of the primitives to handle a stack of values. 
    The stack is materialized by means of a list with a size limit to simulate an array.
"""

# Needed imports
from typing import List,Any

# define your own custom type
Stack = List[Any]



def initialize () -> Stack:

    """
        Initializes an empty stack.
        In:
            * None.
        Out:
            * An empty stack.
    """

    # We use lists to represent stacks
    return []



def is_empty (stack: Stack) -> bool:

    """
        This function checks if the stack is empty.
        In:
            * stack: Stack to check.
        Out:
            * True if the stack is empty, False otherwise.
    """

    # Check list length
    return len(stack) == 0



def push (stack: Stack, item: Any) -> None:

    """
        This function adds an element to the stack.
        The element is added at the last position of the storage structure.
        In:
            * stack: Stack to modify.
            * item:  Element to add.
    """

    # Add the element to the end of the list
    stack.append(item)



def pop (stack: Stack) -> Any:
    
    """
        This function removes the next element of the stack.
        In:
            * stack: Stack to modify.
        Out:
            * The next element of the stack.
    """
    
    # Extract and return the last element of the list
    return stack.pop()
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

public class ListStack {
    private List<Object> stack;

    //The constructeur replace the initialise primitive
    public ListStack() {
        this.stack = new ArrayList<>();
    }

    public boolean isEmpty() {
        /**
         * This function checks if the stack is empty.
         * @return True if the stack is empty, False otherwise.
         */
        return this.stack.isEmpty();
    }

    public void push(Object item) {
        /**
         * This function adds an element to the stack.
         * The element is added at the end.
         * @param item Element to add.
         */
        this.stack.add(item);
    }

    public Object pop() {
        /**
         * This function removes the last element of the stack.
         * @return The last element of the stack.
         */
        if (this.stack.isEmpty()) {
            throw new IllegalStateException("Stack is empty");
        }
        return this.stack.remove(this.stack.size() - 1);
    }
}

2 — Calculer une expression arithmétique postfixée

Dans cet exercice, nous allons utiliser une pile pour calculer une expression arithmétique en notation postfixée, c’est-à-dire où chaque opérateur est précédé de ses opérandes, comme 3 12 3 - 3 / 1 - *. L’expression infixe équivalente en utilisant des parenthèses est 3 * (((12 - 3) / 3) - 1).

Dans le fichier test_stack.py, écrivez une fonction evaluate_postfix qui calcule le résultat d’une expression arithmétique postfixée entière. La fonction prend en entrée une chaîne contenant une expression arithmétique et renvoie son résultat final.

  • Le résultat du calcul sera un nombre réel.
  • L’entrée est une séquence de caractères.
  • Chaque donnée est séparée des autres par un espace.
  • La méthode consiste à utiliser une pile pour stocker les valeurs numériques au fur et à mesure qu’elles sont lues, et à effectuer une opération de traitement lorsqu’un opérateur est lu.
  • Cette opération de traitement retire les deux dernières valeurs de la pile, effectue l’opération, puis pousse le résultat.
  • Le résultat final est stocké dans la pile.
  • Les opérateurs autorisés sont +, -, *, /.

On considère pour le moment que l’expression est correctement écrite et que seuls des entiers sont utilisés.

Information

Vous aurez besoin d’utiliser différents outils de programmation pour implémenter cette fonction.

  • pour découper une chaîne selon un séparateur "3 2 * 25 +".split(" ") découpera la chaîne en utilisant un espace comme délimiteur.
  • pour vérifier si une chaîne peut être traduite en int "3".isnumeric() renvoie vrai alors que "afea".isnumeric() renvoie faux.
  • pour traduire une chaîne en entier int("34") renvoie 34.

Modifiez le point d’entrée de votre fichier test_stack.py pour demander la saisie d’une expression puis afficher le résultat de son évaluation.

Correction
def evaluate_postfix (expression: str) -> float:

    """
        This function uses a stack to evaluate a postfix expression.
        The function takes a string as input, which is a postfix expression.
        The function returns the result of the expression.
        In:
            * expression: Expression to be evaluated.
        Out:
            * Evaluated of the evaluated expression.
    """

    # Create an empty stack
    stack : Stack = initialize()

    # Iterate over the expression
    split_expression = expression.split(" ")
    for i in split_expression:

        # If the character is a number, push it to the stack
        if i.isnumeric():
            push(stack, int(i))

        # If the character is an operator, apply it
        else:

            # Pop the last two elements from the stack
            a = pop(stack)
            b = pop(stack)

            # Apply the operator
            if i == '+':
                push(stack, b + a)
            elif i == '-':
                push(stack, b - a)
            elif i == '*':
                push(stack, b * a)
            elif i == '/':
                push(stack, b / a)
    
    # Return the result
    return pop(stack)
/**
 * To run this code, you need to have Java installed on your computer, then:
 * - Create a file named `Main.java` in a directory of your choice.
 * - Copy this code in the file.
 * - Open a terminal in the directory where the file is located.
 * - Run the command `javac Main.java` to compile the code.
 * - Run the command `java -ea Main` to execute the compiled code.
 * Note: '-ea' is an option to enable assertions in Java.
 */

// Needed imports
import java.util.Stack;

public class Main {

    /**
     * This function uses a stack to evaluate a postfix expression.
     * The function takes a string as input, which is a postfix expression.
     * The function returns the result of the expression.
     *
     * @param expression Expression to be evaluated.
     * @return Evaluated result of the postfix expression.
     */
    public static double evaluatePostfix(String expression) {
        // Create an empty stack
        Stack<Double> stack = new Stack<>();

        // Iterate over the expression
        String[] splitExpression = expression.split(" ");
        for (String i : splitExpression) {

            // If the character is a number, push it to the stack
            if (i.matches("-?\\d+(\\.\\d+)?")) {
                stack.push(Double.parseDouble(i));
            }

            // If the character is an operator, apply it
            else {

                // Pop the last two elements from the stack
                double a = stack.pop();
                double b = stack.pop();

                // Apply the operator
                switch (i) {
                    case "+":
                        stack.push(b + a);
                        break;
                    case "-":
                        stack.push(b - a);
                        break;
                    case "*":
                        stack.push(b * a);
                        break;
                    case "/":
                        stack.push(b / a);
                        break;
                    default:
                        throw new IllegalArgumentException("Invalid operator: " + i);
                }
            }
        }

        // Return the result
        return stack.pop();
    }
}

3 — Comparer différentes implémentations de files

3.1 — Utilisation d’une liste intégrée

Comme vous avez défini une implémentation d’une pile utilisant le type liste intégré (fichier list_stack.py), créez un fichier nommé list_queue.py qui implémente les primitives d’une file en utilisant une liste.

Rappel des primitives :

  • initialize – Initialise la file.
  • push – Ajoute un élément à la fin de la file.
  • pop – Retire le premier élément de la file.
  • is_empty – Renvoie true si la file est vide, et false sinon.
Correction
"""
    This module provides an implementation of the primitives to handle a queue of values. 
    The queue is materialized by means of a list with a size limit to simulate an array.

"""

# Needed imports
from typing import List,Any

# Define your own custom type
Queue = List[Any]



def initialize() -> Queue:

    """
        Initializes an empty queue.
        In:
            * None.
        Out:
            * An empty queue.
    """

    # We use lists to represent queues
    return []



def is_empty (queue: Queue) -> bool:

    """
        This function checks if the queue is empty.
        In:
            * queue: Queue to check.
        Out:
            * True if the queue is empty, False otherwise.
    """

    # Check list length
    return len(queue) == 0



def push (queue: Queue, item: Any) -> None:

    """
        This function adds an element to the queue.
        The element is added at the last position of the storage structure.
        In:
            * queue: Queue to modify.
            * item:  Element to add.
    """

    # Add the element to the end of the list
    queue.append(item)



def pop (queue: Queue) -> Any:
    
    """
        This function removes the next element of the queue.
        In:
            * queue: Queue to modify.
        Out:
            * The next element of the queue.
    """
    
    # Extract and return the first element of the list
    return queue.pop(0)
import java.util.ArrayList;
import java.util.List;

public class ListQueue {
    private List<Object> queue;

    public ListQueue() {
        this.queue = new ArrayList<>();
    }

    public boolean isEmpty() {
        /**
         * This function checks if the queue is empty.
         * @return True if the queue is empty, False otherwise.
         */
        return this.queue.isEmpty();
    }

    public void push(Object item) {
        /**
         * This function adds an element to the queue.
         * The element is added at the end.
         * @param item Element to add.
         */
        this.queue.add(item);
    }

    public Object pop() {
        /**
         * This function removes the last element of the queue.
         * @return The removed element.
         */
        if (this.queue.isEmpty()) {
            throw new IllegalStateException("Queue is empty");
        }
        return this.queue.remove(0);
    }
}

3.2 — Utilisation d’une liste doublement chaînée

La liste doublement chaînée est une structure de données donnant un accès au premier et dernier élément d’une liste. Écrivez une implémentation des primitives pour gérer une liste doublement chaînée dans un fichier dll_queue.py.

Rappel des primitives nécessaires pour cette séance pratique (il peut y en avoir d’autres) :

  • initialize – Initialise une liste vide.
  • insert_at_end – Ajoute un élément à la fin de la liste.
  • pop_from_beginning – Renvoie et retire l’élément en tête de la liste.
  • is_empty – Renvoie true si la liste ne contient aucune donnée.

Testez ensuite votre implémentation par exemple en enfilant quelques éléments puis en les défilant.

Correction
"""
    This module provides functions to manipulate a doubly-linked list.
    The list is represented as a dictionary with 'head' and 'tail' keys pointing to the first and last elements, respectively.
    Each element is also represented as a dictionary with 'data', 'prev', and 'next' keys.
"""

# Needed imports
from typing import Any, Dict, Optional

# Create your own custom type
Element = Dict[str, Any]



def create_element (data: Any) -> Element:

    """
        Creates a new element with the given data.
        In:
            * data: The data to store in the element.
        Out:
            * A dictionary representing the new element.
    """

    # Return the new element
    return {'data': data, 'prev': None, 'next': None}



def initialize () -> Dict[str, Optional[Element]]:

    """
        Initializes an empty doubly-linked list.
        In:
            * None.
        Out:
            * A dictionary representing the doubly-linked list, with 'head' and 'tail' keys.
    """

    # Initialize the default pointers
    return {'head': None, 'tail': None}



def is_empty (dll: Dict[str, Optional[Element]]) -> bool:

    """
        Checks if the doubly-linked list is empty.
        In:
            * dll: The doubly-linked list.
        Out:
            * True if the list is empty, False otherwise.
    """

    # Check if the head pointer is uninitialized
    return dll['head'] is None



def insert_at_beginning (dll: Dict[str, Optional[Element]], data: Any) -> None:
    
    """
        Insert a new element with the given data at the beginning of the list.
        In:
            * dll:  The doubly-linked list.
            * data: The data to store in the new element.
        Out:
            * None.
    """

    # Create the element to store
    new_element = create_element(data)

    # Adapt the pointers
    if dll['head'] is None:
        dll['head'] = new_element
        dll['tail'] = new_element
    else:
        new_element['next'] = dll['head']
        dll['head']['prev'] = new_element
        dll['head'] = new_element



def insert_at_end (dll: Dict[str, Optional[Element]], data: Any) -> None:
    
    """
        Insert a new element with the given data at the end of the list.
        In:
            * dll:  The doubly-linked list.
            * data: The data to store in the new element.
        Out:
            * None.
    """

    # Create the element to store
    new_element = create_element(data)

    # Adapt the pointers
    if dll['tail'] is None:
        dll['head'] = new_element
        dll['tail'] = new_element
    else:
        new_element['prev'] = dll['tail']
        dll['tail']['next'] = new_element
        dll['tail'] = new_element



def pop_from_beginning (dll: Dict[str, Optional[Element]]) -> Any:
    
    """
        Removes and returns the data of the element at the beginning of the list.
        In:
            * dll: The doubly-linked list.
        Out:
            * The data of the element removed from the beginning of the list.
    """

    # Verify it is not empty
    if is_empty(dll):
        return None
    
    # Extract the data
    data = dll['head']['data']

    # Adapt the pointers
    if dll['head'] == dll['tail']:
        dll['head'] = None
        dll['tail'] = None
    else:
        dll['head'] = dll['head']['next']
        dll['head']['prev'] = None
    
    # Return the data
    return data



def pop_from_end (dll: Dict[str, Optional[Element]]) -> Any:

    """
        Removes and returns the data of the element at the end of the list.
        In:
            * dll: The doubly-linked list.
        Out:
            * The data of the element removed from the end of the list.
    """

    # Verify it is not empty
    if is_empty(dll):
        return None

    # Extract the data
    data = dll['tail']['data']

    # Adapt the pointers
    if dll['head'] == dll['tail']:
        dll['head'] = None
        dll['tail'] = None
    else:
        dll['tail'] = dll['tail']['prev']
        dll['tail']['next'] = None

    # Return the data
    return data
/**
 * This class provides methods to manipulate a doubly-linked list.
 * The list is represented with a head and tail pointing to the first and last nodes, respectively.
 * Each node is represented with data, prev, and next fields.
 */

import java.util.Optional;

 class DoublyLinkedList {
     // Define the Node structure
     static class Node {
         int data;
         Node prev;
         Node next;
 
         Node(int data) {
             this.data = data;
             this.prev = null;
             this.next = null;
         }
     }
 
     private Node head;
     private Node tail;
 
     // Initialize the list
     public DoublyLinkedList() {
         this.head = null;
         this.tail = null;
     }

    
    /**
     * Check if the list is empty.
     *
     * @return true if the list is empty, false otherwise.
     */
    public boolean isEmpty() {
        return head == null;
    }
    
     /**
      * Insert a new node with the given data at the beginning of the list.
      *
      * @param data The data to store in the new node.
      */
     public void insertAtBeginning(int data) {
         Node newNode = new Node(data);
         if (head == null) {
             head = tail = newNode;
         } else {
             newNode.next = head;
             head.prev = newNode;
             head = newNode;
         }
     }
 
     /**
      * Insert a new node with the given data at the end of the list.
      *
      * @param data The data to store in the new node.
      */
     public void insertAtEnd(int data) {
         Node newNode = new Node(data);
         if (tail == null) {
             head = tail = newNode;
         } else {
             newNode.prev = tail;
             tail.next = newNode;
             tail = newNode;
         }
     }
 
     /**
      * Delete the first node with the given data from the list.
      *
      * @param data The data of the node to delete.
      */
     public void deleteNode(int data) {
         Node current = head;
         while (current != null) {
             if (current.data == data) {
                 if (current.prev != null) {
                     current.prev.next = current.next;
                 } else {
                     head = current.next;
                 }
                 if (current.next != null) {
                     current.next.prev = current.prev;
                 } else {
                     tail = current.prev;
                 }
                 return;
             }
             current = current.next;
         }
     }

      /**
     * Remove and return the data from the node at the head of the list.
     *
     * @return The data from the node at the head of the list, or -1 if the list is empty.
     */
    public int popFromHead() {
        if (head == null) {
            throw new RuntimeException("Empty list");//notion of Exception seen in prog session 
        }
        int data = head.data;
        head = head.next;
        if (head != null) {
            head.prev = null;
        } else {
            tail = null;
        }
        return data;
    }

    /**
     * Remove and return the data from the node at the tail of the list.
     *
     * @return The data from the node at the tail of the list, or -1 if the list is empty.
     */
    public int popFromTail() {
        if (tail == null) {
            throw new RuntimeException("Empty list");
        }
        int data = tail.data;
        tail = tail.prev;
        if (tail != null) {
            tail.next = null;
        } else {
            head = null;
        }
        return data;
    }
 
     /**
      * Search for a node with the given data.
      *
      * @param data The data to search for.
      * @return An Optional containing the found node, or an empty Optional if no node was found.
      */
     public Optional<Node> searchNode(int data) {
         Node current = head;
         while (current != null) {
             if (current.data == data) {
                 return Optional.of(current);
             }
             current = current.next;
         }
         return Optional.empty();
     }
 
     /**
      * Display the data of all nodes in the list.
      */
     public void displayList() {
         Node current = head;
         while (current != null) {
             System.out.print(current.data + " ");
             current = current.next;
         }
         System.out.println();
     }
 }

3.3 — Comparaison des implémentations

À ce stade de l’activité pratique, vous devriez disposer de deux implémentations possibles d’une file :

  • Utilisation du type liste intégré (dans list_queue.py),
  • Utilisation du type liste doublement chaînée maison, bien que perfectible (dans dll_queue.py).

L’idée maintenant est de voir l’impact du choix d’une implémentation par rapport à une autre. En plus des deux implémentations précédemment mentionnées, une troisième implémentation utilisant le type deque python sera utilisée. Le type deque est similaire à la liste doublement chaînée mais optimisé comparé à notre version maison.

Le code suivant vise à comparer trois implémentations d’une file en se concentrant sur la primitive d’enfilage. 10 000 valeurs sont ajoutées à chaque implémentation de la file et le temps moyen nécessaire pour effectuer cette opération d’enfilage est calculé et affiché.

Copiez le code suivant dans un fichier nommé test_perf_queueing.py et exécutez ce code. Vérifiez également que les fichiers nommés list_queue.py et dll_queue.py sont situés dans le même répertoire que test_perf_queueing.py.

Sortie
"""
    compares the time needed to add elements to the front of a list and deque.
"""

# Needed imports
from collections import deque
from time import perf_counter
from typing import List, Deque, Callable, Any
import list_queue as lq
import dll_queue as dllq

# Number of insertions to perform
NB_OPS = 10_000

# Initialize the various structures
queue_using_list = lq.initialize()
queue_using_dll = dllq.initialize()
queue_using_deque = deque()


def average_time (func: Callable[[Any], None], times: int) -> float:
    
    """
        This function computes the average time needed to execute a function.
        In:
            * func:  Function to execute.
            * times: Number of times to execute the function.
        Out:
            * Average time needed to execute the function.
    """ 

    # Run the function multiple times
    total = 0.0
    for i in range(times):
        start = perf_counter()
        func(i)
        total += (perf_counter() - start) * 1e9
    
    # Return the average time
    return total / times



# Compute the average time needed to insert an element in the list-based queue
list_time = average_time(lambda i: lq.push(queue_using_list, i), NB_OPS)

# Compute the average time needed to insert an element in the doubly linked list-based queue
dll_time = average_time(lambda i: dllq.insert_at_end(queue_using_dll, i), NB_OPS)

# Compute the average time needed to insert an element in the deque-based queue
deque_time = average_time(lambda i: queue_using_deque.appendleft(i), NB_OPS)

# Evaluate the gain
gain = list_time / deque_time
gain2 = list_time / dll_time
gain3 = dll_time / deque_time

# Print results
print("----\n INSERTION \n---")
print(f"Average time list queue:               {list_time:.6} ns")
print(f"Average time doubly-linked list queue: {dll_time:.6} ns")
print(f"Average time deque:                    {deque_time:.6} ns")
print(f"deque over list:               {gain:.6}x faster")
print(f"doubly-linked list over list:  {gain2:.6}x faster")
print(f"deque over doubly-linked list: {gain3:.6}x faster")

# Compute the average time needed to remove an element in the list-based queue
list_time = average_time(lambda i: lq.pop(queue_using_list), NB_OPS)

# Compute the average time needed to remove an element in the doubly linked list-based queue
dll_time = average_time(lambda i: dllq.pop_from_beginning(queue_using_dll), NB_OPS)

# Compute the average time needed to remove an element in the deque-based queue
deque_time = average_time(lambda i: queue_using_deque.pop(), NB_OPS)

# Evaluate the gain
gain = list_time / deque_time
gain2 = list_time / dll_time
gain3 = dll_time / deque_time

# Print results
print("----\n DELETION \n---")
print(f"Average time list queue:               {list_time:.6} ns")
print(f"Average time doubly-linked list queue: {dll_time:.6} ns")
print(f"Average time deque:                    {deque_time:.6} ns")
print(f"deque over list:               {gain:.6}x faster")
print(f"doubly-linked list over list:  {gain2:.6}x faster")
print(f"deque over doubly-linked list: {gain3:.6}x faster")

Qu’observez-vous ?

Correction

Voici le genre de résultats que vous devriez obtenir :

Sortie
----
 INSERTION 
---
Average time list queue:               133.448 ns
Average time doubly-linked list queue: 429.624 ns
Average time deque:                    97.645 ns
deque over list:               1.36666x faster
doubly-linked list over list:  0.310616x faster
deque over doubly-linked list: 4.39986x faster
----
 DELETION 
---
Average time list queue:               763.207 ns
Average time doubly-linked list queue: 375.372 ns
Average time deque:                    91.0943 ns
deque over list:               8.3782x faster
doubly-linked list over list:  2.0332x faster
deque over doubly-linked list: 4.1207x faster

Comme on peut le voir, le type de structure de données utilisé pour implémenter une file peut fortement affecter l’efficacité de certaines primitives. Ici, supprimer un élément en première position d’une liste, même en utilisant le type liste intégré optimisé, est coûteux car cela nécessite de décaler physiquement, en mémoire, tous les éléments de la liste. Ainsi, une opération de défilement conduit à N opérations de décalage lorsque la liste contient N éléments. Cela explique la différence significative en termes d’efficacité comparée à la version liste doublement chaînée, qui ne fait que mettre à jour des pointeurs.

De plus, la comparaison entre la version maison de la liste doublement chaînée et le deque montre que vous devriez toujours privilégier les implémentations officielles lorsqu’elles sont disponibles.

4 — Suppression d’éléments dans une liste doublement chaînée

Complétez l’implémentation maison du type liste doublement chaînée avec une fonction qui supprime le premier élément de la liste ayant une valeur donnée (passée en paramètre de la fonction).

Correction
def delete_element (dll: Dict[str, Optional[Element]], data: Any) -> None:
    
    """
        Deletes the first element with the given data from the list.
        In:
            * dll: The doubly-linked list.
            * data: The data of the element to delete.
        Out:
            * None.
    """

    # Iterate over the list from the head to the tail until data is found
    current = dll['head']
    while current is not None:
        if current['data'] == data:

            # Adapt the pointers
            if current['prev'] is not None:
                current['prev']['next'] = current['next']
            else:
                dll['head'] = current['next']
            if current['next'] is not None:
                current['next']['prev'] = current['prev']
            else:
                dll['tail'] = current['prev']
            
            # Stop here
            return
        
        # Next element
        current = current['next']
/**
      * Delete the first node with the given data from the list.
      *
      * @param data The data of the node to delete.
      */
     public void deleteNode(int data) {
         Node current = head;
         while (current != null) {
             if (current.data == data) {
                 if (current.prev != null) {
                     current.prev.next = current.next;
                 } else {
                     head = current.next;
                 }
                 if (current.next != null) {
                     current.next.prev = current.prev;
                 } else {
                     tail = current.prev;
                 }
                 return;
             }
             current = current.next;
         }
     }

5 — Trier une liste (doublement chaînée)

Appliquez l’algorithme basique de tri par sélection pour transformer n’importe quelle liste doublement chaînée en une liste triée (dans l’ordre croissant).

Correction
def selection_sort (dll: Dict[str, Optional[Element]]) -> None:
    
    """
        Sorts the list using the basic selection sort algorithm.
        Here, sorting is done in place.
        In:
            * dll: The doubly linked list to be sorted.
        Out:
            * None.
    """

    # Nothing to do if empty structure
    if dll['head'] is None:
        return

    # Iterate over the structure
    current = dll['head']
    while current is not None:

        # Find the smallest value in the remaining part of the list (i.e., from current)
        min_node = current
        next_node = current['next']
        while next_node is not None:
            if next_node['data'] < min_node['data']:
                min_node = next_node
            next_node = next_node['next']

        # Swap the data
        if min_node != current:
            current['data'], min_node['data'] = min_node['data'], current['data']

        # Next element
        current = current['next']
 /**
     * Apply the selection sort algorithm on the list
     */
    public void selectionSort() {
        if (head != null) {
    
            Node current = head;
            while (current != null) {
                Node minNode = current;
                Node nextNode = current.next;
        
                while (nextNode != null) {
                    if (nextNode.data < minNode.data) {
                        minNode = nextNode;
                    }
                    nextNode = nextNode.next;
                }
        
                // Swap the data of the current node and the minNode
                if (minNode != current) {
                    int temp = current.data;
                    current.data = minNode.data;
                    minNode.data = temp;
                }
        
                current = current.next;
            }
        }
    }

6 — Liste doublement chaînée triée

Dans certains cas d’application, où les valeurs stockées sont ordinales, il peut être intéressant (pour éventuellement accélérer la récupération des valeurs) de maintenir un ordre des valeurs dans la liste. Étant donné que des valeurs entières sont stockées dans la liste, proposez une implémentation d’une fonction insert_element qui maintient les valeurs dans l’ordre décroissant (ou croissant).

Correction
def insert_order (dll: Dict[str, Optional[Element]], data: Any) -> None:
    
    """
        Insert a new element with the given data into the list, maintaining the elements in increasing order.
        In:
            * dll:  The doubly-linked list.
            * data: The data to store in the new Element.
        Out:
            * None.
    """

    # Create a new Element
    new_element = create_element(data)

    # If the list is empty, initialize it
    if dll['head'] is None:
        dll['head'] = dll['tail'] = new_element
    
    # Otherwise, find where to insert the data
    else:

        # Iterate over elements
        current = dll['head']
        while current is not None and current['data'] < data:
            current = current['next']

        # If it is the largest data seen, add to the end
        if current is None:
            insert_at_end(dll, data)

        # If it is the largest data seen, add to the beginning
        elif current['prev'] is None:
            insert_at_beginning(dll, data)

        # otherwise, adapt the pointers
        else:
            new_element['next'] = current
            new_element['prev'] = current['prev']
            current['prev']['next'] = new_element
            current['prev'] = new_element

7 — Optimisez vos solutions

Ce que vous pouvez faire maintenant est d’utiliser des outils d’IA tels que GitHub Copilot ou ChatGPT, soit pour générer la solution, soit pour améliorer la première solution que vous avez proposée ! Essayez de faire cela pour tous les exercices ci-dessus, pour voir les différences avec vos solutions.

Pour aller plus loin

8 — Convertir une expression infixe en expression postfixée

Écrivez une fonction infix_to_prefix qui convertit une expression infixe avec des entiers (entre parenthèses) en notation postfixée. Vous aurez besoin d’utiliser à la fois une pile et une file.

Par exemple, l’expression 3*(((12-3)/3)-1) sera convertie en : 3 12 3 - 3 / 1 - *

  • Les opérateurs valides sont +, -, * et /.
  • L’algorithme lit une expression arithmétique infixe et stocke le résultat de la conversion dans une file, qui est affichée.
  • L’algorithme utilisera une pile pour gérer les opérateurs, et une file pour construire l’expression postfixée.
  • Les valeurs numériques doivent être directement stockées dans la file, tandis que les opérateurs doivent attendre avant d’être ajoutés à la file après leurs opérandes.
  • Le traitement d’un opérateur, dépiler puis enfiler, doit se produire lorsqu’une parenthèse fermante est rencontrée.
  • Faites aussi attention à la priorité des opérateurs !
Correction
# Needed imports
import re


def infix_to_postfix (infix_expression: str) -> str:

    """
        This function uses a stack and a queue to transform an infix expression to a postfix expression.
        All expressions are supposed to be well parenthesized.
        In:
            * infix_expression: Expression to be transformed to postfix.
        Out:
            * Postfix expression equivalent to the input infix expression.
    """

    # Define the precedence of the operators
    precedence = {"*": 2, "/": 2, "+": 1, "-": 1, "(": 0, ")": 0}

    # Initialize the stack and the queue
    stack = []
    queue = []

    # Split the infix expression into tokens
    tokens = [t for t in re.split(r"(\+|\-|\*|\/|\(|\))", infix_expression) if t.strip() != ""]

    # Iterate over tokens
    for token in tokens:
        
        # If the token is an operand, add it to the queue
        if token.isnumeric():
            queue.append(token)

        # If the token is a left parenthesis, add it to the stack
        elif token == "(":
            stack.append(token)

        # If the token is a right parenthesis, pop all the operators from the stack and add them to the queue until a left parenthesis is found
        elif token == ")":
            while stack and stack[-1] != "(":
                queue.append(stack.pop())
            stack.pop()

        # If the token is an operator, pop all the operators from the stack with higher precedence and add them to the queue
        # Then, add the operator to the stack
        else:
            while stack and precedence[stack[-1]] >= precedence[token]:
                queue.append(stack.pop())
            stack.append(token)

    # Add the remaining operators to the queue
    while stack:
        queue.append(stack.pop())

    # Return the postfix expression
    return " ".join(queue)


# Test the function
print("3*(((12-3)/3)-1) transformed into", infix_to_postfix("3*(((12-3)/3)-1)"))
print("1+(2*3) transformed into", infix_to_postfix("1+(2*3)"))
print("(1+2)*3 transformed into", infix_to_postfix("(1+2)*3"))
print("3*(2+1) transformed into", infix_to_postfix("3*(2+1)"))
/**
 * To run this code, you need to have Java installed on your computer, then:
 * - Create a file named `Main.java` in a directory of your choice.
 * - Copy this code in the file.
 * - Open a terminal in the directory where the file is located.
 * - Run the command `javac Main.java` to compile the code.
 * - Run the command `java -ea Main` to execute the compiled code.
 * Note: '-ea' is an option to enable assertions in Java.
 */

// Needed imports
import java.util.Stack;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Main {

    /**
     * This function uses a stack and a queue to transform an infix expression to a postfix expression.
     * All expressions are supposed to be well parenthesized.
     *
     * @param infixExpression Expression to be transformed to postfix.
     * @return Postfix expression equivalent to the input infix expression.
     */
    public static String infixToPostfix(String infixExpression) {
        // Define the precedence of the operators
        var precedence = new java.util.HashMap<String, Integer>();
        precedence.put("*", 2);
        precedence.put("/", 2);
        precedence.put("+", 1);
        precedence.put("-", 1);
        precedence.put("(", 0);
        precedence.put(")", 0);

        // Initialize the stack and the queue
        Stack<String> stack = new Stack<>();
        List<String> queue = new ArrayList<>();

        // Split the infix expression into tokens
        Pattern pattern = Pattern.compile("(\\+|\\-|\\*|\\/|\\(|\\))");
        Matcher matcher = pattern.matcher(infixExpression);
        List<String> tokens = new ArrayList<>();
        int lastIndex = 0;
        while (matcher.find()) {
            if (matcher.start() > lastIndex) {
                tokens.add(infixExpression.substring(lastIndex, matcher.start()).trim());
            }
            tokens.add(matcher.group());
            lastIndex = matcher.end();
        }
        if (lastIndex < infixExpression.length()) {
            tokens.add(infixExpression.substring(lastIndex).trim());
        }

        // Iterate over tokens
        for (String token : tokens) {
            // If the token is an operand, add it to the queue
            if (token.matches("\\d+")) {
                queue.add(token);
            }

            // If the token is a left parenthesis, add it to the stack
            else if (token.equals("(")) {
                stack.push(token);
            }

            // If the token is a right parenthesis, pop all the operators from the stack and add them to the queue until a left parenthesis is found
            else if (token.equals(")")) {
                while (!stack.isEmpty() && !stack.peek().equals("(")) {
                    queue.add(stack.pop());
                }
                stack.pop();
            }

            // If the token is an operator, pop all the operators from the stack with higher precedence and add them to the queue
            // Then, add the operator to the stack
            else {
                while (!stack.isEmpty() && precedence.get(stack.peek()) >= precedence.get(token)) {
                    queue.add(stack.pop());
                }
                stack.push(token);
            }
        }

        // Add the remaining operators to the queue
        while (!stack.isEmpty()) {
            queue.add(stack.pop());
        }

        // Return the postfix expression
        return String.join(" ", queue);
    }


    /**
     * This is the entry point of your program.
     * It contains the first codes that are going to be executed.
     *
     * @param args Command line arguments received.
     */
    public static void main(String[] args) {
        // Test the function
        System.out.println("3*(((12-3)/3)-1) transformed into " + infixToPostfix("3*(((12-3)/3)-1)"));
        System.out.println("1+(2*3) transformed into " + infixToPostfix("1+(2*3)"));
        System.out.println("(1+2)*3 transformed into " + infixToPostfix("(1+2)*3"));
        System.out.println("3*(2+1) transformed into " + infixToPostfix("3*(2+1)"));
    }

}

9 — Calculer l’enveloppe convexe de Graham

L’algorithme de Graham est une méthode de géométrie algorithmique utilisée pour trouver l’enveloppe convexe d’un ensemble de points dans un plan 2D, avec une complexité $O(n \cdot log(n))$. L’enveloppe convexe est le plus petit polygone convexe pouvant englober tous les points donnés. Il utilise une pile pour détecter et supprimer efficacement les concavités dans la frontière.

Écrivez l’algorithme de Graham qui prend :

  • En entrée – Un ensemble de n points dans le plan, chacun représenté par ses coordonnées (abscisse et ordonnée).
  • En sortie – Une liste de points qui sont les sommets du polygone entourant l’enveloppe convexe, dans l’ordre dans lequel ils apparaissent sur ce polygone.

Vous devez suivre les étapes de l’algorithme de Graham :

  1. Trouver le point ayant la plus petite coordonnée y. S’il y a plusieurs points avec la même coordonnée y la plus basse, choisissez celui avec la plus petite coordonnée x. Ce point est appelé point pivot.
  2. Trier l’ensemble des points par ordre croissant de l’angle qu’ils forment avec le point pivot et l’axe des x. Tout algorithme de tri généraliste est approprié pour cela.
  3. Parcourir la liste triée des points pour construire l’enveloppe convexe :
    • On commence par le pivot.
    • Puis on examine les points triés. Pour chaque point, on détermine d’abord si le passage des deux points immédiatement précédents ce point constitue un virage à gauche (orientation anti-horaire) ou un virage à droite (orientation horaire).
    • En cas de virage à droite, cela signifie que le point qui terminait le polygone en cours de création (le point en rouge) est à gauche du segment formé par le point avant-dernier du polygone en cours de création et le point considéré (le point en noir). Dans ce cas, le dernier point (celui en rouge) ne fait pas partie de l’enveloppe convexe et doit donc être rejeté. Ces rejets sont répétés tant qu’il y a un “virage à droite”.
    • Si l’algorithme rencontre un “virage à gauche”, il passe au point suivant dans le tableau trié.

Graham algorithm right turn

Déterminer si trois points constituent un “virage à gauche” ou un “virage à droite” ne nécessite pas de calculer l’angle réel entre les deux segments de droite, et peut être réalisé avec une simple arithmétique. Pour trois points $(x_1, y_1)$, $(x_2, y_2)$ et $(x_3, y_3)$, calculez simplement la direction du produit vectoriel des deux vecteurs $[(x_1, y_1), (x_2, y_2)]$ et $[(x_1, y_1), (x_3, y_3)]$, donnée par le signe de l’expression $(x_2 - x_1)(y_3 - y_1) - (y_2 - y_1)(x_3 - x_1)$ :

  • Si le résultat est nul, les points sont alignés.
  • S’il est positif, les trois points forment un “virage à gauche” ou orientation anti-horaire.
  • Si le résultat est négatif, c’est un “virage à droite” ou orientation horaire.
Correction
# Needed imports
import math
import matplotlib.pyplot as plt



def min_y (points: list[tuple[int, int]]) -> tuple[int, int]:
    
    """
        Returns the points having the smallest y coordinate.
        In case of tie, the point with the smallest x coordinate is returned.
        In:
            * points: List of points.
        Out:
            * The point with the smallest y coordinate.
    """

    # Check all the points
    min_idx = 0
    for i in range(1, len(points)):
        if points[i][1] < points[min_idx][1]:
            min_idx = i
        elif points[i][1] == points[min_idx][1] and points[i][0] < points[min_idx][0]:
            min_idx = i
    
    # Return the found one
    return points[min_idx]



def right_turn (p1: tuple[int, int], p2: tuple[int, int], p3: tuple[int, int]) -> None:
    
    """
        Determines if the points p1, p2, p3 make a right turn.
        In:
            * p1: Point 1.
            * p2: Point 2.
            * p3: Point 3.
        Out:
            * None.

    """

    # Check value of Euclidean distance
    d = (p2[0] - p1[0]) * (p3[1] - p1[1]) - (p2[1] - p1[1]) * (p3[0] - p1[0])
    return d < 0



def graham_scan (points: list[tuple[int, int]]) -> list[tuple[int, int]]:

    """
        Determines the convex hull of a set of points using the Graham scan algorithm.
        In:
            * points: List of points.
        Out:
            * Points forming the convex hull.
    """

    # Get the pivot
    pivot = min_y(points)

    # Sort the points by the angle they make with the pivot and the x-axis
    sorted_points = sorted(points, key=lambda x: math.atan2(x[1] - pivot[1], x[0] - pivot[0]))
    hull = []
    for point in sorted_points:

        # Remove the point from the stack if one finds a right turn
        while len(hull) > 1 and right_turn(hull[-2], hull[-1], point):
            hull.pop()

        # Add the point to the stack and move to the next one
        hull.append(point)
    
    # Return the hull
    return hull



# Make some tests
if __name__ == "__main__":

    # Test on a few poins
    points = [(0, 0), (1, 1), (1.5, 2.5), (3.5, 1), (2, 2), (3, 0), (3.5, 2), (0, 3), (3, 3), (2, 3.5)]        
    hull = graham_scan(points)
    print("Convex hull:", hull)

    # Plot the hull
    plt.plot([x for x, y in points], [y for x, y in points], 'ro')
    plt.plot([x for x, y in hull] + [hull[0][0]], [y for x, y in hull] + [hull[0][1]], 'bo-')
    plt.show()
 import java.util.*;

public class ConvexHull {
    /**
     * This function calculates the Euclidean distance between two points.
     * @param p1 point 1 defined by its two coordinates - array of 2 integers
     * @param  p2 point 2 defined by its two coordinates - array of 2 integers
     * @return the Euclidean distance.
     */

    public static double euclideanDistance(int[] p1, int[] p2) {
        return Math.pow(p2[0] - p1[0], 2) + Math.pow(p2[1] - p1[1], 2);
    }

    /**
     * This function calculates the direction of the vector product of the two vectors defined by the points p, q and p, r
     * @param p point 1 defined by its two coordinates - array of 2 integers
     * @param q point 2 defined by its two coordinates - array of 2 integers
     * @param r point 3 defined by its two coordinates - array of 2 integers
     * @return  true if the vector product is negative, false otherwise
     */

    public static boolean rightTurn(int[] p, int[] q, int[] r) {
        int direction = (q[0] - p[0]) * (r[1] - p[1]) - (q[1] - p[1]) * (r[0] - p[0]);
        return direction < 0;
    }

    /**
     * This function implements the Graham algorithm to compute the convex hull of point in a 2D plane.  
     * @param points list of points in the plane, each represented by its coordinates (abscissa, ordinate) - a tuple with two integer elements.
     * @return list of integer arrays representing the points forming the convex envelope.
     */

    public static List<int[]> grahamScan(List<int[]> points) {
        // Find the point with the lowest y-coordinate, if there are multiple points with the same y-coordinate, pick the one with the smallest x-coordinate
        int[] minY = Collections.min(points, Comparator.comparingInt((int[] p) -> p[1]).thenComparingInt(p -> p[0]));

        // Sort the set of points in increasing order of the angle they and the pivot point make with the x-axis.
        points.sort((p1, p2) -> {
            double angle1 = Math.atan2(p1[1] - minY[1], p1[0] - minY[0]);
            double angle2 = Math.atan2(p2[1] - minY[1], p2[0] - minY[0]);
            if (angle1 != angle2) {
                return Double.compare(angle1, angle2);
            } else {
                return Double.compare(euclideanDistance(minY, p1), euclideanDistance(minY, p2));
            }
        });

        // Create the convexe hull
        Stack<int[]> hull = new Stack<>();
        for (int[] point : points) {
            while (hull.size() > 1 && rightTurn(hull.get(hull.size() - 2), hull.peek(), point)) {
                hull.pop(); // remove the point from the stack
            }
            hull.push(point); // add the point to the stack and move to the next one
        }

        return new ArrayList<>(hull);
    }
    public static void main(String[] args) {
        // Example 
        List<int[]> points = Arrays.asList(
            new int[]{0, 0}, new int[]{1, 1}, new int[]{1, 2}, new int[]{2, 2},
            new int[]{3, 0}, new int[]{3, 2}, new int[]{0, 3}, new int[]{3, 3},
            new int[]{2, 4}
        );

        List<int[]> hull = grahamScan(points);
        System.out.println("Convex hull:");
        for (int[] point : hull) {
            System.out.println(Arrays.toString(point));
        }
    }
}

Pour aller plus loin

10 — Utiliser des pointeurs dans un langage où ils sont explicites

Essayez l’implémentation d’une liste d’entiers en C suivant le tutoriel disponible à l’url. Vous pourriez avoir besoin d’un tutoriel d’introduction à la programmation en C cependant :)