Practical activity
Duration2h30Présentation & objectifs
Voici une liste d’exercices pour étudier la complexité d’un algorithme, construire des algorithmes en se concentrant sur les arbres binaires de recherche et les algorithmes randomisés.
L’objectif 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 basée uniquement sur un nom de fichier judicieusement choisi.
Pour l’entraînement, 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 de manière fondamentale et pratique.
De plus, nous vous fournissons les solutions aux exercices.
Assurez-vous de les consulter uniquement après avoir 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 — Complexité des opérations sur les listes
Dans cet exercice, vous allez essayer de comprendre l’utilisation, en termes de complexité, des opérations sur les listes (classiques) et les listes triées.
1.1 — Évaluer la complexité
Voici quelques opérations sur ces structures, avec quelques implémentations possibles (pas nécessairement optimales) :
-
Opérations sur listes classiques – Les fonctions suivantes manipulent une liste classique :
def create_list (): """ Create an empty list. In: * None. Out: * An empty list. """ return [] def insert_list (l: list[int], element: int) -> None: """ Insert an element at the end of the list In: * l: The list. * element: The element to insert. Out: * None. """ l.append(element) def delete_list (l: list[int], element: int) -> None: """ Delete the first occurrence of the element from the list. In: * l: The list. * element: The element to delete. Out: * None """ # Find the element to delete for i in range(len(l)): if l[i] == element: # Shift elements to the left for j in range(i, len(l) - 1): l[j] = l[j + 1] # Remove the last element l.pop() break def find_min_list (l: list[int]) -> int | None: """ Find and return the minimum element in the list. In: * l: The list. Out: * The minimum element in the list. """ # Check that not empty if not l: return None # Initialize the minimum value to the first element min_value = l[0] # Find the minimum value in the list for value in l: if value < min_value: min_value = value # Return the minimum value return min_value # Test the functions print("Testing regular list operations:") a_list = create_list() insert_list(a_list, 10) insert_list(a_list, 5) insert_list(a_list, 20) print("list after inserts:", a_list) delete_list(a_list, 5) print("list after deleting 5:", a_list) print("Minimum element in the list:", find_min_list(a_list))
import java.util.Arrays; /** * Regular list Operations */ public class RegularlistOperations { /** * Create an empty list * @return An empty list */ public static int[] createlist() { return new int[0]; } /** * Insert an element at the end of the list * @param list The list * @param element The element to insert */ public static int[] insertlist(int[] list, int element) { // lists are immutable in Java, so we need to create a new list int[] newlist = Arrays.copyOf(list, list.length + 1); // insert the element at the end of the new list newlist[list.length] = element; // return the new list return newlist; } /** * Delete the first occurrence of the element from the list * @param list The list * @param element The element to delete */ public static int[] deletelist(int[] list, int element) { for (int i = 0; i < list.length; i++) { // find the element to delete if (list[i] == element) { // create a new list without the element to delete int[] newlist = new int[list.length - 1]; // copy the elements before the element to delete System.arraycopy(list, 0, newlist, 0, i); // copy the elements after the element to delete System.arraycopy(list, i + 1, newlist, i, list.length - i - 1); // return the new list return newlist; } } // return the original list if the element is not found return list; } /** * Find and return the minimum element in the list * @param list The list * @return The minimum element in the list */ public static Integer findMinlist(int[] list) { // check if the list is empty if (list.length == 0) { return null; } // initialize the minimum value to the first element int minValue = list[0]; // find the minimum value in the list for (int value : list) { // check if the current value is less than the minimum value if (value < minValue) { // update the minimum value minValue = value; } } // return the minimum value return minValue; } public static void main(String[] args) { System.out.println("Testing Regular list Operations:"); int[] list = createlist(); list = insertlist(list, 10); list = insertlist(list, 5); list = insertlist(list, 20); System.out.println("list after inserts: " + Arrays.toString(list)); list = deletelist(list, 5); System.out.println("list after deleting 5: " + Arrays.toString(list)); System.out.println("Minimum element in the list: " + findMinlist(list)); } }
-
Opérations sur listes triées – La fonction suivante manipule une liste triée :
def create_sorted_list (): """ Create an empty list . In: * None. Out: * An empty list. """ return [] def insert_sorted_list (l: list[int], element: int) -> None: """ Insert an element into the sorted list while maintaining the list's order. In: * l: The sorted list. * element: The element to insert. Out: * None """ # Find the correct position for the new element index = 0 while index < len(l) and l[index] < element: index += 1 # Shift elements to make space for the new element l.append(0) for i in range(len(l) - 1, index, -1): l[i] = l[i - 1] # Insert the new element at the correct position l[index] = element def delete_sorted_list (l: list[int], element: int) -> None: """ Delete the first occurrence of the element from the sorted list. In: * l: The sorted list. * element: The element to delete. Out: * None """ # Find the element to delete for i in range(len(l)): if l[i] == element: # Shift elements to the left for j in range(i, len(l) - 1): l[j] = l[j + 1] # Remove the last element l.pop() break def find_min_sorted_list (l: list[int]) -> int | None: """ Find and return the minimum element in the sorted list. In: * l: The sorted list. Out: * The minimum element in the list. """ # Check that not empty if not l: return None # The minimum element is the first element in the sorted list return l[0] # Test sorted list operations print("\nTesting Sorted list Operations:") sorted_list = create_sorted_list() insert_sorted_list(sorted_list, 10) insert_sorted_list(sorted_list, 5) insert_sorted_list(sorted_list, 20) print("Sorted list after inserts:", sorted_list) delete_sorted_list(sorted_list, 5) print("Sorted list after deleting 5:", sorted_list) print("Minimum element in the sorted list:", find_min_sorted_list(sorted_list))
import java.util.Arrays; /** * Sorted list operations. */ public class SortedlistOperations { /** * Creates an empty sorted list. * * @return an empty integer list. */ public static int[] createSortedlist() { return new int[0]; } /** * Inserts an element into the sorted list while maintaining the sorted order. * * @param list the original sorted list. * @param element the element to be inserted. * @return a new sorted list with the element inserted. */ public static int[] insertSortedlist(int[] list, int element) { int index = 0; // Find the correct position for the new element while (index < list.length && list[index] < element) { index++; } // In java, lists are immutable, so we need to create a new list with the element inserted int[] newlist = new int[list.length + 1]; // Copy the elements before the index System.arraycopy(list, 0, newlist, 0, index); // Copy the elements after the index System.arraycopy(list, index, newlist, index + 1, list.length - index); // Insert the element at the correct position newlist[index] = element; // Return the new sorted list return newlist; } /** * Deletes an element from the sorted list. * * @param list the original sorted list. * @param element the element to be deleted. * @return a new sorted list with the element removed, or the original list if the element is not found. */ public static int[] deleteSortedlist(int[] list, int element) { for (int i = 0; i < list.length; i++) { // Find the element to delete if (list[i] == element) { // Create a new list without the element to delete int[] newlist = new int[list.length - 1]; // Copy the elements before the index System.arraycopy(list, 0, newlist, 0, i); // Copy the elements after the index System.arraycopy(list, i + 1, newlist, i, list.length - i - 1); // Return the new sorted list return newlist; } } // Return the original list if the element is not found return list; } /** * Finds the minimum element in the sorted list. * * @param list the sorted list. * @return the minimum element in the list, or null if the list is empty. */ public static Integer findMinSortedlist(int[] list) { // Check if the list is empty if (list.length == 0) { return null; } // The minimum element is the first element in the sorted list return list[0]; } /** * Main method to test the sorted list operations. * * @param args command line arguments. */ public static void main(String[] args) { System.out.println("\nTesting Sorted list Operations:"); int[] sortedlist = createSortedlist(); sortedlist = insertSortedlist(sortedlist, 10); sortedlist = insertSortedlist(sortedlist, 5); sortedlist = insertSortedlist(sortedlist, 20); System.out.println("Sorted list after inserts: " + Arrays.toString(sortedlist)); sortedlist = deleteSortedlist(sortedlist, 5); System.out.println("Sorted list after deleting 5: " + Arrays.toString(sortedlist)); System.out.println("Minimum element in the sorted list: " + findMinSortedlist(sortedlist)); } }
Quelles sont les complexités temporelles (au pire cas) des différentes opérations ?
Comment la complexité temporelle dans une liste triée se compare-t-elle à celle d’une liste classique ?
1.2 — Écrivez vos propres fonctions
Maintenant, pour chacune des listes ci-dessus, écrivez une fonction qui, pour chaque élément de la liste, compte le nombre d’autres éléments plus grands ou égaux dans la liste.
La fonction doit retourner un dictionnaire qui, pour chaque élément, associe son résultat.
Comme votre fonction doit pouvoir comparer les éléments, un assert
pourrait être intéressant à utiliser.
Nous supposons également que la liste ne doit contenir que des valeurs distinctes.
Quelles sont les complexités de ces fonctions ?
2 — Nombre premier
Un nombre premier est un nombre strictement positif qui n’est divisible (au sens de la division entière) que par 1 et lui-même (par exemple : 2, 3, 5, 7, 11, etc.).
Nous voulons déterminer si un entier est un nombre premier.
La méthode basique consiste à prouver que le nombre à traiter n’est pas premier en cherchant un diviseur qui donne un reste égal à 0.
Si un tel diviseur est trouvé, le nombre n’est pas premier.
Dans un fichier nommé prime.py
, écrivez plusieurs fonctions pour tester si un nombre donné est premier.
-
Fonction
complete_check_prime (number: int) -> Tuple[bool, int]
– Utilisez la bouclefor
pour tester tous les diviseurs de 2 à (number - 1).
La fonction retournera si le nombre est premier ou non.
Elle retournera également le nombre d’itérations effectuées pour obtenir le résultat.
Indiquez ensuite sa complexité algorithmique. -
Fonction
lless_dividers_prime (number: int, limit: int) -> Tuple[bool, int]
– La première façon de faire est de traiter séparément le cas des nombres pairs.
Si le nombre est divisible par 2, il n’est pas intéressant de vérifier les diviseurs pairs.
La recherche doit être restreinte aux diviseurs impairs (3, 5, 7, etc.).
La deuxième approche consiste à travailler sur des nombres ayant plusieurs diviseurs, comme 24, pour mettre en évidence une propriété des diviseurs qui limite la plage de recherche, comme la racine carrée d’un nombre.Écrivez la fonction qui implémente cette méthode dans
prime_bis.py
.
Le programme retournera si le nombre est premier ou non.
Il retournera également le nombre d’itérations effectuées pour obtenir le résultat.
La bouclefor
peut être remplacée par une bouclewhile
, plus adaptée à ce type de traitement.L’argument
limit
peut être égal au nombre, ou à sa racine carrée.
Indiquez la complexité algorithmique pour chaque cas.Notez le nombre d’itérations retourné lorsque vous le testez sur des nombres premiers et non premiers pairs et impairs.
Si vous souhaitez évaluer la performance de votre algorithme, vous pouvez le tester avec un nombre premier plus grand, comme 10007 ou 1e9+7
qui est, soit dit en passant, le plus grand nombre premier stocké dans un entier 32 bits.
3 — Arbre binaire de recherche
Un arbre binaire de recherche (BST) est un arbre binaire dans lequel chaque nœud a une clé, et pour tout nœud x, la clé dans le sous-arbre gauche de x est inférieure à la clé dans x, et la clé dans le sous-arbre droit de x est supérieure à la clé dans x (voir le cours relatif aux BST pour plus de détails).
Nous allons implémenter un arbre binaire de recherche en Python à partir de zéro.
Nous fournirons les fonctions suivantes :
tree_insert(tree: , value)
pour insérer la valeur dans l’arbre.tree_search(tree, value)
pour rechercher la valeur dans l’arbre.tree_delete(tree, value)
pour supprimer la valeur de l’arbre.
Afin d’encoder le concept de nœud, nous utiliserons une liste de 3 éléments [value, left, right]
où value
est la valeur du nœud, left
est l’enfant gauche, et right
est l’enfant droit.
Les enfants gauche et droit sont aussi des listes, ou None
s’ils n’existent pas.
3.1 — Insertion
L’algorithme d’insertion est le suivant :
- Si l’arbre est vide, créez un nouveau nœud avec la valeur donnée et retournez l’arbre.
- Si la valeur est inférieure à la valeur de la racine, insérez la valeur dans le sous-arbre gauche et retournez l’arbre.
- Si la valeur est supérieure à la valeur de la racine, insérez la valeur dans le sous-arbre droit et retournez l’arbre.
- Si la valeur est égale à la valeur de la racine, retournez l’arbre (inchangé).
Puis retournez la racine de l’arbre.
Notez que l’algorithme est récursif.
Créez un fichier nommé binary_tree.py
et implémentez la fonction tree_insert(tree, value)
.
3.2 — Recherche
L’algorithme de recherche est le suivant :
- Si l’arbre est vide, retournez
False
. - Si la valeur est égale à la valeur de la racine, retournez
True
. - Si la valeur est inférieure à la valeur de la racine, recherchez dans le sous-arbre gauche.
- Si la valeur est supérieure à la valeur de la racine, recherchez dans le sous-arbre droit.
L’algorithme est récursif.
Implémentez la fonction tree_search(tree, value)
dans le fichier binary_tree.py
.
3.3 — Suppression
L’algorithme de suppression est plus complexe que ceux d’insertion et de recherche.
Le successeur en ordre d’un nœud dans un arbre binaire de recherche (BST) est le nœud avec la plus petite clé supérieure à la clé du nœud donné.
Nous considérerons trois cas :
- Si le nœud à supprimer n’a pas d’enfants, on peut simplement le supprimer.
- Si le nœud n’a qu’un seul enfant, on peut remplacer le nœud par son enfant.
- Si le nœud a deux enfants, il faut trouver le successeur en ordre du nœud, remplacer le nœud par le successeur en ordre, supprimer le successeur en ordre, puis ajouter un nouveau lien entre les enfants du successeur en ordre et les parents du successeur en ordre.
Implémentez la fonction tree_delete(tree, value)
dans le fichier binary_tree.py
.
4 — Implémentation et test de la méthode congruentielle linéaire en Python
4.1 — Méthode congruentielle linéaire
Implémentez la méthode congruentielle linéaire (LCM) en Python pour générer une séquence de nombres pseudo-aléatoires.
La formule LCM est donnée par :
$$X_{n} = (aX_{n-1} + c) \mod m \;,$$
où :
- $X_{n-1}$ est le nombre aléatoire courant.
- $X_{n}$ est le nombre aléatoire suivant.
- $m$ est le module.
- $a$ est le multiplicateur.
- $c$ est l’incrément.
La fonction (appelée linear_congruential_generator
) doit accepter les paramètres $m$, $a$, $c$, $X_0$, et $n$ (le nombre de nombres aléatoires à générer), et générer des séquences en utilisant deux ensembles de paramètres :
- Ensemble 1 : $m = 100, a = 17, X_0 = 27, c = 43$.
- Ensemble 2 : $a = 7^5, m = 2^{31}-1, X_0 = 27, c = 0$.
Lors de la génération de nombres aléatoires, il est important de tester la qualité des nombres générés.
Nous pouvons tester l’uniformité des nombres générés en visualisant un histogramme et en calculant la moyenne et l’écart-type.
# Matplotlib library is required
import matplotlib.pyplot as plt
# First, normalize the sequence to be between 0 and 1
normalized_sequence = [x / m for x in sequence]
# Then, plot the histogram of the generated numbers
plt.hist(normalized_sequence, bins=10, edgecolor='black')
plt.title("Histogram of Generated Numbers")
plt.xlabel("Value")
plt.ylabel("Frequency")
plt.show()
# Calculate mean and standard deviation
mean = np.mean(normalized_sequence)
std_dev = np.std(normalized_sequence)
print(f"Mean: {mean}, Standard Deviation: {std_dev}")
4.2 — Test d’autocorrélation
Testez l’autocorrélation en mesurant la corrélation entre des nombres adjacents dans la séquence.
Pour ce faire, calculez le coefficient de corrélation entre la séquence et la séquence décalée d’une position.
La mesure la plus connue de la dépendance entre deux quantités est le coefficient de corrélation produit-moment de Pearson (PPMCC), ou “coefficient de corrélation de Pearson”.
Le PPMCC est une mesure de la corrélation linéaire entre deux variables $X$ et $Y$, donnant une valeur comprise entre +1 et −1 inclus, où 1 est une corrélation linéaire positive totale, 0 est aucune corrélation linéaire, et −1 est une corrélation linéaire négative totale.
Ici, $X$ est la séquence et $Y$ est la séquence décalée d’une position.
La formule du PPMCC est donnée par :
$$\begin{align*}
r_{XY} &= \frac{\sum_{i=1}^{n}{(X_i - \bar{X})(Y_i - \bar{Y})}}{\sqrt{\sum_{i=1}^{n}{(X_i - \bar{X})^2}}\sqrt{\sum_{i=1}^{n}{(Y_i - \bar{Y})^2}}}
\end{align*} \;,$$
où :
- $X_i$ et $Y_i$ sont les valeurs des séquences $X$ et $Y$ à la position $i$.
- $\bar{X}$ et $\bar{Y}$ sont les moyennes des séquences $X$ et $Y$.
- $n$ est la longueur des séquences.
Implémentez la fonction pearson_autocorrelation(x, y)
, exécutez-la sur les séquences générées, et affichez l’autocorrélation pour chaque séquence.
Qu’observez-vous ? Pouvez-vous affirmer que la séquence est aléatoire ?
4.3 — Test de la longueur du cycle
Nous voulons maintenant tester la longueur du cycle de la LCM.
La longueur du cycle est le nombre d’itérations avant que la séquence ne commence à se répéter.
Pour ce faire, nous allons stocker les nombres générés dans un ensemble et vérifier si le nombre est déjà dans l’ensemble.
Si le nombre est déjà dans l’ensemble, nous avons trouvé un cycle et pouvons retourner la longueur du cycle.
Si le nombre n’est pas dans l’ensemble, nous l’ajoutons à l’ensemble et continuons à générer des nombres.
Implémentez la fonction test_cycle_length(sequence)
et exécutez-la sur les séquences générées.
5 — 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
On dirait que cette section est vide !
Quelque chose que vous auriez aimé voir ici ?
Faites-le nous savoir sur le serveur Discord !
Peut-être pouvons-nous l’ajouter rapidement.
Sinon, cela nous aidera à améliorer le cours pour l’année prochaine !
Pour aller au-delà
6 — Coefficient de corrélation de Pearson incrémental
Le coefficient de corrélation de Pearson peut être calculé de manière incrémentale, ce qui peut être utile lorsqu’on traite de grands ensembles de données.
L’article suivant fournit une bonne explication du calcul incrémental du coefficient de corrélation de Pearson :
Tschumitschew, K., Klawonn, F. (2012). Incremental Statistical Measures.
In: Sayed-Mouchaweh, M., Lughofer, E. (eds) Learning in Non-Stationary Environments. Springer, New York, NY. https://doi.org/10.1007/978-1-4419-8020-5_2
Implémentez le calcul incrémental du coefficient de corrélation de Pearson et testez-le sur les séquences générées.