CI4 : La banque Lannister
Durée des exercices obligatoires : 2h10 en présentiel + 20mn en hors présentiel
- Exercice 1 : obligatoire (facile, ∼45mn)
- Exercice 2 : obligatoire (difficile, ∼1h45)
- Exercice 3 : entraînement (facile, ∼10mn)
- Exercice 4 : entraînement (difficile)
Dans cet exercice, nous concevons une petite banque en nous focalisant sur le problème de la recherche rapide d'un client à partir de son nom. Ce problème de recherche rapide est assez général : n'importe quelle application Web comme Facebook doit retrouver un compte à partir d'un identifiant de connexion.
À haut niveau, le problème du stockage des clients d'une banque est assez similaire au problème du stockage des monstres dans une armée de monstres. Puisque le nombre de clients d'une banque est non connu et non borné, nous avons jusqu'à maintenant vu qu'on pouvait stocker les clients dans un tableau extensible ou dans une liste chaînée. Aucune de ces deux structures n'est adéquate :
- Un tableau extensible est inadéquat, car une banque possède un grand nombre (non borné) de clients. Comme vu en CI3, si on utilisait un tableau extensible pour stocker nos clients, on prendrait alors le risque de devoir étendre (et donc copier) un immense tableau lors de l'ajout d'un nouveau client, ce qui grèverait les performances.
- Une liste chaînée est inadéquate, car cette structure est particulièrement inefficace pour effectuer une recherche. En effet, avec une liste chaînée, le seul moyen d'atteindre le nième élément de la liste est de traverser exhaustivement les n-1 nœuds précédents. Si la liste chaînée possède N nœuds et si les recherches sont équitablement réparties sur l'ensemble des clients, il faudrait donc, en moyenne, parcourir N/2 nœuds avant de trouver un client. Si N est très grand, cette complexité ruine les performances.
Comme aucune des structures de données que nous avons étudiées jusqu'à maintenant n'est adéquate, nous vous présentons les arbres binaires de recherche dans ce TP. Cette structure de données a l'avantage de pouvoir gérer un nombre non borné d'éléments tout en étant efficace lors de l'ajout ou de la recherche d'éléments. L'algorithme que nous allons étudier dans ce TP est (relativement) simple à mettre en œuvre, mais peut poser des problèmes d'inefficacité que nous vous présentons en fin d'exercice. Pour palier ces problèmes d'inefficacité, il existe les arbres rouges-noirs ou les arbres 2-3-4, et nous invitons les étudiants curieux et amateurs de jolis algorithmes à aller les étudier.
Les comptes (∼45mn – facile – obligatoire)
Dans un premier temps, nous nous focalisons sur la conception des comptes des clients de la banque. Cet exercice a pour but de vous faire manipuler des méthodes d'instances dans un cadre conceptuel simple. Pour cela, nous commençons par utiliser des méthodes de classe, puis nous modifions le code de façon à les remplacer par des méthodes d'instance.
- name : cette chaîne de caractères indique le nom du propriétaire du compte en banque,
- balance : ce nombre de type double indique le solde du compte.
- Math.random() qui renvoie un nombre flottant (double) compris entre 0 et 1,
- Math.floor(double d) qui renvoie la partie entière du nombre flottant d.
- pour la méthode main qui permet de démarrer votre application,
- pour les méthodes create qui permettent d'allouer et d'initialiser des objets (vous verrez au cours suivant une façon plus élégante d'initialiser un objet).
- un nombre positif si le propriétaire du compte est plus grand que name (en suivant l'ordre lexicographique),
- 0 si le propriétaire est égal à name,
- un nombre négatif sinon.
La banque (∼1h45 – difficile – obligatoire)
Maintenant que vous avez conçu les comptes, vous pouvez construire une banque pour stocker les comptes.
Pour cela, nous vous proposons d'utiliser un arbre binaire de recherche. De façon générale, un arbre binaire est constitué de nœuds et chaque nœud possède deux fils : un fils gauche et un fils droit.
Pour un arbre binaire de recherche, chaque nœud est étiqueté par une valeur définie sur un ensemble totalement ordonné. Dans notre cas, il s'agit d'un compte en banque et l'ordre utilisé est l'ordre lexicographique sur les noms des propriétaires des comptes. Le principe d'un arbre binaire de recherche est que les étiquettes se trouvant sous le fils gauche (inclus) d'un nœud soient toutes plus petites que l'étiquette du nœud, et que celle se trouvant sous le fils droit (inclus) soient toutes plus grandes.
La figure ci-dessous illustre le principe. Si on prend, par exemple, le nœud étiqueté par le compte de Jaime, on peut constater que les nœuds se trouvant à gauche sont plus petits et que ceux se trouvant à droite sont plus grands.
De façon à simplifier le code, nous considérons aussi des nœuds dits vides qui ne sont pas représentés sur la figure. Un nœud vide est un nœud dont l'étiquette est la référence null. Par opposition, nous considérons qu'un nœud est non vide si son étiquette est différente de la référence null.
Dans notre mise en œuvre, un nœud non vide possède toujours deux fils, ces derniers pouvant être vides ou non vides. Si le fils est un nœud vide, c'est que ce fils n'existe pas et que nous sommes en bas de l'arbre. Par exemple, dans l'exemple ci-dessus, le nœud non vide étiqueté avec le compte de Cersei possède deux fils : celui de gauche est un nœud non vide et celui de droite est un nœud vide non représenté dans la figure.
- créant une nouvelle banque,
- appelant lookupOrCreate avec comme paramètre la chaîne de caractères "Tyrion".
- Si l'étiquette du compte du nœud courant (this) est non vide, vous avez trois choix :
- Si l'étiquette du compte est strictement plus petite que le nom passé en paramètre, c'est que le compte - s'il existe - se trouve à droite. Vous devez donc renvoyer le résultat de l'appel à lookupOrCreate sur le nœud droite.
- Si l'étiquette du compte est strictement plus grande, c'est que le compte - s'il existe - se trouve à gauche,
- Sinon, c'est que l'étiquette du compte est égale au nom passé en paramètre. Vous avez donc trouvé le compte associé au nom passé en paramètre et il suffit de renvoyer l'étiquette (le compte) référencé par le nœud courant.
- Sinon, c'est que vous êtes arrivé en bas de l'arbre sans avoir trouvé le compte
associé au paramètre. Vous devez donc transformer le nœud courant qui est vide
en un nœud non vide associé à un nouveau compte, lui-même associé au nom
passé en paramètre. Techniquement, vous devez :
- allouer un nouveau nœud vide et l'associer au champ gauche du nœud courant,
- allouer un nouveau nœud vide et l'associer au champ droite du nœud courant,
- créer un compte associé au nom passé en paramètre, l'associer au champ étiquette du nœud courant et enfin renvoyer ce nouveau compte.
Cependant, en moyenne, la fonction a une complexité en , ce qui explique l'attrait pour cette structure.
bonus
Discussion (∼10mn en hors présentiel – moyen – entraînement)
Pour aller plus loin (entraînement)
La hauteur d'un arbre peut-être calculée de manière récursive.
- Les deux fils du nœud à supprimer sont des feuilles. Alors, il faut remplacer le nœud à supprimer par une feuille.
- Un seul fils du nœud à supprimer est une feuille. On fait alors remontrer le fils non-feuille d'un niveau.
- Les deux fils ne sont pas des feuilles. On remplace alors le nœud à supprimer par le minimum du fils de gauche (ou le maximum du fils de droite) et on supprime ce nouveau nœud du fils.
- Préfixe : on insère, dans l'ordre, le nœud actuel, la représentation du fils de gauche et la représentation du fils de droite.
- Infixe : on insère, dans l'ordre, la représentation du fils de gauche, le nœud actuel et la représentation du fils de droite.
- Postfixe : on insère, dans l'ordre, la représentation du fils de gauche, la représentation du fils de droite et le nœud actuel.
- Préfixe : Tyrion -> Jaime -> Cersei -> Alton -> Kevan -> Lancel -> Martyn -> Tywin -> Willem
- Infixe : Alton -> Cersei -> Jaime -> Kevan -> Lancel -> Martyn -> Tyrion -> Tywin -> Willem
- Postfixe : Alton -> Cersei -> Martyn -> Lancel -> Kevan -> Jaime -> Willem -> Tywin -> Tyrion
On pourra utiliser une fonction auxiliary récursive.