Algo Synthese Finie
Algo Synthese Finie
Algo Synthese Finie
: synthèse
Chapitre 1 : Les tableaux à 2 dimensions
1.1) Définition :
La dimension d’un tableau est le nombre d’indices qu’on utilise pour faire référence à un de ses
éléments.
1.2) Notations :
1.2.1) Déclarer :
Pour déclarer un tableau à 2 dimensions, on écrira :
Pour accéder à une case du tableau on donnera les deux indices entre crochets. On considère que la
première colonne portent le numéro ( l’indice ) 0
Notez que la vue sous forme de tableau avec des lignes et des colonnes est une vision humaine. Il n’y
a pas de lignes ni de colonnes en mémoire.
Exemple )
Un tableau déclarer :
Int[4x5] nombres
On peut le visualiser en 4 lignes et 5 colonnes
On pourrait aussi visualiser un tableau à deux dimensions comme un tableau à une dimension dont
chacun des éléments est lui-même un tableau à une dimension. La vision << tableau de tableaux >>
donnerait
Dans cette représentation, le tableau nombres est d’abord décomposé à un premier niveau en quatre
éléments auxquels on accède par le premier indice. Ensuite, chaque élément de premier niveau est
décomposé en cinq éléments de deuxième niveau accessibles par le deuxième indice
1.2.5 ) Complexité :
Un algorithme est idéalement :
Pour comparer des algorithmes en terme d’utilisation de temps et d’espace, un outil incontournable
est le complexité.
Instructions élémentaires :
Une des façons les plus simples pour quantifier la complexité temporelle est de compter le nombre
d’instructions élémentaires utilisées par un algorithme.
Pour s’affranchir d’un calcul précis du nombre d’instructions, nous allons nous intéresser à la
complexité en « grand O ». Dans ce qui suit, n représente un entier qui estime la taille des données
d’un algorithme. Il s’agit souvent de la taille d’un tableau donné en paramètre, ou d’un entier donné
en paramètre. La signification de la notation « grand O » est la suivante :
▷ On dit qu’un algorithme (qui dépend a priori d’un entier n) a une complexité en O(1) si le nombre
d’instructions exécutées ne dépend pas de n. On dit qu’il est en temps constant.
▷ On dit qu’un algorithme (qui dépend a priori d’un entier n) a une complexité en O(n) si le nombre
d’instructions exécutées est toujours inférieur à une quantité proportionnelle à n. (Notons qu’un
algorithme en O(1) est également en O(n).)
▷ Similairement un algorithme aura une complexité en O(n 2 ) si le nombre d’instructions exécutées
est toujours inférieur à une quantité proportionnelle à n 2 .
De manière générale, un algorithme a une complexité en O(f(n)) si le nombre d’instructions
exécutées est toujours inférieur à une quantité proportionnelle à f(n).
L’arithmétique des grands 0 :
Une complexité dans le pire des cas d’un algorithme peut être calculée à partir des complexités des
constituants de cet algorithme. Voici quelques règles naturelles. Considérons des algorithmes
dépendant d’un paramètre n :
▷ A1 en O(f(n)),
▷ A2 en O(g(n)) et
▷ A3 en O(h(n)).
Concaténation :
Lorsque l’on réalise l’algorithme A1 suivi de l’algorithme A2, l’algorithme résultant est en O(f(n) +
g(n)).
Appel de méthode :
Un appel à une méthode compte pour une instruction, dès lors seule la complexité de la méthode
elle-même rentre en ligne de compte 3 .
1.3) Parcours :
Nos algorithmes sont valables quel que soit le type des éléments. Utilisons un type T, pour désigner
un type quelconque
T[n x m] tab
La diagonale descendante. Si le tableau est carré (n = m) on peut aussi envisager le parcours des deux
diagonales
Pour la diagonale descendante, parfois appelé diagonale principale, les éléments à visiter sont en
coordonnées (0,0), (1,1), (2,2), …, (n – 1, n – 1)
Print tab[i,i]
La diagonale montante. Pour la diagonale montante d’un tableau carré (n = m), représentée ci-
dessous, on peut envisager deux solutions :
Avec deux indices, ou
Avec un seule, en se basant sur le fait que
Void afficherDiagonaleMontante(T[n x n] tab)
Int col
Lg = n-1
Lg = lg – 1
Parcours par lignes et par colonnes. Les deux parcours les plus courants sont les parcours << ligne par
ligne >> et << colonne par colonne >>. Les tableaux suivants montrent dans quel ordre chaque case
est visitée dans ces deux parcours
Mais on peut obtenir le même résultat avec une seule boucle si l’indice sert juste à compter le
nombre de passages (n∗m) et que les indices de lignes et de colonnes sont gérés manuellement.
L’algorithme suivant montre ce que ça donne pour un parcours ligne par ligne. La solution pour un
parcours colonne par colonne est similaire et laissée en exercice.
L’avantage de cette solution apparaitra quand on verra des situations plus difficiles.
Imaginons que l’on désire manipuler par programme une liste de courses. Cette liste va varier ; sa
taille n’est donc pas fixée. Utiliser un tableau à cet effet n’est pas l’idéal : la taille d’un tableau ne peut
plus changer une fois le tableau créé. On peut bien sûr s’en sortir avec un tableau, mais cela
implique :
1. "fromage"
2. "pain"
3. "salami"
On pourrait ajouter un élément en fin de liste, par exemple de l’eau, pour obtenir la liste :
1. "fromage"
2. "pain"
3. "salami"
4. "eau"
On pourrait aussi supprimer un élément de la liste, par exemple le pain, et obtenir :
1. "fromage"
2. "salami"
3. "eau"
On pourrait aussi insérer un élément dans la liste, par exemple une baguette, ce qui décale, de facto,
la position des suivants :
1. "fromage"
2. "salami"
3. "baguette"
4. "eau"
Et au niveau du code ? Nous aimerions pouvoir écrire des choses comme :
Dans cet exemple nous avons créé une liste de chaînes de caractères (String). De façon générale on
pourra imaginer une liste d’entiers, de booléens, ou d’autres choses encore. Voici par exemple un
morceau de code manipulant une liste d’entiers :
Après sa création, la liste est vide. Ensuite, elle passe par les états suivants :
Enfin, le dernier appel la vide complètement
2.2 ) Comment implémenter une liste en mémoire
Rappelons qu’une classe permet de définir un nouveau type de données, et donc d’encapsuler au
sein d’une seule entité différentes propriétés.
Rappelons aussi que pour les tableaux, le problème est nettement plus simple : il suffit de réserver le
bon nombre de cases en mémoire, et on accède à chaque case via son indice. Comme la taille d’un
tableau ne peut plus changer une fois créé, ceci suffit.
Dans ce chapitre, nous allons voir qu’il est en fait possible d’écrire une telle classe de deux façons
distinctes, à savoir :
1. au moyen d’un tableau géré dynamiquement par la classe. Le tableau est encapsulé par la classe et
remplacé par un tableau plus grand lorsque cela devient nécessaire (une ArrayList),
2. en chaînant les éléments les uns à la suite des autres en mémoire (une LinkedList).
Nous verrons également que ces choisir une de ces implémentations (ArrayList ou LinkedList) impacte
les performances : la complexité des algorithmes implémentés par les méthodes add et get par
exemples varie d’une classe à l’autre.
2.3) Les ArrayList
Nous avons vu dans le cours de DEV1 qu’un tableau a une taille fixe, appelée sa taille physique. On y
accède en Java avec la propriété length (par exemple : tab.length). Lorsqu’on ne connait pas a priori le
nombre d’éléments à mettre dans le tableau, on peut instancier un tableau « suffisamment grand »
pour y mettre les éléments. Le nombre de cases effectivement utilisées est alors appelé la taille
logique. Cette taille doit être ajustée au fur et à mesure qu’on ajoute ou retire des éléments.
Pour rappel voici deux méthodes écrites en DEV1 :
Ces deux méthodes acceptent le tableau registered ainsi que la taille logique nRegistered en
paramètre, modifient le tableau et retournent la nouvelle taille logique.
Le principe d’une ArrayList est alors d’implémenter une liste au moyen d’un tableau encapsulé dans la
classe, la taille logique du tableau étant géré par les méthodes de la classe.
2.5) L’interface d’une liste
Inspirons-nous de ce qui existe en Java.
Remarque : Nous ne précisons pas encore la façon dont ces méthodes sont implémentées, vu que
cela va dépendre de la représentation mémoire choisie pour notre liste (ArrayList ou LinkedList). Nous
donnerons ces implémentations dans les sections qui suivent. Les méthodes fournies ci-dessous ne
constituent donc qu’une interface.
▷ add(pos,value) insère un élément à une position donnée (entre 0 et taille-1). L’élément qui s’y
trouvait est décalé d’une position ainsi que tous les éléments suivants.
Nous utiliserons des valeurs de type « chaînes de caractères » (String), mais l’implémentation pourra
s’adapter à d’autres types de données .
Les attributs La classe a donc deux attributs privés : un tableau et sa taille logique.
Ces attributs sont accessibles par le constructeur et toutes les méthodes de la classe.
Constructeur Le rôle du constructeur est d’initialiser les attributs de la classe.
Nous allons définir deux constructeurs. Dans le premier, nous recevons un entier qui correspond à la
taille physique du tableau. Ceci correspond au nombre maximal de String que notre objet sera
capable de contenir.
Insertion La classe offrira notamment deux méthodes pour insérer des valeurs :
Notons que chacune de ces méthodes est responsable de modifier la taille logique. Il n’y a donc plus
d’utilité de retourner cette taille logique. Une implémentation de la première méthode add serait :
La complexité est O(1). Notez que les deux dernières lignes pourraient se réduire à :
Pour la seconde méthode, notons d’abord une conséquence de la simple existence de cette méthode.
En effet, cette méthode permettant d’insérer une valeur à une position précise donnée, ceci implique
que la position des valeurs a de l’importance. Dès lors, insérer une valeur à une position doit décaler
les autres valeurs, afin que l’ordre relatif des valeurs soit préservé. Muni de cette information nous
savons maintenant implémenter la méthode :
Dans la mesure où il faut potentiellement parcourir tout le tableau, cette méthode est en O(n).
Nous aborderons plus tard l’implémentation pour les méthodes de suppression (remove,
removePos).
Taille (size) Comme c’est la classe qui est responsable de tenir à jour sa taille logique, il doit y avoir
une méthode pour récupérer la taille (logique) courante :
Trouver un élément Pour retrouver un élément, nous allons définir deux méthodes.
Supprimer un élément Pour supprimer un élément, nous définissons deux méthodes :
Vider la liste Nous définissons également une méthode permettant de vider complètement la liste.
2.7) Liste chainées
Nous avons vu une première classe, ArrayList, qui implémente toutes les méthodes de l’interface List.
Nous allons en voir une autre. Cette fois, l’idée est que chaque valeur de la liste sera contenue dans
un objet (de type Node), qui « connaît » son successeur. Pour pouvoir parcourir une liste, il suffit donc
de connaître le premier Node, et de suivre les successeurs successifs. Ceci est illustré sur le dessin ci-
dessous :
Pour cette raison, "une liste" sera un objet (de type SinglyLinkedList) contenant simplement une
référence vers un Node.
Remarquez qu’il serait aussi possible de chaîner la liste dans les deux sens, comme illustré ci-
dessous :
Ce type de liste chaînée est appelée liste chaînée double. Lorsqu’on mentionne une liste chaînée, il
est donc important de savoir de quoi on parle (une liste simplement chaînée ? une liste double ? 5 ).
L’implémentation que nous allons donner dans les sections qui suivent correspond à une liste chaînée
simple, ce qui explique le nom de la classe : "SinglyLinkedList".
Cohérence interne : il faut qu’il n’y ait pas de cycles (c’est-à-dire un noeud qui référence un nœud qui
référence un nœud . . . ultimement référence un noeud précédent). En particulier la chaîne devra
forcément se terminer par null.
Voyons comment implémenter certaines méthodes de notre interface selon ce principe.
Constructeur Le rôle du constructeur est d’initialiser les attributs de la classe. Pour rappel le
constructeur de nos listes crée une liste vide. Il n’y a donc aucune valeur, et donc aucun Node.
Insertion Rappelons les méthodes à implémenter :
Accéder à un élément avec son indice (get, set) avec findElemAtPos nous avons fait l’essentiel du
travail pour implémenter get et set.
La taille Contrairement au cas de ArrayList, la taille n’est pas un attribut de notre classe . Pour
compter le nombre d’éléments, il faut donc parcourir la liste.
isEmpty Pour déterminer si une liste est vide, il est naturel de se demander si la taille vaut 0.
Cependant, notre méthode size a une complexité linéaire. Une implémentation en temps constant est
simplement :
▷ modifier la classe Node pour obtenir une version doublement chaînée, parcourable dans les deux
sens.
Chapitre 3) La pile
3.1) Définition :
Une pile est une collection d’éléments admettant les fonctionnalités suivantes :
On ne peut donc pas parcourir une pile, ou consulter directement le n-ième élément. Les opérations
permises avec les piles sont donc peu nombreuses, mais c’est précisément là leur spécificité : elles ne
sont utilisées en informatique que dans des situations particulières où seules ces opérations sont
requises et utilisées. Paradoxalement, on implémentera une pile en restreignant des structures plus
riches aux seules opérations autorisées par les piles. Cette restriction permet de n’utiliser que les
fonctionnalités nécessaires de la pile, simplifiant ainsi son utilisation.
Des exemples d’utilisations sont la gestion de la mémoire par les micro-processeurs, l’évaluation des
expressions mathématiques en notation polonaise inverse, la fonction « ctrl-Z » dans un traitement
de texte qui permet d’annuler les frappes précédentes, la mémorisation des pages web visitées par
un navigateur, etc. Nous les utiliserons aussi plus loin dans ce cours pour parcourir les arbres et les
graphes.
3.2) Implémentation orienté-objet :
3.2.2) Remarques :
▷ Théoriquement, et dans la majorité des utilisations, la pile est infinie, c’est-à-dire qu’on peut y
ajouter un nombre indéterminé d’éléments. Dans certaines situations, on peut cependant imposer
une capacité maximale à la pile. Nous aborderons ce cas particulier dans les exercices.
▷ Lors de l’implémentation de la classe, il faudra songer à envoyer un message d’erreur lorsqu’on
utilise les méthodes sommet et dépiler si la pile est vide. Si la pile possède une taille maximale, alors
c’est empiler qui doit générer une erreur lorsque la pile est pleine.
▷ Nous avons utilisé ici des noms de méthodes neutres indépendants de tout langage de
programmation. Dans la littérature anglaise, on trouvera souvent push, top et pop en lieu et place de
empiler, sommet et dépiler.
3.3) Exemple d’utilisation :
Afin d’illustrer l’utilisation d’une classe implémentant l’interface Pile, nous donnons pour exemple un
algorithme qui lit une suite d’enregistrements d’un fichier fileIn (de type Info) et les reproduit en
ordre inverse dans le fichier fileOut.
Chapitre 4) La file
4.1) Définition :
Une file est une collection d’éléments admettant les fonctionnalités suivantes :
▷ on peut toujours ajouter un élément à la collection
La file est donc une collection de données de type premier entré, premier sorti. L’analogie avec une
file de clients à un guichet est évidente : c’est le premier arrivé qui est le premier servi, et il est très
malvenu d’essayer de doubler une personne dans une file ! Noter qu’une fois entré dans une file – au
sens informatique du terme – on ne peut pas en sortir par l’arrière, le seul scénario possible pour en
sortir est d’attendre patiemment son tour et d’arriver en tête de la file. De même que pour la pile, on
ne peut donc pas non plus parcourir une file, ou consulter directement le n-ième élément. Les files
sont très utiles en informatique, citons par exemple la création de mémoire tampon (buffer) dans de
nombreuses applications, les processeurs multitâches qui doivent accorder du temps-machine à
chaque tâche, la file d’attente des impressions pour une imprimante, ...
4.2) Implémentation orienté-objet :
Comme pour la pile, l’interface File ne contient qu’un nombre restreint de méthodes qui
correspondent aux quelques opérations permises avec cette structure : ajouter un élément (« enfiler
»), consulter l’élément de tête, et le retirer (« défiler »).
4.2.2) Remarques :
▷ De même que dans le chapitre précédent, la file est supposée infinie, c’est-à-dire qu’on peut y
ajouter un nombre indéterminé d’éléments..
▷ Dans l’implémentation, il faudra songer à envoyer un message d’erreur lorsqu’on utilise les
méthodes tête et défiler si la file est vide. Si la file possède une taille maximale, alors c’est enfiler qui
doit générer une erreur lorsque la file est pleine.
nous avons commencé le cours d’algorithmique de DEV1 en situant les notions de problème et de
résolution. Nous avons vu qu’un problème bien spécifié s’inscrit dans le schéma :
▷ les données « simples » (variables isolées : entiers, réels, chaines, caractères, booléens) ;
▷ le tableau, qui contient un nombre déterminé de variables de même type, accessibles via un indice
ou plusieurs pour les tableaux multidimensionnels ;
▷ les objets, combinant une série d’attributs et des méthodes agissant sur ces attributs ;
▷ la liste, qui peut contenir un nombre indéfini d’éléments de même type. Chacune de ces structures
possède ses spécificités propres quant à la façon d’accéder aux valeurs, de les parcourir, de les
modifier, d’ajouter ou de supprimer des éléments à la collection. D’autres structures particulières
s’ajouteront dans le cours d’algorithmique d’ALG3 : les listes chainées, les piles, les files, les arbres, les
associations et les graphes.